From b126cc3cb3fc5c73dc19b52ba8e5ddca0457c4f1 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 16 Aug 2017 15:47:56 -0600 Subject: [PATCH 001/121] initial impl of cached action counts using ee --- .vscode/settings.json | 8 ++++ graph/mutators/action.js | 6 +-- models/comment.js | 10 ++++- services/actions.js | 81 +++++++++++++++++++++++++--------------- services/comments.js | 43 +++++++++++++++++++++ services/events.js | 17 +++++++++ 6 files changed, 128 insertions(+), 37 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 services/events.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..838540390 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "eslint.autoFixOnSave": true, + "editor.tabSize": 2, + "editor.rulers": [ + 80 + ], + "files.trimTrailingWhitespace": true +} diff --git a/graph/mutators/action.js b/graph/mutators/action.js index d62d3b4c5..0b0d76895 100644 --- a/graph/mutators/action.js +++ b/graph/mutators/action.js @@ -1,4 +1,3 @@ -const ActionModel = require('../../models/action'); const ActionsService = require('../../services/actions'); const UsersService = require('../../services/users'); const errors = require('../../errors'); @@ -52,10 +51,7 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id, * @return {Promise} resolves to the deleted action, or null if not found. */ const deleteAction = ({user}, {id}) => { - return ActionModel.findOneAndRemove({ - id, - user_id: user.id - }); + return ActionsService.delete({id, user_id: user.id}); }; module.exports = (context) => { diff --git a/models/comment.js b/models/comment.js index 352172e57..39d233b8e 100644 --- a/models/comment.js +++ b/models/comment.js @@ -66,15 +66,23 @@ const CommentSchema = new Schema({ enum: COMMENT_STATUS, default: 'NONE' }, + + // parent_id is the id of the parent comment (null if there is none). parent_id: String, + // Counts to store related to actions taken on the given comment. + action_counts: { + default: {}, + type: Object, + }, + // Tags are added by the self or by administrators. tags: [TagLinkSchema], // Additional metadata stored on the field. metadata: { default: {}, - type: Object + type: Object, } }, { timestamps: { diff --git a/services/actions.js b/services/actions.js index 74c3bdad3..864666a07 100644 --- a/services/actions.js +++ b/services/actions.js @@ -1,6 +1,30 @@ const ActionModel = require('../models/action'); const _ = require('lodash'); const errors = require('../errors'); +const events = require('./events'); + +const findOneAndUpdate = async (query, update, options = {}) => new Promise((resolve, reject) => { + ActionModel.findOneAndUpdate(query, update, Object.assign({}, options, { + + // Use raw result to get `updatedExisting`. + passRawResult: true, + + // Ensure that if it's new, we return the new object created. + new: true, + + // Perform an upsert in the event that this doesn't exist. + upsert: true, + + // Set the default values if not provided based on the mongoose models. + setDefaultsOnInsert: true + }), (err, doc, raw) => { + if (err) { return reject(err); } + if (raw.lastErrorObject.updatedExisting) { + return reject(new errors.ErrAlreadyExists(raw.value)); + } + return resolve(raw.value); + }); +}); module.exports = class ActionsService { @@ -19,46 +43,26 @@ module.exports = class ActionsService { * @param {String} action the new action to the item * @return {Promise} */ - static insertUserAction(action) { + static async insertUserAction(action) { // Actions are made unique by using a query that can be reproducable, i.e., // not containing user inputable values. - let query = { + let foundAction = await findOneAndUpdate({ action_type: action.action_type, item_type: action.item_type, item_id: action.item_id, user_id: action.user_id, - group_id: action.group_id - }; + group_id: action.group_id, + }, { - // Create/Update the action. - return new Promise((resolve, reject) => { - ActionModel.findOneAndUpdate( - query, { - - // Only set when not existing. - $setOnInsert: action, - }, { - - // Ensure that if it's new, we return the new object created. - new: true, - - // Use raw result to get `updatedExisting`. - passRawResult: true, - - // Perform an upsert in the event that this doesn't exist. - upsert: true, - - // Set the default values if not provided based on the mongoose models. - setDefaultsOnInsert: true - }, (err, doc, raw) => { - if (err) { return reject(err); } - if (raw.lastErrorObject.updatedExisting) { - return reject(new errors.ErrAlreadyExists(raw.value)); - } - return resolve(raw.value); - }); + // Only set when not existing. + $setOnInsert: action, }); + + // Emit that there was a new action created. + events.emit('actions.new', foundAction); + + return foundAction; } /** @@ -190,6 +194,21 @@ module.exports = class ActionsService { }); } + static async delete({id, user_id}) { + let action = await ActionModel.findOneAndRemove({ + id, + user_id, + }); + if (!action) { + return; + } + + // Emit that the action was deleted. + events.emit('actions.delete', action); + + return action; + } + /** * Finds all comments ids for a specific action. * @param {String} action_type type of action diff --git a/services/comments.js b/services/comments.js index af7593a8e..c9bf92153 100644 --- a/services/comments.js +++ b/services/comments.js @@ -5,6 +5,49 @@ const ActionsService = require('./actions'); const SettingsService = require('./settings'); const errors = require('../errors'); +const events = require('./events'); + +// When a new action is created, modify the comment. +events.on('actions.new', async (action) => { + if (!action || action.item_type !== 'COMMENTS') { + return; + } + + const ACTION_TYPE = action.action_type.toLowerCase(); + + try { + await CommentModel.update({ + id: action.item_id, + }, { + $inc: { + [`action_counts.${ACTION_TYPE}`]: 1, + } + }); + } catch (err) { + console.error(`Can't increment the action_counts.${ACTION_TYPE}:`, err); + } +}); + +// When an action is deleted, modify the comment. +events.on('actions.delete', async (action) => { + if (!action || action.item_type !== 'COMMENTS') { + return; + } + + const ACTION_TYPE = action.action_type.toLowerCase(); + + try { + await CommentModel.update({ + id: action.item_id, + }, { + $inc: { + [`action_counts.${ACTION_TYPE}`]: -1, + } + }); + } catch (err) { + console.error(`Can't decrement the action_counts.${ACTION_TYPE}:`, err); + } +}); module.exports = class CommentsService { diff --git a/services/events.js b/services/events.js new file mode 100644 index 000000000..f32ddfcd1 --- /dev/null +++ b/services/events.js @@ -0,0 +1,17 @@ +const {EventEmitter2} = require('eventemitter2'); +const debug = require('debug')('talk:services:events'); +const enabled = require('debug').enabled('talk:services:events'); + +const events = new EventEmitter2({ + wildcard: true, +}); + +// If event debugging is enabled, bind the debugger to all events being emitted +// and log a debug message. +if (enabled) { + events.onAny(function(event) { + debug(`[${event}] ${arguments.length - 1} argument${arguments.length - 1 === 1 ? '' : 's'}`); + }); +} + +module.exports = events; From d4536cb1a2e712d466cad433deaa9f8542b39d42 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 16 Aug 2017 16:01:22 -0600 Subject: [PATCH 002/121] replaced emit with emitAsync --- services/actions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/actions.js b/services/actions.js index 864666a07..93668506d 100644 --- a/services/actions.js +++ b/services/actions.js @@ -60,7 +60,7 @@ module.exports = class ActionsService { }); // Emit that there was a new action created. - events.emit('actions.new', foundAction); + events.emitAsync('actions.new', foundAction); return foundAction; } @@ -204,7 +204,7 @@ module.exports = class ActionsService { } // Emit that the action was deleted. - events.emit('actions.delete', action); + events.emitAsync('actions.delete', action); return action; } From 165d07cae99c2b05bc559d63c9ff210d47b6489b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 17 Aug 2017 11:56:58 -0600 Subject: [PATCH 003/121] improved event layout, added db indicies --- models/asset.js | 2 +- models/comment.js | 17 +++++ services/actions.js | 39 +++++++++-- services/comments.js | 92 ++++++++++++++----------- services/events/constants.js | 2 + services/{events.js => events/index.js} | 5 ++ 6 files changed, 108 insertions(+), 49 deletions(-) create mode 100644 services/events/constants.js rename services/{events.js => events/index.js} (83%) diff --git a/models/asset.js b/models/asset.js index d3a40852d..b2e49dcc0 100644 --- a/models/asset.js +++ b/models/asset.js @@ -72,7 +72,7 @@ AssetSchema.index({ subsection: 'text', author: 'text' }, { - background: true + background: true, }); /** diff --git a/models/comment.js b/models/comment.js index 39d233b8e..972f37e46 100644 --- a/models/comment.js +++ b/models/comment.js @@ -102,6 +102,23 @@ CommentSchema.index({ background: false }); +// Add an index that is optimized for sorting based on the action count data. +CommentSchema.index({ + 'created_at': 1, + 'action_counts': 1, +}, { + background: true, +}); + +// Add an index that is optimized for sorting based on the created_at timestamp +// but also good at locating comments that have a specific asset id. +CommentSchema.index({ + 'asset_id': 1, + 'created_at': 1, +}, { + background: true, +}); + CommentSchema.virtual('edited').get(function() { return this.body_history.length > 1; }); diff --git a/services/actions.js b/services/actions.js index 93668506d..8ee50df61 100644 --- a/services/actions.js +++ b/services/actions.js @@ -2,8 +2,21 @@ const ActionModel = require('../models/action'); const _ = require('lodash'); const errors = require('../errors'); const events = require('./events'); +const { + ACTIONS_NEW, + ACTIONS_DELETE, +} = require('./events/constants'); -const findOneAndUpdate = async (query, update, options = {}) => new Promise((resolve, reject) => { +/** + * findOnlyOneAndUpdate will perform a fondOneAndUpdate on the mongo collection + * and ensure that the found record wasn't already found. This is essentially + * a "findOneOrUpsert". + * + * @param {object} query the query operation for the mongo findOneAndUpdate op + * @param {object} update the update operation for the mongo findOneAndUpdate op + * @param {object} options the options operation for the mongo findOneAndUpdate op + */ +const findOnlyOneAndUpdate = async (query, update, options = {}) => new Promise((resolve, reject) => { ActionModel.findOneAndUpdate(query, update, Object.assign({}, options, { // Use raw result to get `updatedExisting`. @@ -30,6 +43,7 @@ module.exports = class ActionsService { /** * Finds an action by the id. + * * @param {String} id identifier of the action (uuid) */ static findById(id) { @@ -38,6 +52,7 @@ module.exports = class ActionsService { /** * Inserts an action. + * * @param {String} item_id identifier of the item (uuid) * @param {String} user_id user id of the action (uuid) * @param {String} action the new action to the item @@ -47,7 +62,7 @@ module.exports = class ActionsService { // Actions are made unique by using a query that can be reproducable, i.e., // not containing user inputable values. - let foundAction = await findOneAndUpdate({ + let foundAction = await findOnlyOneAndUpdate({ action_type: action.action_type, item_type: action.item_type, item_id: action.item_id, @@ -60,13 +75,14 @@ module.exports = class ActionsService { }); // Emit that there was a new action created. - events.emitAsync('actions.new', foundAction); + events.emitAsync(ACTIONS_NEW, foundAction); return foundAction; } /** * Finds actions in an array of ids. + * * @param {String} ids array of user identifiers (uuid) */ static async findByItemIdArray(item_ids) { @@ -84,6 +100,7 @@ module.exports = class ActionsService { /** * Fetches the action summaries for the given asset, and comments around the * given user id. + * * @param {[type]} asset_id [description] * @param {[type]} comments [description] * @param {String} [current_user_id=''] [description] @@ -110,7 +127,8 @@ module.exports = class ActionsService { } /** - * Returns summaries of actions for an array of ids + * Returns summaries of actions for an array of ids. + * * @param {String} ids array of user identifiers (uuid) */ static getActionSummaries(item_ids, current_user_id = '') { @@ -182,8 +200,9 @@ module.exports = class ActionsService { ]); } - /* + /** * Finds all comments for a specific action. + * * @param {String} action_type type of action * @param {String} item_type type of item the action is on */ @@ -194,6 +213,13 @@ module.exports = class ActionsService { }); } + /** + * delete will remove the record from the collection if it exists. Otherwise + * it will do nothing. This will then return the deleted action. + * + * @param {object} param the action to use as the query source, for which we + * only look at the id, user_id. + */ static async delete({id, user_id}) { let action = await ActionModel.findOneAndRemove({ id, @@ -204,13 +230,14 @@ module.exports = class ActionsService { } // Emit that the action was deleted. - events.emitAsync('actions.delete', action); + events.emitAsync(ACTIONS_DELETE, action); return action; } /** * Finds all comments ids for a specific action. + * * @param {String} action_type type of action * @param {String} item_type type of item the action is on */ diff --git a/services/comments.js b/services/comments.js index c9bf92153..4c57907e4 100644 --- a/services/comments.js +++ b/services/comments.js @@ -6,48 +6,10 @@ const SettingsService = require('./settings'); const errors = require('../errors'); const events = require('./events'); - -// When a new action is created, modify the comment. -events.on('actions.new', async (action) => { - if (!action || action.item_type !== 'COMMENTS') { - return; - } - - const ACTION_TYPE = action.action_type.toLowerCase(); - - try { - await CommentModel.update({ - id: action.item_id, - }, { - $inc: { - [`action_counts.${ACTION_TYPE}`]: 1, - } - }); - } catch (err) { - console.error(`Can't increment the action_counts.${ACTION_TYPE}:`, err); - } -}); - -// When an action is deleted, modify the comment. -events.on('actions.delete', async (action) => { - if (!action || action.item_type !== 'COMMENTS') { - return; - } - - const ACTION_TYPE = action.action_type.toLowerCase(); - - try { - await CommentModel.update({ - id: action.item_id, - }, { - $inc: { - [`action_counts.${ACTION_TYPE}`]: -1, - } - }); - } catch (err) { - console.error(`Can't decrement the action_counts.${ACTION_TYPE}:`, err); - } -}); +const { + ACTIONS_NEW, + ACTIONS_DELETE, +} = require('./events/constants'); module.exports = class CommentsService { @@ -333,3 +295,49 @@ module.exports = class CommentsService { return CommentModel.find(query); } }; + +//============================================================================== +// Event Hooks +//============================================================================== + +// When a new action is created, modify the comment. +events.on(ACTIONS_NEW, async (action) => { + if (!action || action.item_type !== 'COMMENTS') { + return; + } + + const ACTION_TYPE = action.action_type.toLowerCase(); + + try { + await CommentModel.update({ + id: action.item_id, + }, { + $inc: { + [`action_counts.${ACTION_TYPE}`]: 1, + } + }); + } catch (err) { + console.error(`Can't increment the action_counts.${ACTION_TYPE}:`, err); + } +}); + +// When an action is deleted, modify the comment. +events.on(ACTIONS_DELETE, async (action) => { + if (!action || action.item_type !== 'COMMENTS') { + return; + } + + const ACTION_TYPE = action.action_type.toLowerCase(); + + try { + await CommentModel.update({ + id: action.item_id, + }, { + $inc: { + [`action_counts.${ACTION_TYPE}`]: -1, + } + }); + } catch (err) { + console.error(`Can't decrement the action_counts.${ACTION_TYPE}:`, err); + } +}); diff --git a/services/events/constants.js b/services/events/constants.js new file mode 100644 index 000000000..bc6b962b9 --- /dev/null +++ b/services/events/constants.js @@ -0,0 +1,2 @@ +module.exports.ACTIONS_DELETE = 'actions.delete'; +module.exports.ACTIONS_NEW = 'actions.new'; diff --git a/services/events.js b/services/events/index.js similarity index 83% rename from services/events.js rename to services/events/index.js index f32ddfcd1..214e23d65 100644 --- a/services/events.js +++ b/services/events/index.js @@ -14,4 +14,9 @@ if (enabled) { }); } +// The default error handler. +events.on('error', (err) => { + console.error('events error:', err); +}); + module.exports = events; From a9abd558958a587441da38716ce2a47e31203b08 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 17 Aug 2017 15:32:47 -0600 Subject: [PATCH 004/121] added group_id support to action counts, cli verifier --- bin/cli | 1 + bin/cli-verify | 48 +++++++++ bin/verifications/database/comments.js | 141 +++++++++++++++++++++++++ bin/verifications/database/index.js | 12 +++ services/comments.js | 53 +++++----- 5 files changed, 229 insertions(+), 26 deletions(-) create mode 100755 bin/cli-verify create mode 100644 bin/verifications/database/comments.js create mode 100644 bin/verifications/database/index.js diff --git a/bin/cli b/bin/cli index 52d67c9af..4eee09f70 100755 --- a/bin/cli +++ b/bin/cli @@ -16,6 +16,7 @@ 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' diff --git a/bin/cli-verify b/bin/cli-verify new file mode 100755 index 000000000..be4420a7b --- /dev/null +++ b/bin/cli-verify @@ -0,0 +1,48 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +const program = require('./commander'); +const mongoose = require('../services/mongoose'); +const util = require('./util'); +const databaseVerifications = require('./verifications/database'); + +// Register the shutdown criteria. +util.onshutdown([ + () => mongoose.disconnect() +]); + +async function database(program) { + try { + for (const verification of databaseVerifications) { + await verification(program); + } + } 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('-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(); +} diff --git a/bin/verifications/database/comments.js b/bin/verifications/database/comments.js new file mode 100644 index 000000000..1f9a81951 --- /dev/null +++ b/bin/verifications/database/comments.js @@ -0,0 +1,141 @@ +const CommentModel = require('../../../models/comment'); +const ActionsService = require('../../../services/actions'); +const {arrayJoinBy} = require('../../../graph/loaders/util'); +const sc = require('snake-case'); + +const getBatch = async (limit, offset) => CommentModel + .find({}) + .select({'id': 1, 'action_counts': 1}) + .limit(limit) + .skip(offset) + .sort('created_at'); + +module.exports = async ({fix = false, batch = 1000}) => { + 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...`); + + // 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 action summaries. + let allActionSummaries = await ActionsService + .getActionSummaries(commentIDs) + .then(arrayJoinBy(commentIDs, 'item_id')); + + // Loop over the comments, with their action summaries. + for (let i = 0; i < comments.length; i++) { + let comment = comments[i]; + let actionSummaries = allActionSummaries[i]; + + // And check to see if the action summaries we just computed match what is + // currently set for the comments. + let commentOperations = []; + + // First we process all the group id's. + for (let actionSummary of actionSummaries) { + if (actionSummary.group_id === null) { + continue; + } + + const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase()); + const GROUP_ID = sc(actionSummary.group_id.toLowerCase()); + + if (GROUP_ID.length <= 0) { + continue; + } + + const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`; + + // Check that the action summaries match the cached counts. + if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== actionSummary.count) { + + // Batch updates for those changes. + commentOperations.push({ + [`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count, + }); + } + } + + // Group all the action summaries together from all the different group + // ids. + let groupedActionSummaries = actionSummaries.reduce((acc, actionSummary) => { + const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase()); + + if (!(ACTION_TYPE in acc)) { + acc[ACTION_TYPE] = 0; + } + + acc[ACTION_TYPE] += actionSummary.count; + + return acc; + }, {}); + + Object.keys(groupedActionSummaries).forEach((ACTION_COUNT_FIELD) => { + const count = groupedActionSummaries[ACTION_COUNT_FIELD]; + + // Check that the action summaries match the cached counts. + if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) { + + // Batch updates for those changes. + commentOperations.push({ + [`action_counts.${ACTION_COUNT_FIELD}`]: count, + }); + } + }); + + // If this comment has action summaries that should be updated, then + // perform an update! + if (commentOperations.length > 0) { + operations.push({ + updateOne: { + filter: { + id: comment.id + }, + update: { + $set: Object.assign({}, ...commentOperations), + }, + }, + }); + } + } + + console.log(`Processed batch of ${comments.length} comments.`); + + offset += batch; + } + + const OPERATIONS_LENGTH = operations.length; + + 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) { + console.log(`Fixing ${OPERATIONS_LENGTH} documents...`); + + while (operations.length) { + let batchOperations = operations.splice(0, batch); + let result = await CommentModel.collection.bulkWrite(batchOperations); + + console.log(`Fixed batch of ${result.modifiedCount} documents.`); + } + + console.log(`Fixed all ${OPERATIONS_LENGTH} documents.`); + } else { + console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors'); + } + } +}; diff --git a/bin/verifications/database/index.js b/bin/verifications/database/index.js new file mode 100644 index 000000000..3dafeb3bf --- /dev/null +++ b/bin/verifications/database/index.js @@ -0,0 +1,12 @@ +// 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 derrived. +module.exports = [ + require('./comments'), +]; diff --git a/services/comments.js b/services/comments.js index 4c57907e4..8db14966b 100644 --- a/services/comments.js +++ b/services/comments.js @@ -4,6 +4,7 @@ const ActionModel = require('../models/action'); const ActionsService = require('./actions'); const SettingsService = require('./settings'); +const sc = require('snake-case'); const errors = require('../errors'); const events = require('./events'); const { @@ -300,25 +301,37 @@ module.exports = class CommentsService { // Event Hooks //============================================================================== +const incrActionCounts = async (action, value) => { + const ACTION_TYPE = sc(action.action_type.toLowerCase()); + + const update = { + [`action_counts.${ACTION_TYPE}`]: value, + }; + + if (action.group_id !== null && action.group_id.length > 0) { + const GROUP_ID = sc(action.group_id.toLowerCase()); + + update[`action_counts.${ACTION_TYPE}_${GROUP_ID}`] = value; + } + + try { + await CommentModel.update({ + id: action.item_id, + }, { + $inc: update, + }); + } catch (err) { + console.error(`Can't mutate the action_counts.${ACTION_TYPE}:`, err); + } +}; + // When a new action is created, modify the comment. events.on(ACTIONS_NEW, async (action) => { if (!action || action.item_type !== 'COMMENTS') { return; } - const ACTION_TYPE = action.action_type.toLowerCase(); - - try { - await CommentModel.update({ - id: action.item_id, - }, { - $inc: { - [`action_counts.${ACTION_TYPE}`]: 1, - } - }); - } catch (err) { - console.error(`Can't increment the action_counts.${ACTION_TYPE}:`, err); - } + return incrActionCounts(action, 1); }); // When an action is deleted, modify the comment. @@ -327,17 +340,5 @@ events.on(ACTIONS_DELETE, async (action) => { return; } - const ACTION_TYPE = action.action_type.toLowerCase(); - - try { - await CommentModel.update({ - id: action.item_id, - }, { - $inc: { - [`action_counts.${ACTION_TYPE}`]: -1, - } - }); - } catch (err) { - console.error(`Can't decrement the action_counts.${ACTION_TYPE}:`, err); - } + return incrActionCounts(action, -1); }); From 686eb614f7a61356ce889f6a6b389aedb564ca4a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 17 Aug 2017 16:43:55 -0600 Subject: [PATCH 005/121] changed cursor to Cursor scalar type, changed SORT_ORDER --- .../coral-admin/src/containers/UserDetail.js | 2 +- client/coral-admin/src/reducers/moderation.js | 2 +- .../Moderation/components/ModerationMenu.js | 4 +-- .../Moderation/containers/Moderation.js | 2 +- .../src/routes/Moderation/graphql.js | 4 +-- .../src/containers/Stream.js | 6 ++-- client/coral-framework/utils/index.js | 6 ++-- .../containers/ProfileContainer.js | 4 +-- graph/loaders/comments.js | 8 ++--- graph/loaders/users.js | 4 +-- graph/resolvers/cursor.js | 35 +++++++++++++++++++ graph/resolvers/index.js | 6 ++-- graph/typeDefs.graphql | 23 ++++++------ .../client/containers/TabPane.js | 6 ++-- .../client/index.js | 2 +- 15 files changed, 77 insertions(+), 37 deletions(-) create mode 100644 graph/resolvers/cursor.js diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index 2ae6e4c9c..edde4b585 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -124,7 +124,7 @@ class UserDetailContainer extends React.Component { } const LOAD_MORE_QUERY = gql` - query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $author_id: ID!, $statuses: [COMMENT_STATUS!]) { + query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $author_id: ID!, $statuses: [COMMENT_STATUS!]) { comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) { ...CoralAdmin_Moderation_CommentConnection } diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index e0a608488..cf08bda60 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -6,7 +6,7 @@ const initialState = { storySearchVisible: false, storySearchString: '', shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show', - sortOrder: 'REVERSE_CHRONOLOGICAL', + sortOrder: 'DESC', }; export default function moderation (state = initialState, action) { diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js b/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js index 7b427ac54..a28e90c65 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js @@ -36,8 +36,8 @@ const ModerationMenu = ({ label="Sort" value={sort} onChange={(sort) => selectSort(sort)}> - - + + diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index d5ca5c840..ba8a80677 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -279,7 +279,7 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql` `; const LOAD_MORE_QUERY = gql` - query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) { + query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) { comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) { nodes { ...${getDefinitionName(Comment.fragments.comment)} diff --git a/client/coral-admin/src/routes/Moderation/graphql.js b/client/coral-admin/src/routes/Moderation/graphql.js index 0d4991f72..e3dac8e8f 100644 --- a/client/coral-admin/src/routes/Moderation/graphql.js +++ b/client/coral-admin/src/routes/Moderation/graphql.js @@ -36,7 +36,7 @@ function shouldCommentBeAdded(root, queue, comment, sort) { return true; } const cursor = new Date(root[queue].endCursor); - return sort === 'CHRONOLOGICAL' + return sort === 'ASC' ? new Date(comment.created_at) <= cursor : new Date(comment.created_at) >= cursor; } @@ -46,7 +46,7 @@ function addCommentToQueue(root, queue, comment, sort) { return root; } - const sortAlgo = sort === 'CHRONOLOGICAL' ? ascending : descending; + const sortAlgo = sort === 'ASC' ? ascending : descending; const changes = { [`${queue}Count`]: {$set: root[`${queue}Count`] + 1}, }; diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 451b115a9..de05f27b6 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -102,7 +102,7 @@ class StreamContainer extends React.Component { cursor: comment.replies.endCursor, parent_id, asset_id: this.props.root.asset.id, - sort: 'CHRONOLOGICAL', + sort: 'ASC', excludeIgnored: this.props.data.variables.excludeIgnored, }, updateQuery: (prev, {fetchMoreResult:{comments}}) => { @@ -119,7 +119,7 @@ class StreamContainer extends React.Component { cursor: this.props.root.asset.comments.endCursor, parent_id: null, asset_id: this.props.root.asset.id, - sort: 'REVERSE_CHRONOLOGICAL', + sort: 'DESC', excludeIgnored: this.props.data.variables.excludeIgnored, }, updateQuery: (prev, {fetchMoreResult:{comments}}) => { @@ -202,7 +202,7 @@ const COMMENTS_EDITED_SUBSCRIPTION = gql` `; const LOAD_MORE_QUERY = gql` - query CoralEmbedStream_LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { + query CoralEmbedStream_LoadMoreComments($limit: Int = 5, $cursor: Cursor, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) { nodes { id diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 63dfd7dc4..84249667d 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -156,12 +156,12 @@ const ascending = (a, b) => { const descending = (a, b) => ascending(a, b) * -1; -export function insertCommentsSorted(nodes, comments, sortOrder = 'CHRONOLOGICAL') { +export function insertCommentsSorted(nodes, comments, sortOrder = 'ASC') { const added = nodes.concat(comments); - if (sortOrder === 'CHRONOLOGICAL') { + if (sortOrder === 'ASC') { return added.sort(ascending); } - if (sortOrder === 'REVERSE_CHRONOLOGICAL') { + if (sortOrder === 'DESC') { return added.sort(descending); } throw new Error(`Unknown sort order ${sortOrder}`); diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index e33c0b6cd..920f50e73 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -38,7 +38,7 @@ class ProfileContainer extends Component { me: { comments: { nodes: { - $apply: (nodes) => insertCommentsSorted(nodes, comments.nodes, 'REVERSE_CHRONOLOGICAL'), + $apply: (nodes) => insertCommentsSorted(nodes, comments.nodes, 'DESC'), }, hasNextPage: {$set: comments.hasNextPage}, endCursor: {$set: comments.endCursor}, @@ -112,7 +112,7 @@ const CommentFragment = gql` `; const LOAD_MORE_QUERY = gql` - query TalkSettings_LoadMoreComments($limit: Int, $cursor: Date) { + query TalkSettings_LoadMoreComments($limit: Int, $cursor: Cursor) { comments(query: {limit: $limit, cursor: $cursor}) { ...TalkSettings_CommentConnectionFragment } diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index 7a17cfabd..0df73e970 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -312,7 +312,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a } if (cursor) { - if (sort === 'REVERSE_CHRONOLOGICAL') { + if (sort === 'DESC') { comments = comments.where({ created_at: { $lt: cursor @@ -328,7 +328,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a } let query = comments - .sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -1 : 1}); + .sort({created_at: sort === 'DESC' ? -1 : 1}); if (limit) { query = query.limit(limit + 1); } @@ -363,7 +363,7 @@ const genRecentReplies = (context, ids) => { } }}, - // sort these by their created at timestamp, CHRONOLOGICAL'y as these are + // sort these by their created at timestamp, ASC'y as these are // replies {$sort: { created_at: 1 @@ -413,7 +413,7 @@ const genRecentComments = (_, ids) => { } }}, - // sort these by their created at timestamp, CHRONOLOGICAL'y as these are + // sort these by their created at timestamp, ASC'y as these are // replies {$sort: { created_at: 1 diff --git a/graph/loaders/users.js b/graph/loaders/users.js index c4850eaf0..fe7a6285d 100644 --- a/graph/loaders/users.js +++ b/graph/loaders/users.js @@ -36,7 +36,7 @@ const getUsersByQuery = ({user}, {ids, limit, cursor, statuses = null, sort}) => } if (cursor) { - if (sort === 'REVERSE_CHRONOLOGICAL') { + if (sort === 'DESC') { users = users.where({ created_at: { $lt: cursor @@ -52,7 +52,7 @@ const getUsersByQuery = ({user}, {ids, limit, cursor, statuses = null, sort}) => } return users - .sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -1 : 1}) + .sort({created_at: sort === 'DESC' ? -1 : 1}) .limit(limit); }; diff --git a/graph/resolvers/cursor.js b/graph/resolvers/cursor.js new file mode 100644 index 000000000..2e7fec605 --- /dev/null +++ b/graph/resolvers/cursor.js @@ -0,0 +1,35 @@ +const GraphQLScalarType = require('graphql').GraphQLScalarType; +const Kind = require('graphql/language').Kind; + +module.exports = new GraphQLScalarType({ + name: 'Cursor', + description: 'Cursor represents a paginating cursor.', + serialize(value) { + if (value instanceof Date) { + return value.toISOString(); + } + + return value; + }, + parseValue(value) { + if (typeof value === 'string') { + return new Date(value); + } + + return value; + }, + parseLiteral(ast) { + switch (ast.kind) { + case Kind.STRING: + + // This handles an empty string. + if (ast.value && ast.value.length === 0) { + return null; + } + + return new Date(ast.value); + default: + return ast.value; + } + } +}); diff --git a/graph/resolvers/index.js b/graph/resolvers/index.js index 850a2f15a..69ae9ca3e 100644 --- a/graph/resolvers/index.js +++ b/graph/resolvers/index.js @@ -5,8 +5,9 @@ const ActionSummary = require('./action_summary'); const Action = require('./action'); const AssetActionSummary = require('./asset_action_summary'); const Asset = require('./asset'); -const Comment = require('./comment'); const CommentStatusHistory = require('./comment_status_history'); +const Comment = require('./comment'); +const Cursor = require('./cursor'); const Date = require('./date'); const FlagActionSummary = require('./flag_action_summary'); const FlagAction = require('./flag_action'); @@ -31,8 +32,9 @@ let resolvers = { Action, AssetActionSummary, Asset, - Comment, CommentStatusHistory, + Comment, + Cursor, Date, FlagActionSummary, FlagAction, diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index d2c1ffe1f..505026389 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -5,6 +5,9 @@ # Date represented as an ISO8601 string. scalar Date +# Cursor represents a paginating cursor. +scalar Cursor + ################################################################################ ## Reliability ################################################################################ @@ -126,10 +129,10 @@ input UsersQuery { limit: Int = 10 # Skip results from the last created_at timestamp. - cursor: Date + cursor: Cursor # Sort the results by created_at. - sort: SORT_ORDER = REVERSE_CHRONOLOGICAL + sort: SORT_ORDER = DESC } # AssetsQuery allows teh ability to query assets by specific fields @@ -241,10 +244,10 @@ input CommentsQuery { limit: Int = 10 # Skip results from the last created_at timestamp. - cursor: Date + cursor: Cursor # Sort the results by created_at. - sort: SORT_ORDER = REVERSE_CHRONOLOGICAL + sort: SORT_ORDER = DESC # Filter by a specific tag name. tags: [String!] @@ -314,7 +317,7 @@ type Comment { recentReplies: [Comment!] # the replies that were made to the comment. - replies(sort: SORT_ORDER = CHRONOLOGICAL, limit: Int = 3, excludeIgnored: Boolean): CommentConnection! + replies(sort: SORT_ORDER = ASC, limit: Int = 3, excludeIgnored: Boolean): CommentConnection! # The count of replies on a comment. replyCount(excludeIgnored: Boolean): Int @@ -348,10 +351,10 @@ type CommentConnection { hasNextPage: Boolean! # Cursor of first comment in subset. - startCursor: Date + startCursor: Cursor # Cursor of last comment in subset. - endCursor: Date + endCursor: Cursor # Subset of comments. nodes: [Comment!]! @@ -573,7 +576,7 @@ type Asset { # If `deep` is true, it will return comments of all depths, # otherwise only top-level comments are returned. comments( - sort: SORT_ORDER = REVERSE_CHRONOLOGICAL, + sort: SORT_ORDER = DESC, limit: Int = 10, excludeIgnored: Boolean, tags: [String!] @@ -652,10 +655,10 @@ type ValidationUserError implements UserError { enum SORT_ORDER { # newest to oldest order. - REVERSE_CHRONOLOGICAL + DESC # oldest to newer order. - CHRONOLOGICAL + ASC } # All queries that can be executed. diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js index 2195b0107..4c36c5c62 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js @@ -18,7 +18,7 @@ class TabPaneContainer extends React.Component { limit: 5, cursor: this.props.root.asset.featuredComments.endCursor, asset_id: this.props.root.asset.id, - sort: 'REVERSE_CHRONOLOGICAL', + sort: 'DESC', excludeIgnored: this.props.data.variables.excludeIgnored, }, updateQuery: (previous, {fetchMoreResult:{comments}}) => { @@ -26,7 +26,7 @@ class TabPaneContainer extends React.Component { asset: { featuredComments: { nodes: { - $apply: (nodes) => insertCommentsSorted(nodes, comments.nodes, 'REVERSE_CHRONOLOGICAL'), + $apply: (nodes) => insertCommentsSorted(nodes, comments.nodes, 'DESC'), }, hasNextPage: {$set: comments.hasNextPage}, endCursor: {$set: comments.endCursor}, @@ -47,7 +47,7 @@ class TabPaneContainer extends React.Component { } const LOAD_MORE_QUERY = gql` - query TalkFeaturedComments_LoadMoreComments($limit: Int = 5, $cursor: Date, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { + query TalkFeaturedComments_LoadMoreComments($limit: Int = 5, $cursor: String, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { comments(query: {limit: $limit, cursor: $cursor, tags: ["FEATURED"], asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) { nodes { ...${getDefinitionName(Comment.fragments.comment)} diff --git a/plugins/talk-plugin-featured-comments/client/index.js b/plugins/talk-plugin-featured-comments/client/index.js index ad10f87ca..c796521e9 100644 --- a/plugins/talk-plugin-featured-comments/client/index.js +++ b/plugins/talk-plugin-featured-comments/client/index.js @@ -60,7 +60,7 @@ export default { asset: { featuredComments: { nodes: { - $apply: (nodes) => insertCommentsSorted(nodes, comment, 'REVERSE_CHRONOLOGICAL') + $apply: (nodes) => insertCommentsSorted(nodes, comment, 'DESC') } }, featuredCommentsCount: { From a7a80a452b02aeb8b2fec9bb3827e3445c3d0cd8 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sat, 19 Aug 2017 09:59:46 -0600 Subject: [PATCH 006/121] removed old deps + outdated e2e --- package.json | 129 +- scripts/poste2e.sh | 10 - scripts/pree2e.sh | 25 - selenium.config.js | 9 - test/client/.babelrc | 3 - test/client/.eslintrc.json | 23 - test/client/coral-admin/actions/assets.js | 89 -- test/client/coral-admin/reducers/assets.js | 64 - .../reducers/assetReducer.spec.js | 19 - test/e2e/globals.js | 18 - test/e2e/mocks.js | 26 - test/e2e/pages/adminPage.js | 38 - test/e2e/pages/embedStreamPage.js | 165 --- test/e2e/tests/00_AppTest.js | 10 - test/e2e/tests/01_EmbedStreamTest.js | 52 - test/e2e/tests/Admin/LoginTest.js | 23 - test/e2e/tests/Commenter/FlagCommentTest.js | 34 - test/e2e/tests/Commenter/FlagUsernameTest.js | 34 - test/e2e/tests/Commenter/LikeCommentTest.js | 27 - test/e2e/tests/Commenter/LoginTest.js | 20 - test/e2e/tests/Commenter/PermalinkTest.js | 34 - test/e2e/tests/Commenter/PostComment.js | 38 - test/e2e/tests/EmbedStreamTests.js | 182 --- test/e2e/tests/Moderator/LoginTest.js | 23 - test/e2e/tests/Visitor/FlagCommentTest.js | 20 - test/e2e/tests/Visitor/LikeCommentTest.js | 20 - test/e2e/tests/Visitor/SignUpTest.js | 23 - test/helpers/browser.js | 51 - yarn.lock | 1099 ++--------------- 29 files changed, 151 insertions(+), 2157 deletions(-) delete mode 100755 scripts/poste2e.sh delete mode 100755 scripts/pree2e.sh delete mode 100644 selenium.config.js delete mode 100644 test/client/.babelrc delete mode 100644 test/client/.eslintrc.json delete mode 100644 test/client/coral-admin/actions/assets.js delete mode 100644 test/client/coral-admin/reducers/assets.js delete mode 100644 test/client/coral-framework/reducers/assetReducer.spec.js delete mode 100644 test/e2e/globals.js delete mode 100644 test/e2e/mocks.js delete mode 100644 test/e2e/pages/adminPage.js delete mode 100644 test/e2e/pages/embedStreamPage.js delete mode 100644 test/e2e/tests/00_AppTest.js delete mode 100644 test/e2e/tests/01_EmbedStreamTest.js delete mode 100644 test/e2e/tests/Admin/LoginTest.js delete mode 100644 test/e2e/tests/Commenter/FlagCommentTest.js delete mode 100644 test/e2e/tests/Commenter/FlagUsernameTest.js delete mode 100644 test/e2e/tests/Commenter/LikeCommentTest.js delete mode 100644 test/e2e/tests/Commenter/LoginTest.js delete mode 100644 test/e2e/tests/Commenter/PermalinkTest.js delete mode 100644 test/e2e/tests/Commenter/PostComment.js delete mode 100644 test/e2e/tests/EmbedStreamTests.js delete mode 100644 test/e2e/tests/Moderator/LoginTest.js delete mode 100644 test/e2e/tests/Visitor/FlagCommentTest.js delete mode 100644 test/e2e/tests/Visitor/LikeCommentTest.js delete mode 100644 test/e2e/tests/Visitor/SignUpTest.js delete mode 100644 test/helpers/browser.js diff --git a/package.json b/package.json index 71477361e..9801c6ead 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "postinstall": "./bin/cli plugins reconcile --skip-remote", "start": "./bin/cli serve -j -w", - "dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js && ./bin/cli -c .env serve -j -w\"", + "dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"yarn generate-introspection && ./bin/cli -c .env serve -j -w\"", "prebuild": "yarn generate-introspection", "build": "WEBPACK=TRUE NODE_ENV=production webpack -p --config webpack.config.js --bail", "prebuild-watch": "yarn generate-introspection", @@ -15,10 +15,6 @@ "lint-fix": "eslint bin/* . --fix", "test": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}", "test-cover": "TEST_MODE=unit NODE_ENV=test istanbul cover _mocha --report text --check-coverage -- -R spec", - "pree2e": "NODE_ENV=test TALK_PORT=3011 scripts/pree2e.sh", - "e2e": "NODE_ENV=test nightwatch", - "poste2e": "NODE_ENV=test scripts/poste2e.sh", - "embed-start": "NODE_ENV=development yarn build && ./bin/cli serve --jobs", "heroku-postbuild": "./bin/cli plugins reconcile && yarn build", "generate-introspection": "WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js" }, @@ -59,8 +55,23 @@ "homepage": "https://github.com/coralproject/talk#readme", "dependencies": { "accepts": "^1.3.3", + "apollo-client": "^1.9.1", "app-module-path": "^2.2.0", - "async": "^2.5.0", + "autoprefixer": "^6.5.2", + "babel-cli": "^6.24.0", + "babel-core": "^6.24.0", + "babel-eslint": "^7.2.1", + "babel-loader": "^6.4.1", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-transform-async-to-generator": "^6.16.0", + "babel-plugin-transform-class-properties": "^6.23.0", + "babel-plugin-transform-decorators-legacy": "^1.3.4", + "babel-plugin-transform-object-assign": "^6.8.0", + "babel-plugin-transform-object-rest-spread": "^6.23.0", + "babel-plugin-transform-react-jsx": "^6.23.0", + "babel-polyfill": "^6.23.0", + "babel-preset-es2015": "^6.24.0", + "babel-preset-react": "^6.23.0", "bcryptjs": "^2.4.3", "body-parser": "^1.17.1", "bowser": "^1.7.0", @@ -68,39 +79,53 @@ "clipboard": "^1.7.1", "colors": "^1.1.2", "commander": "^2.9.0", + "common-tags": "^1.4.0", "compression": "^1.6.2", - "connect-redis": "^3.1.0", + "compression-webpack-plugin": "^0.4.0", "cookie-parser": "^1.4.3", + "copy-webpack-plugin": "^4.0.0", "cross-spawn": "^5.1.0", - "csurf": "^1.9.0", + "css-loader": "^0.27.3", "dataloader": "^1.3.0", "debug": "^2.6.3", + "dialog-polyfill": "^0.4.4", "dotenv": "^4.0.0", "ejs": "^2.5.6", "env-rewrite": "^1.0.2", "eventemitter2": "^4.1.2", + "exports-loader": "^0.6.4", "express": "^4.15.2", - "express-session": "^1.15.1", "file-loader": "^0.11.2", "form-data": "^2.1.2", "fs-extra": "^3.0.1", "gql-merge": "^0.0.4", "graphql": "^0.9.1", + "graphql-docs": "^0.2.0", "graphql-errors": "^2.1.0", "graphql-redis-subscriptions": "^1.1.5", "graphql-server-express": "^0.6.0", "graphql-subscriptions": "^0.4.3", + "graphql-tag": "^1.2.3", "graphql-tools": "^0.10.1", + "hammerjs": "^2.0.8", "helmet": "^3.5.0", + "history": "^3.0.0", + "ignore-styles": "^5.0.1", "immutability-helper": "^2.2.0", + "imports-loader": "^0.7.1", "inquirer": "^3.2.1", + "istanbul": "^1.1.0-alpha.1", "joi": "^10.4.1", + "json-loader": "^0.5.4", "jsonwebtoken": "^7.3.0", "jwt-decode": "^2.2.0", + "keymaster": "^1.6.2", "kue": "^0.11.5", + "license-webpack-plugin": "^0.4.2", "linkify-it": "^2.0.3", "lodash": "^4.16.6", "marked": "^0.3.6", + "material-design-lite": "^1.2.1", "metascraper": "^1.0.6", "minimist": "^1.2.0", "mongoose": "^4.9.8", @@ -114,55 +139,52 @@ "passport": "^0.3.2", "passport-jwt": "^2.2.1", "passport-local": "^1.0.0", + "postcss-loader": "^1.3.3", + "postcss-modules": "^0.5.2", + "postcss-smart-import": "^0.5.1", + "precss": "^1.4.0", "prop-types": "^15.5.10", + "pym.js": "^1.1.1", "query-strings": "^0.0.1", + "react": "^15.4.2", "react-apollo": "^1.4.12", + "react-dom": "^15.4.2", + "react-highlight-words": "^0.6.0", "react-input-autosize": "^1.1.4", + "react-linkify": "^0.1.3", + "react-mdl": "^1.7.2", + "react-mdl-selectfield": "^0.2.0", "react-recaptcha": "^2.2.6", + "react-redux": "^4.4.5", + "react-router": "^3.0.0", + "react-tagsinput": "^3.17.0", "react-toastify": "^1.5.0", "react-transition-group": "^1.1.3", "recompose": "^0.23.1", "redis": "^2.7.1", + "redux": "^3.6.0", + "redux-thunk": "^2.1.0", "resolve": "^1.3.2", "semver": "^5.3.0", "simplemde": "^1.11.2", "smoothscroll-polyfill": "^0.3.5", "snake-case": "^2.1.0", + "style-loader": "^0.16.0", "subscriptions-transport-ws": "^0.7.2", + "timeago.js": "^2.0.3", "timekeeper": "^1.0.0", + "url-loader": "^0.5.9", "url-search-params": "^0.9.0", "uuid": "^3.0.1", + "webpack": "^2.3.1", "yaml-loader": "^0.4.0", "yamljs": "^0.2.10" }, "devDependencies": { - "apollo-client": "^1.9.1", - "autoprefixer": "^6.5.2", - "babel-cli": "^6.24.0", - "babel-core": "^6.24.0", - "babel-eslint": "^7.2.1", - "babel-jest": "^19.0.0", - "babel-loader": "^6.4.1", - "babel-plugin-add-module-exports": "^0.2.1", - "babel-plugin-transform-async-to-generator": "^6.16.0", - "babel-plugin-transform-class-properties": "^6.23.0", - "babel-plugin-transform-decorators-legacy": "^1.3.4", - "babel-plugin-transform-object-assign": "^6.8.0", - "babel-plugin-transform-object-rest-spread": "^6.23.0", - "babel-plugin-transform-react-jsx": "^6.23.0", - "babel-polyfill": "^6.23.0", - "babel-preset-es2015": "^6.24.0", - "babel-preset-react": "^6.23.0", - "babel-preset-stage-0": "^6.16.0", "chai": "^3.5.0", "chai-as-promised": "^6.0.0", "chai-http": "^3.0.0", - "common-tags": "^1.4.0", - "compression-webpack-plugin": "^0.4.0", - "copy-webpack-plugin": "^4.0.0", - "css-loader": "^0.27.3", - "dialog-polyfill": "^0.4.4", - "enzyme": "^2.6.0", + "enzyme": "^2.9.1", "eslint": "^3.12.1", "eslint-config-postcss": "^2.0.2", "eslint-config-standard": "^6.2.1", @@ -173,50 +195,11 @@ "eslint-plugin-promise": "^3.3.1", "eslint-plugin-react": "^6.6.0", "eslint-plugin-standard": "^2.0.1", - "exports-loader": "^0.6.4", - "fetch-mock": "^5.5.0", - "graphql-docs": "^0.2.0", - "graphql-tag": "^1.2.3", - "hammerjs": "^2.0.8", - "history": "^3.0.0", - "ignore-styles": "^5.0.1", - "imports-loader": "^0.7.1", - "istanbul": "^1.1.0-alpha.1", - "jsdom": "^9.8.3", - "json-loader": "^0.5.4", - "keymaster": "^1.6.2", - "license-webpack-plugin": "^0.4.2", - "material-design-lite": "^1.2.1", "mocha": "^3.1.2", "mocha-junit-reporter": "^1.12.1", - "nightwatch": "^0.9.11", "nodemon": "^1.11.0", - "postcss-loader": "^1.3.3", - "postcss-modules": "^0.5.2", - "postcss-smart-import": "^0.5.1", "pre-git": "^3.10.0", - "precss": "^1.4.0", - "pym.js": "^1.1.1", - "react": "^15.4.2", - "react-addons-test-utils": "^15.4.2", - "react-dom": "^15.4.2", - "react-highlight-words": "^0.6.0", - "react-linkify": "^0.1.3", - "react-mdl": "^1.7.2", - "react-mdl-selectfield": "^0.2.0", - "react-redux": "^4.4.5", - "react-router": "^3.0.0", - "react-tagsinput": "^3.17.0", - "redux": "^3.6.0", - "redux-mock-store": "^1.2.1", - "redux-thunk": "^2.1.0", - "regenerator": "^0.8.46", - "selenium-standalone": "^5.11.2", - "style-loader": "^0.16.0", - "supertest": "^2.0.1", - "timeago.js": "^2.0.3", - "url-loader": "^0.5.9", - "webpack": "^2.3.1" + "supertest": "^2.0.1" }, "engines": { "node": "^8" diff --git a/scripts/poste2e.sh b/scripts/poste2e.sh deleted file mode 100755 index 1a8279da1..000000000 --- a/scripts/poste2e.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# If there is a PID file from the e2e tests... -if [ -e /tmp/talk-e2e.pid ] -then - - # Then kill the running talk server. - kill $(cat /tmp/talk-e2e.pid) - -fi diff --git a/scripts/pree2e.sh b/scripts/pree2e.sh deleted file mode 100755 index 5b6f21ab3..000000000 --- a/scripts/pree2e.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -# fail the e2e if any of these fail -set -e - -# install selenium -selenium-standalone install --config=./selenium.config.js - -# Clear db -mongo test --eval "db.dropDatabase()" - -# Init application -./bin/cli setup --defaults - -# Creating Admin Test User -./bin/cli users create --flag_mode --email "admin@test.com" --password "testtest" --name "AdminTestUser" --role "ADMIN" - -# Creating Moderator Test User -./bin/cli users create --flag_mode --email "moderator@test.com" --password "testtest" --name "ModeratorTestUser" --role "MODERATOR" - -# Creating Commenter Test User -./bin/cli users create --flag_mode --email "commenter@test.com" --password "testtest" --name "CommenterTestUser" - -# Start the server and write the PID to a file to be killed later. -./bin/cli --pid /tmp/talk-e2e.pid serve --jobs & diff --git a/selenium.config.js b/selenium.config.js deleted file mode 100644 index b43c31ef0..000000000 --- a/selenium.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - drivers: { - chrome: { - version: '2.25', - arch: process.arch, - baseURL: 'https://chromedriver.storage.googleapis.com' - }, - }, -}; diff --git a/test/client/.babelrc b/test/client/.babelrc deleted file mode 100644 index 633f93f42..000000000 --- a/test/client/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../.babelrc" -} diff --git a/test/client/.eslintrc.json b/test/client/.eslintrc.json deleted file mode 100644 index a935eaec6..000000000 --- a/test/client/.eslintrc.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "env": { - "browser": true, - "es6": true, - "mocha": true - }, - "extends": "../.eslintrc.json", - "parserOptions": { - "ecmaFeatures": { - "experimentalObjectRestSpread": true, - "jsx": true - }, - "sourceType": "module" - }, - "parser": "babel-eslint", - "plugins": [ - "react" - ], - "rules": { - "react/jsx-uses-react": "error", - "react/jsx-uses-vars": "error" - } -} diff --git a/test/client/coral-admin/actions/assets.js b/test/client/coral-admin/actions/assets.js deleted file mode 100644 index 24a28daef..000000000 --- a/test/client/coral-admin/actions/assets.js +++ /dev/null @@ -1,89 +0,0 @@ -import 'react'; -import 'redux'; -import {expect} from 'chai'; -import fetchMock from 'fetch-mock'; -import * as actions from '../../../../client/coral-admin/src/actions/assets'; -import {Map} from 'immutable'; - -import configureStore from 'redux-mock-store'; - -const mockStore = configureStore(); - -describe('Asset actions', () => { - let store; - - const assets = [ - { - url: 'http://test.com', - id: '123', - status: 'closed' - }, - { - url: 'http://test.org', - id: '456', - status: 'open' - } - ]; - - beforeEach(() => { - store = mockStore(new Map({})); - fetchMock.restore(); - }); - - describe('FETCH_ASSETS_REQUEST', () => { - - it('should fetch a list of assets', () => { - - fetchMock.get('*', JSON.stringify({ - result: assets, - count: 2 - })); - - return actions.fetchAssets(2, 20)(store.dispatch) - .then(() => { - expect(store.getActions()[0]).to.have.property('type', 'FETCH_ASSETS_REQUEST'); - expect(store.getActions()[1]).to.have.property('type', 'FETCH_ASSETS_SUCCESS'); - expect(store.getActions()[1]).to.have.property('count', 2); - expect(store.getActions()[1]).to.have.property('assets'). - and.to.deep.equal(assets); - }); - }); - - it('should return an error appropriatly', () => { - - fetchMock.get('*', 404); - - return actions.fetchAssets(2, 20)(store.dispatch) - .then(() => { - expect(store.getActions()[0]).to.have.property('type', 'FETCH_ASSETS_REQUEST'); - expect(store.getActions()[1]).to.have.property('type', 'FETCH_ASSETS_FAILURE'); - }); - }); - }); - - describe('UPDATE_ASSET_STATE_REQUEST', () => { - - it('should update an asset', () => { - - fetchMock.put('*', JSON.stringify(assets[0])); - - return actions.updateAssetState('123', 'status', 'open')(store.dispatch) - .then(() => { - expect(store.getActions()[0]).to.have.property('type', 'UPDATE_ASSET_STATE_REQUEST'); - expect(store.getActions()[1]).to.have.property('type', 'UPDATE_ASSET_STATE_SUCCESS'); - }); - - }); - - it('should return an error appropriately', () => { - - fetchMock.put('*', 404); - - return actions.updateAssetState('123', 'status', 'open')(store.dispatch) - .then(() => { - expect(store.getActions()[0]).to.have.property('type', 'UPDATE_ASSET_STATE_REQUEST'); - expect(store.getActions()[1]).to.have.property('type', 'UPDATE_ASSET_STATE_FAILURE'); - }); - }); - }); -}); diff --git a/test/client/coral-admin/reducers/assets.js b/test/client/coral-admin/reducers/assets.js deleted file mode 100644 index 753061612..000000000 --- a/test/client/coral-admin/reducers/assets.js +++ /dev/null @@ -1,64 +0,0 @@ -import {Map, fromJS} from 'immutable'; -import {expect} from 'chai'; -import assetsReducer from '../../../../client/coral-admin/src/reducers/assets'; - -describe ('assetsReducer', () => { - describe('FETCH_ASSETS_SUCCESS', () => { - it('should replace the existing assets', () => { - const action = { - type: 'FETCH_ASSETS_SUCCESS', - count: 200, - assets: [ - { - id: '123', - url: 'http://test.com', - closedAt: 'tomorrow' - }, - { - id: '456', - url: 'http://test2.com', - closedAt: 'thursday' - }, - ] - }; - const store = new Map({}); - const result = assetsReducer(store, action); - expect(result.getIn(['byId', '123']).toJS()).to.deep.equal({ - url: 'http://test.com', - closedAt: 'tomorrow', - id: '123' - }); - expect(result.getIn(['ids']).toJS()).to.deep.equal([ - '123', - '456' - ]); - expect(result.getIn(['count'])).to.equal(200); - }); - }); - - describe('UPDATE_ASSET_STATE_REQUEST', () => { - it('should update the state of a particular asset', () => { - const action = { - type: 'UPDATE_ASSET_STATE_REQUEST', - id: '123', - closedAt: null - }; - const store = new fromJS({ - byId: { - '123': { - id: '123', - url: 'http://test.com', - closedAt: Date.now() - }, - '456': { - id: '456', - url: 'http://test2.com', - closedAt: 'thursday' - } - } - }); - const result = assetsReducer(store, action); - expect(result.getIn(['byId', '123', 'closedAt'])).to.equal.null; - }); - }); -}); diff --git a/test/client/coral-framework/reducers/assetReducer.spec.js b/test/client/coral-framework/reducers/assetReducer.spec.js deleted file mode 100644 index c54f51737..000000000 --- a/test/client/coral-framework/reducers/assetReducer.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -import {Map} from 'immutable'; -import {expect} from 'chai'; -import assetReducer from '../../../../client/coral-framework/reducers/asset'; -import * as actions from '../../../../client/coral-framework/constants/asset'; - -describe ('coral-embed-stream assetReducer', () => { - describe('UPDATE_COUNT_CACHE', () => { - it('should update the count cache', () => { - const action = { - type: actions.UPDATE_COUNT_CACHE, - id: '123', - count: 456 - }; - const store = new Map({}); - const result = assetReducer(store, action); - expect(result.getIn(['countCache', '123'])).to.equal(456); - }); - }); -}); diff --git a/test/e2e/globals.js b/test/e2e/globals.js deleted file mode 100644 index da80a5dd9..000000000 --- a/test/e2e/globals.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - waitForConditionTimeout: 8000, - baseUrl: 'http://localhost:3011', - users: { - admin: { - email: 'admin@test.com', - pass: 'testtest' - }, - moderator: { - email: 'moderator@test.com', - pass: 'testtest' - }, - commenter: { - email: 'commenter@test.com', - pass: 'testtest' - } - }, -}; diff --git a/test/e2e/mocks.js b/test/e2e/mocks.js deleted file mode 100644 index f1e46f0ea..000000000 --- a/test/e2e/mocks.js +++ /dev/null @@ -1,26 +0,0 @@ -const Comments = require('../../services/comments'); -const Users = require('../../services/users'); -const Actions = require('../../services/actions'); -const Assets = require('../../services/assets'); -const Settings = require('../../services/settings'); -const globals = require('./globals'); - -/* Create an array of comments */ -module.exports.comments = (comments) => Assets.findOrCreateByUrl(globals.baseUrl) - .then((asset) => { - comments = comments.map((comment) => { - comment.asset_id = asset.id; - return comment; - }); - return Comments.publicCreate(comments); - }); - -/* Create an array of users */ -module.exports.users = (users) => Users.createLocalUsers(users); - -/* Create an array of actions */ -module.exports.actions = (actions) => Actions.create(actions); - -/* Update a setting */ -module.exports.settings = (setting) => Settings.init().then(() => - Settings.update(setting)); diff --git a/test/e2e/pages/adminPage.js b/test/e2e/pages/adminPage.js deleted file mode 100644 index e1568ef51..000000000 --- a/test/e2e/pages/adminPage.js +++ /dev/null @@ -1,38 +0,0 @@ -const embedStreamCommands = { - url: function () { - return `${this.api.launchUrl}/admin`; - }, - ready() { - return this - .waitForElementVisible('body', 2000); - }, - approveComment() { - return this - .waitForElementVisible('@moderateNav') - .click('@moderateNav') - .waitForElementVisible('@moderationList') - .waitForElementVisible('@approveButton') - .click('@approveButton'); - } -}; - -module.exports = { - commands: [embedStreamCommands], - elements: { - moderateNav: { - selector: '#moderateNav' - }, - moderationList: { - selector: '#moderationList' - }, - banButton: { - selector: '#moderationList .actions:first-child .ban' - }, - rejectButton: { - selector: '#moderationList .actions:first-child .reject' - }, - approveButton: { - selector: '#moderationList .actions:first-child .approve' - } - } -}; diff --git a/test/e2e/pages/embedStreamPage.js b/test/e2e/pages/embedStreamPage.js deleted file mode 100644 index 75e9e0ecd..000000000 --- a/test/e2e/pages/embedStreamPage.js +++ /dev/null @@ -1,165 +0,0 @@ -const embedStreamCommands = { - url: function () { - return this - .api.launchUrl; - }, - ready() { - return this - .waitForElementVisible('body', 4000) - .waitForElementVisible('#coralStreamEmbed > iframe') - .api.frame('coralStreamEmbed_iframe'); - }, - signUp(user) { - return this - .waitForElementVisible('@signInButton', 2000) - .click('@signInButton') - .waitForElementVisible('@signInDialog') - .waitForElementVisible('@registerButton') - .click('@registerButton') - .setValue('@signInDialogEmail', user.email) - .setValue('@signInDialogPassword', user.pass) - .setValue('@signUpDialogConfirmPassword', user.pass) - .setValue('@signUpDialogUsername', user.username) - .waitForElementVisible('@signUpButton') - .click('@signUpButton') - .waitForElementVisible('@signInViewTrigger') - .click('@signInViewTrigger') - .waitForElementVisible('@logInButton') - .click('@logInButton') - .waitForElementVisible('@logoutButton', 5000); - }, - login(user) { - return this - .waitForElementVisible('@signInButton', 2000) - .click('@signInButton') - .waitForElementVisible('@signInDialog') - .waitForElementVisible('@signInDialogEmail') - .waitForElementVisible('@signInDialogPassword') - .setValue('@signInDialogEmail', user.email) - .setValue('@signInDialogPassword', user.pass) - .waitForElementVisible('@logInButton') - .click('@logInButton') - .waitForElementVisible('@logoutButton', 5000); - }, - logout() { - return this - .waitForElementVisible('@logoutButton', 5000) - .click('@logoutButton') - .waitForElementVisible('@signInButton', 5000); - }, - postComment(comment = 'Test Comment') { - return this - .waitForElementVisible('@commentBox', 2000) - .setValue('@commentBox', comment) - .click('@postButton'); - }, - likeComment() { - return this - .waitForElementVisible('@likeButton') - .click('@likeButton'); - }, - flagComment() { - return this - .waitForElementVisible('@flagButton') - .click('@flagButton'); - }, - flagUsername() { - return this - .waitForElementVisible('@flagButton') - .click('@flagButton'); - }, - getPermalink(fn) { - return this - .waitForElementVisible('@permalinkButton') - .click('@permalinkButton') - .waitForElementVisible('@permalinkPopUp') - .getValue('@permalinkInput', (result) => fn(result.value)); - } -}; - -module.exports = { - commands: [embedStreamCommands], - elements: { - signInButton: { - selector: '#coralSignInButton' - }, - signInDialog:{ - selector: '#signInDialog' - }, - signInDialogEmail: { - selector: '#signInDialog #email' - }, - signInDialogPassword: { - selector: '#signInDialog #password' - }, - signUpDialogConfirmPassword: { - selector: '#signInDialog #confirmPassword' - }, - signUpDialogUsername: { - selector: '#signInDialog #username' - }, - logInButton: { - selector: '#coralLogInButton' - }, - signUpButton: { - selector: '#coralSignUpButton' - }, - signInViewTrigger: { - selector: '#coralSignInViewTrigger' - }, - logoutButton: { - selector: '#coralStream #logout' - }, - commentBox: { - selector: '.talk-plugin-commentbox-textarea' - }, - postButton: { - selector: '#commentBox .talk-plugin-commentbox-button' - }, - likeButton: { - selector: '.embed__stream .comment .talk-plugin-likes-container .talk-plugin-likes-button' - }, - likeText: { - selector: '.embed__stream .comment .talk-plugin-likes-container .talk-plugin-likes-button .talk-plugin-likes-button-text' - }, - likesCount: { - selector: '.embed__stream .comment .talk-plugin-likes-container .talk-plugin-likes-button .talk-plugin-likes-like-count' - }, - flagButton: { - selector: '.embed__stream .comment .talk-plugin-flags-container .talk-plugin-flags-button' - }, - flagPopUp: { - selector: '.embed__stream .comment .talk-plugin-flags-popup' - }, - flagCommentOption: { - selector: '.embed__stream .comment .talk-plugin-flags-popup .talk-plugin-flags-popup-radio-label[for="COMMENTS"]' - }, - flagUsernameOption: { - selector: '.embed__stream .comment .talk-plugin-flags-popup .talk-plugin-flags-popup-radio-label[for="USERS"]' - }, - flagOtherOption: { - selector: '.embed__stream .comment .talk-plugin-flags-popup .talk-plugin-flags-popup-radio-label[for="other"]' - }, - flagHeaderMessage: { - selector: '.embed__stream .comment .talk-plugin-flags-popup .talk-plugin-flags-popup-header' - }, - flagButtonText: { - selector: '.embed__stream .comment .talk-plugin-flags-button-text' - }, - flagDoneButton: { - selector: '.embed__stream .comment .talk-plugin-flags-popup .talk-plugin-flags-popup-button' - }, - permalinkButton: { - selector: '.embed__stream .comment .talk-plugin-permalinks-button' - }, - permalinkPopUp: { - selector: '.embed__stream .comment .talk-plugin-permalinks-popover.active' - }, - permalinkInput: { - selector: '.embed__stream .comment .talk-plugin-permalinks-popover.active input' - }, - registerButton: { - selector: '#signInDialog #coralRegister' - } - } -}; diff --git a/test/e2e/tests/00_AppTest.js b/test/e2e/tests/00_AppTest.js deleted file mode 100644 index 4294c4483..000000000 --- a/test/e2e/tests/00_AppTest.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - '@tags': ['app'], - 'Base url and Hostname': (browser) => { - const {baseUrl} = browser.globals; - browser - .url(baseUrl) - .assert.title('Coral Talk') - .waitForElementPresent('body', 1000); - } -}; diff --git a/test/e2e/tests/01_EmbedStreamTest.js b/test/e2e/tests/01_EmbedStreamTest.js deleted file mode 100644 index 50d1feef0..000000000 --- a/test/e2e/tests/01_EmbedStreamTest.js +++ /dev/null @@ -1,52 +0,0 @@ -const mocks = require('../mocks'); - -module.exports = { - '@tags': ['embedStream'], - before: (client) => { - client.perform((client, done) => { - mocks.settings({moderation: 'PRE'}) - .then(() => { - const embedStreamPage = client.page.embedStreamPage(); - embedStreamPage - .navigate() - .ready(); - done(); - }); - }); - }, - 'Login as commenter': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - const {users} = client.globals; - embedStreamPage - .login(users.commenter); - }, - 'Add test comment': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - embedStreamPage - .postComment('Test Comment'); - }, - 'Logout': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - embedStreamPage - .logout(); - }, - 'Login as admin': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - const {users} = client.globals; - embedStreamPage - .login(users.admin); - }, - 'Approve test comment': (client) => { - const adminPage = client.page.adminPage(); - - adminPage - .navigate() - .ready(); - - adminPage - .approveComment(); - }, - after: (client) => { - client.end(); - } -}; diff --git a/test/e2e/tests/Admin/LoginTest.js b/test/e2e/tests/Admin/LoginTest.js deleted file mode 100644 index dcc076631..000000000 --- a/test/e2e/tests/Admin/LoginTest.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - '@tags': ['login', 'ADMIN'], - before(client) { - const embedStreamPage = client.page.embedStreamPage(); - const {launchUrl} = client; - - client - .url(launchUrl); - - embedStreamPage - .ready(); - }, - 'Admin logs in': (client) => { - const {users} = client.globals; - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .login(users.admin); - }, - after: (client) => { - client.end(); - } -}; diff --git a/test/e2e/tests/Commenter/FlagCommentTest.js b/test/e2e/tests/Commenter/FlagCommentTest.js deleted file mode 100644 index 87927528f..000000000 --- a/test/e2e/tests/Commenter/FlagCommentTest.js +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = { - '@tags': ['flag', 'comments', 'commenter'], - before: (client) => { - const embedStreamPage = client.page.embedStreamPage(); - const {users} = client.globals; - - embedStreamPage - .navigate() - .ready(); - - embedStreamPage - .login(users.commenter); - }, - 'Commenter flags a comment': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .flagComment() - .waitForElementVisible('@flagPopUp') - .waitForElementVisible('@flagCommentOption') - .click('@flagCommentOption') - .waitForElementVisible('@flagDoneButton') - .click('@flagDoneButton') - .waitForElementVisible('@flagOtherOption') - .click('@flagOtherOption') - .waitForElementVisible('@flagDoneButton') - .click('@flagDoneButton') - .click('@flagDoneButton') - .expect.element('@flagButtonText').text.to.equal('Reported'); - }, - after: (client) => { - client.end(); - } -}; diff --git a/test/e2e/tests/Commenter/FlagUsernameTest.js b/test/e2e/tests/Commenter/FlagUsernameTest.js deleted file mode 100644 index c25299df9..000000000 --- a/test/e2e/tests/Commenter/FlagUsernameTest.js +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = { - '@tags': ['flag', 'commenter'], - before: (client) => { - const embedStreamPage = client.page.embedStreamPage(); - const {users} = client.globals; - - embedStreamPage - .navigate() - .ready(); - - embedStreamPage - .login(users.commenter); - }, - 'Commenter flags a username': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .flagUsername() - .click('@flagButton') - .waitForElementVisible('@flagPopUp') - .waitForElementVisible('@flagUsernameOption') - .click('@flagUsernameOption') - .waitForElementVisible('@flagDoneButton') - .click('@flagDoneButton') - .waitForElementVisible('@flagOtherOption') - .click('@flagOtherOption') - .waitForElementVisible('@flagDoneButton') - .click('@flagDoneButton') - .click('@flagDoneButton'); - }, - after: (client) => { - client.end(); - } -}; diff --git a/test/e2e/tests/Commenter/LikeCommentTest.js b/test/e2e/tests/Commenter/LikeCommentTest.js deleted file mode 100644 index b8a964558..000000000 --- a/test/e2e/tests/Commenter/LikeCommentTest.js +++ /dev/null @@ -1,27 +0,0 @@ -module.exports = { - '@tags': ['like', 'comments', 'commenter'], - before: (client) => { - const embedStreamPage = client.page.embedStreamPage(); - const {users} = client.globals; - - embedStreamPage - .navigate() - .ready(); - - embedStreamPage - .login(users.commenter); - }, - 'Commenter likes a comment': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .postComment(`hi ${Math.random()}`) - .likeComment() - .waitForElementVisible('@likesCount', 2000) - .expect.element('@likeText').text.to.equal('Liked'); - - }, - after: (client) => { - client.end(); - } -}; diff --git a/test/e2e/tests/Commenter/LoginTest.js b/test/e2e/tests/Commenter/LoginTest.js deleted file mode 100644 index bc04f72d3..000000000 --- a/test/e2e/tests/Commenter/LoginTest.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - '@tags': ['login', 'commenter'], - before: (client) => { - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .navigate() - .ready(); - }, - 'Commenter logs in': (client) => { - const {users} = client.globals; - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .login(users.commenter); - }, - after: (client) => { - client.end(); - } -}; diff --git a/test/e2e/tests/Commenter/PermalinkTest.js b/test/e2e/tests/Commenter/PermalinkTest.js deleted file mode 100644 index d3d369264..000000000 --- a/test/e2e/tests/Commenter/PermalinkTest.js +++ /dev/null @@ -1,34 +0,0 @@ -let permalink = ''; - -module.exports = { - '@tags': ['permalink', 'commenter'], - before: (client) => { - const embedStreamPage = client.page.embedStreamPage(); - const {users} = client.globals; - - embedStreamPage - .navigate() - .ready(); - - embedStreamPage - .login(users.commenter); - }, - 'Commenter gets the permalink of a comment': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - embedStreamPage - .getPermalink((value) => { - permalink = value; - }); - }, - 'Commenter navigates to the permalink': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - embedStreamPage - .navigate(permalink); - - client.assert.urlContains(permalink); - }, - after: (client) => { - client.end(); - } -}; - diff --git a/test/e2e/tests/Commenter/PostComment.js b/test/e2e/tests/Commenter/PostComment.js deleted file mode 100644 index 033d52cbe..000000000 --- a/test/e2e/tests/Commenter/PostComment.js +++ /dev/null @@ -1,38 +0,0 @@ -module.exports = { - '@tags': ['write', 'commenter'], - before: (client) => { - const embedStreamPage = client.page.embedStreamPage(); - const {users} = client.globals; - - embedStreamPage - .navigate() - .ready(); - - embedStreamPage - .login(users.commenter); - }, - 'Commenter posts a comment': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .postComment('I read the comments'); - }, - after: (client) => { - const adminPage = client.page.adminPage(); - const embedStreamPage = client.page.embedStreamPage(); - const {users} = client.globals; - - embedStreamPage - .logout() - .login(users.admin); - - adminPage - .navigate() - .ready(); - - adminPage - .approveComment(); - - client.end(); - } -}; diff --git a/test/e2e/tests/EmbedStreamTests.js b/test/e2e/tests/EmbedStreamTests.js deleted file mode 100644 index 0db0ad4d7..000000000 --- a/test/e2e/tests/EmbedStreamTests.js +++ /dev/null @@ -1,182 +0,0 @@ -const mongoose = require('../../helpers/mongoose'); -const mocks = require('../mocks'); - -const mockComment = 'I read the comments'; -const mockReply = 'This is a test reply'; -const mockUser = { - email: `${Date.now()}@test.com`, - name: `testuser${Math.random() - .toString() - .slice(-5)}`, - pw: 'testtest' -}; - -module.exports = { - '@tags': ['embed-stream', 'comment', 'premodoff', 'premodon'], - before: () => { - mongoose.waitTillConnect(function(err) { - if (err) { - console.error(err); - } - }); - }, - 'User registers and posts a comment with premod off': (client) => { - client.perform((client, done) => { - mocks.settings({moderation: 'POST'}) - .then(() => { - - // Load Page - client.resizeWindow(1200, 800) - .url(client.globals.baseUrl) - .frame('coralStreamEmbed_iframe') - - // Register and Log In - .waitForElementVisible('#coralSignInButton', 2000) - .click('#coralSignInButton') - .waitForElementVisible('#coralRegister', 1000) - .click('#coralRegister') - .waitForElementVisible('#email', 1000) - .setValue('#email', mockUser.email) - .setValue('#username', mockUser.name) - .setValue('#password', mockUser.pw) - .setValue('#confirmPassword', mockUser.pw) - .click('#coralSignUpButton') - .waitForElementVisible('#coralLogInButton', 10000) - .click('#coralLogInButton') - .waitForElementVisible('.talk-plugin-commentbox-button', 4000) - - // Post a comment - .setValue('.talk-plugin-commentbox-textarea', mockComment) - .click('.talk-plugin-commentbox-button') - .waitForElementVisible('.embed__stream .talk-plugin-commentcontent-text', 1000) - - // Verify that it appears - .assert.containsText('.embed__stream .talk-plugin-commentcontent-text', mockComment); - done(); - }) - .catch((err) => { - console.log(err); - done(); - }); - }); - }, - 'User posts a comment with premod on': (client) => { - client.perform((client, done) => { - mocks.settings({moderation: 'PRE'}) - .then(() => { - - // Load Page - client.url(client.globals.baseUrl) - .frame('coralStreamEmbed_iframe'); - - // Post a comment - client.waitForElementVisible('.talk-plugin-commentbox-button', 2000) - .setValue('.talk-plugin-commentbox-textarea', mockComment) - .click('.talk-plugin-commentbox-button'); - done(); - }) - .catch((err) => { - console.log(err); - done(); - }); - }); - }, - 'User replies to a comment with premod off': (client) => { - client.perform((client, done) => { - mocks.settings({moderation: 'POST'}) - .then(() => { - - // Load Page - client.resizeWindow(1200, 800) - .url(client.globals.baseUrl) - .frame('coralStreamEmbed_iframe'); - - // Post a comment - client.waitForElementVisible('.talk-plugin-commentbox-button', 2000) - .setValue('.talk-plugin-commentbox-textarea', mockComment) - .click('.talk-plugin-commentbox-button') - - // Post a reply - - .waitForElementVisible('.embed__stream .talk-plugin-replies-reply-button', 5000) - .click('.embed__stream .talk-plugin-replies-reply-button') - .waitForElementVisible('#replyText') - .setValue('#replyText', mockReply) - .click('.embed__stream .talk-plugin-replies-textarea .talk-plugin-commentbox-button') - .waitForElementVisible('.embed__stream .reply', 20000) - - // Verify that it appears - .assert.containsText('.embed__stream .reply', mockReply); - done(); - }) - .catch((err) => { - console.log(err); - done(); - }); - }); - }, - - // Commenting out this test, it fails if the notification is below the fold, which - // happens if too many previous tests have added comments - // 'User replies to a comment with premod on': client => { - // client.perform((client, done) => { - // mocks.settings({moderation: 'PRE'}) - // - // // Add a mock user - // .then(() => mocks.users([{ - // username: 'BabyBlue', - // email: 'whale@tale.sea', - // password: 'krillaretasty' - // }])) - // - // // Add a mock preapproved comment by that user - // .then((user) => mocks.comments([{ - // body: 'Whales are not fish.', - // status: 'ACCEPTED', - // author_id: user.id - // }])) - // .then(() => { - // - // // Load Page - // client.resizeWindow(1200, 800) - // .url(client.globals.baseUrl) - // .frame('coralStreamEmbed_iframe'); - // - // // Post a reply - // client.waitForElementVisible('.talk-plugin-replies-reply-button', 5000) - // .click('.talk-plugin-replies-reply-button') - // .waitForElementVisible('#replyText') - // .setValue('#replyText', mockReply) - // .click('.talk-plugin-replies-textarea button') - // .waitForElementVisible('#coral-notif', 1000) - // - // // Verify that it appears - // .assert.containsText('#coral-notif', 'moderation team'); - // done(); - // }) - // .catch((err) => { - // console.log(err); - // done(); - // }); - // }); - // }, - 'Total comment count premod on': (client) => { - client.perform((client, done) => { - client.url(client.globals.baseUrl) - .frame('coralStreamEmbed_iframe'); - - // Verify that comment count is correct - client.waitForElementVisible('.talk-plugin-comment-count-text', 2000) - .assert.containsText('.talk-plugin-comment-count-text', '5 Comments'); - done(); - }); - }, - after: (client) => { - mongoose.disconnect(function(err) { - if (err) { - console.error(err); - } - }); - client.end(); - } -}; diff --git a/test/e2e/tests/Moderator/LoginTest.js b/test/e2e/tests/Moderator/LoginTest.js deleted file mode 100644 index bb32679ee..000000000 --- a/test/e2e/tests/Moderator/LoginTest.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - '@tags': ['login', 'MODERATOR'], - before: (client) => { - const embedStreamPage = client.page.embedStreamPage(); - const {launchUrl} = client; - - client - .url(launchUrl); - - embedStreamPage - .ready(); - }, - 'Moderator logs in': (client) => { - const {users} = client.globals; - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .login(users.moderator); - }, - after: (client) => { - client.end(); - } -}; diff --git a/test/e2e/tests/Visitor/FlagCommentTest.js b/test/e2e/tests/Visitor/FlagCommentTest.js deleted file mode 100644 index b4a5a802f..000000000 --- a/test/e2e/tests/Visitor/FlagCommentTest.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - '@tags': ['flag', 'comments', 'visitor'], - before: (client) => { - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .navigate() - .ready(); - }, - 'Visitor tries to flag a comment': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .flagComment() - .waitForElementVisible('@signInDialog', 2000); - }, - after: (client) => { - client.end(); - } -}; diff --git a/test/e2e/tests/Visitor/LikeCommentTest.js b/test/e2e/tests/Visitor/LikeCommentTest.js deleted file mode 100644 index 1b931683e..000000000 --- a/test/e2e/tests/Visitor/LikeCommentTest.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - '@tags': ['like', 'comments', 'visitor'], - before: (client) => { - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .navigate() - .ready(); - }, - 'Visitor tries to like a comment': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .likeComment() - .waitForElementVisible('@signInDialog', 2000); - }, - after: (client) => { - client.end(); - } -}; diff --git a/test/e2e/tests/Visitor/SignUpTest.js b/test/e2e/tests/Visitor/SignUpTest.js deleted file mode 100644 index 3436ca850..000000000 --- a/test/e2e/tests/Visitor/SignUpTest.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - '@tags': ['signup', 'visitor'], - before: (client) => { - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .navigate() - .ready(); - }, - 'Visitor signs up': (client) => { - const embedStreamPage = client.page.embedStreamPage(); - - embedStreamPage - .signUp({ - email: `visitor_${Date.now()}@test.com`, - username: `visitor${Date.now()}`, - pass: 'testtest' - }); - }, - after: (client) => { - client.end(); - } -}; diff --git a/test/helpers/browser.js b/test/helpers/browser.js deleted file mode 100644 index 07da2a566..000000000 --- a/test/helpers/browser.js +++ /dev/null @@ -1,51 +0,0 @@ -/* eslint-env browser */ - -const jsdom = require('jsdom').jsdom; -const fs = require('fs'); -const path = require('path'); - -// Storage Mock -function storageMock() { - const storage = {}; - - return { - setItem: function(key, value) { - storage[key] = value || ''; - }, - getItem: function(key) { - return storage[key] || null; - }, - removeItem: function(key) { - delete storage[key]; - }, - get length() { - return Object.keys(storage).length; - }, - key: function(i) { - const keys = Object.keys(storage); - return keys[i] || null; - } - }; -} - -global.document = jsdom(fs.readFileSync(path.resolve(__dirname, 'index.test.html'))); -global.window = document.defaultView; - -// these lines are required for react-mdl -global.window.CustomEvent = undefined; -require('react-mdl/extra/material'); - -global.Element = global.window.Element; - -global.navigator = { - userAgent: 'node.js' -}; - -global.documentRef = document; -global.localStorage = {}; -global.sessionStorage = storageMock(); -global.XMLHttpRequest = storageMock(); - -global.Headers = function(headers) { - return headers; -}; diff --git a/yarn.lock b/yarn.lock index 818049337..eb513b370 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,7 +55,7 @@ dependencies: "@types/node" "*" -abab@^1.0.0, abab@^1.0.3: +abab@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" @@ -82,7 +82,7 @@ acorn-globals@^1.0.4: dependencies: acorn "^2.1.0" -acorn-globals@^3.0.0, acorn-globals@^3.1.0: +acorn-globals@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf" dependencies: @@ -114,13 +114,6 @@ addressparser@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746" -agent-base@2: - version "2.0.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-2.0.1.tgz#bd8f9e86a8eb221fffa07bd14befd55df142815e" - dependencies: - extend "~3.0.0" - semver "~5.0.1" - ajv-keywords@^1.0.0, ajv-keywords@^1.1.1: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" @@ -144,12 +137,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" -alter@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/alter/-/alter-0.2.0.tgz#c7588808617572034aae62480af26b1d4d1cb3cd" - dependencies: - stable "~0.1.3" - amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" @@ -264,10 +251,6 @@ arr-flatten@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.3.tgz#a274ed85ac08849b6bd7847c4580745dc51adfb1" -array-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -293,7 +276,7 @@ array.prototype.find@^2.0.1: define-properties "^1.1.2" es-abstract "^1.7.0" -arrify@^1.0.0, arrify@^1.0.1: +arrify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -327,26 +310,10 @@ assert@^1.1.1: dependencies: util "0.10.3" -assertion-error@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.0.tgz#c7f85438fdd466bc7ca16ab90c81513797a5d23b" - assertion-error@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" -ast-traverse@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ast-traverse/-/ast-traverse-0.1.1.tgz#69cf2b8386f19dcda1bb1e05d68fe359d8897de6" - -ast-types@0.8.12: - version "0.8.12" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.12.tgz#a0d90e4351bb887716c83fd637ebf818af4adfcc" - -ast-types@0.9.6, ast-types@0.x.x: - version "0.9.6" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" - async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" @@ -355,10 +322,6 @@ async@0.2.x: version "0.2.10" resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" -async@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/async/-/async-1.2.1.tgz#a4816a17cd5ff516dfa2c7698a453369b9790de0" - async@1.x, async@^1.4.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -369,7 +332,7 @@ async@2.1.4: dependencies: lodash "^4.14.0" -async@^2.0.1, async@^2.1.2, async@^2.1.4, async@^2.5.0: +async@^2.0.1, async@^2.1.2, async@^2.1.4: version "2.5.0" resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" dependencies: @@ -431,7 +394,7 @@ babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.0" -babel-core@^6.0.0, babel-core@^6.24.0, babel-core@^6.24.1: +babel-core@^6.24.0, babel-core@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.24.1.tgz#8c428564dce1e1f41fb337ec34f4c3b022b5ad83" dependencies: @@ -477,22 +440,6 @@ babel-generator@^6.18.0, babel-generator@^6.24.1: source-map "^0.5.0" trim-right "^1.0.1" -babel-helper-bindify-decorators@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz#14c19e5f142d7b47f19a52431e52b1ccbc40a330" - dependencies: - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" - dependencies: - babel-helper-explode-assignable-expression "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - babel-helper-builder-react-jsx@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.24.1.tgz#0ad7917e33c8d751e646daca4e77cc19377d2cbc" @@ -519,23 +466,6 @@ babel-helper-define-map@^6.24.1: babel-types "^6.24.1" lodash "^4.2.0" -babel-helper-explode-assignable-expression@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" - dependencies: - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-explode-class@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz#7dc2a3910dee007056e1e31d640ced3d54eaa9eb" - dependencies: - babel-helper-bindify-decorators "^6.24.1" - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - babel-helper-function-name@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" @@ -603,14 +533,6 @@ babel-helpers@^6.24.1: babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-jest@^19.0.0: - version "19.0.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-19.0.0.tgz#59323ced99a3a84d359da219ca881074ffc6ce3f" - dependencies: - babel-core "^6.0.0" - babel-plugin-istanbul "^4.0.0" - babel-preset-jest "^19.0.0" - babel-loader@^6.4.1: version "6.4.1" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-6.4.1.tgz#0b34112d5b0748a8dcdbf51acf6f9bd42d50b8ca" @@ -636,62 +558,22 @@ babel-plugin-check-es2015-constants@^6.22.0: dependencies: babel-runtime "^6.22.0" -babel-plugin-istanbul@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.1.tgz#c12de0fc6fe42adfb16be56f1ad11e4a9782eca9" - dependencies: - find-up "^2.1.0" - istanbul-lib-instrument "^1.6.2" - test-exclude "^4.0.3" - -babel-plugin-jest-hoist@^19.0.0: - version "19.0.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-19.0.0.tgz#4ae2a04ea612a6e73651f3fde52c178991304bea" - babel-plugin-syntax-async-functions@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" -babel-plugin-syntax-async-generators@^6.5.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" - -babel-plugin-syntax-class-constructor-call@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416" - babel-plugin-syntax-class-properties@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" -babel-plugin-syntax-decorators@^6.1.18, babel-plugin-syntax-decorators@^6.13.0: +babel-plugin-syntax-decorators@^6.1.18: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b" -babel-plugin-syntax-do-expressions@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz#5747756139aa26d390d09410b03744ba07e4796d" - -babel-plugin-syntax-dynamic-import@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" - -babel-plugin-syntax-exponentiation-operator@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" - -babel-plugin-syntax-export-extensions@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721" - babel-plugin-syntax-flow@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" -babel-plugin-syntax-function-bind@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz#48c495f177bdf31a981e732f55adc0bdd2601f46" - babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" @@ -700,19 +582,7 @@ babel-plugin-syntax-object-rest-spread@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" -babel-plugin-syntax-trailing-function-commas@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" - -babel-plugin-transform-async-generator-functions@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db" - dependencies: - babel-helper-remap-async-to-generator "^6.24.1" - babel-plugin-syntax-async-generators "^6.5.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-async-to-generator@^6.16.0, babel-plugin-transform-async-to-generator@^6.24.1: +babel-plugin-transform-async-to-generator@^6.16.0: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" dependencies: @@ -720,15 +590,7 @@ babel-plugin-transform-async-to-generator@^6.16.0, babel-plugin-transform-async- babel-plugin-syntax-async-functions "^6.8.0" babel-runtime "^6.22.0" -babel-plugin-transform-class-constructor-call@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz#80dc285505ac067dcb8d6c65e2f6f11ab7765ef9" - dependencies: - babel-plugin-syntax-class-constructor-call "^6.18.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-class-properties@^6.23.0, babel-plugin-transform-class-properties@^6.24.1: +babel-plugin-transform-class-properties@^6.23.0: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" dependencies: @@ -745,23 +607,6 @@ babel-plugin-transform-decorators-legacy@^1.3.4: babel-runtime "^6.2.0" babel-template "^6.3.0" -babel-plugin-transform-decorators@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d" - dependencies: - babel-helper-explode-class "^6.24.1" - babel-plugin-syntax-decorators "^6.13.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-do-expressions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz#28ccaf92812d949c2cd1281f690c8fdc468ae9bb" - dependencies: - babel-plugin-syntax-do-expressions "^6.8.0" - babel-runtime "^6.22.0" - babel-plugin-transform-es2015-arrow-functions@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" @@ -930,21 +775,6 @@ babel-plugin-transform-es2015-unicode-regex@^6.24.1: babel-runtime "^6.22.0" regexpu-core "^2.0.0" -babel-plugin-transform-exponentiation-operator@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" - dependencies: - babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" - babel-plugin-syntax-exponentiation-operator "^6.8.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-export-extensions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz#53738b47e75e8218589eea946cbbd39109bbe653" - dependencies: - babel-plugin-syntax-export-extensions "^6.8.0" - babel-runtime "^6.22.0" - babel-plugin-transform-flow-strip-types@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf" @@ -952,20 +782,13 @@ babel-plugin-transform-flow-strip-types@^6.22.0: babel-plugin-syntax-flow "^6.18.0" babel-runtime "^6.22.0" -babel-plugin-transform-function-bind@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz#c6fb8e96ac296a310b8cf8ea401462407ddf6a97" - dependencies: - babel-plugin-syntax-function-bind "^6.8.0" - babel-runtime "^6.22.0" - babel-plugin-transform-object-assign@^6.8.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-assign/-/babel-plugin-transform-object-assign-6.22.0.tgz#f99d2f66f1a0b0d498e346c5359684740caa20ba" dependencies: babel-runtime "^6.22.0" -babel-plugin-transform-object-rest-spread@^6.22.0, babel-plugin-transform-object-rest-spread@^6.23.0: +babel-plugin-transform-object-rest-spread@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921" dependencies: @@ -1056,12 +879,6 @@ babel-preset-flow@^6.23.0: dependencies: babel-plugin-transform-flow-strip-types "^6.22.0" -babel-preset-jest@^19.0.0: - version "19.0.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-19.0.0.tgz#22d67201d02324a195811288eb38294bb3cac396" - dependencies: - babel-plugin-jest-hoist "^19.0.0" - babel-preset-react@^6.23.0: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-preset-react/-/babel-preset-react-6.24.1.tgz#ba69dfaea45fc3ec639b6a4ecea6e17702c91380" @@ -1073,41 +890,6 @@ babel-preset-react@^6.23.0: babel-plugin-transform-react-jsx-source "^6.22.0" babel-preset-flow "^6.23.0" -babel-preset-stage-0@^6.16.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz#5642d15042f91384d7e5af8bc88b1db95b039e6a" - dependencies: - babel-plugin-transform-do-expressions "^6.22.0" - babel-plugin-transform-function-bind "^6.22.0" - babel-preset-stage-1 "^6.24.1" - -babel-preset-stage-1@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0" - dependencies: - babel-plugin-transform-class-constructor-call "^6.24.1" - babel-plugin-transform-export-extensions "^6.22.0" - babel-preset-stage-2 "^6.24.1" - -babel-preset-stage-2@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1" - dependencies: - babel-plugin-syntax-dynamic-import "^6.18.0" - babel-plugin-transform-class-properties "^6.24.1" - babel-plugin-transform-decorators "^6.24.1" - babel-preset-stage-3 "^6.24.1" - -babel-preset-stage-3@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz#836ada0a9e7a7fa37cb138fb9326f87934a48395" - dependencies: - babel-plugin-syntax-trailing-function-commas "^6.22.0" - babel-plugin-transform-async-generator-functions "^6.24.1" - babel-plugin-transform-async-to-generator "^6.24.1" - babel-plugin-transform-exponentiation-operator "^6.24.1" - babel-plugin-transform-object-rest-spread "^6.22.0" - babel-register@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f" @@ -1214,12 +996,6 @@ bindings@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" -bl@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.0.tgz#1397e7ec42c5f5dc387470c500e34a9f6be9ea98" - dependencies: - readable-stream "^2.0.5" - block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -1294,10 +1070,6 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" -breakable@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/breakable/-/breakable-1.0.0.tgz#784a797915a38ead27bad456b5572cb4bbaa78c1" - brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -1368,10 +1140,6 @@ bson@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.4.tgz#93c10d39eaa5b58415cbc4052f3e53e562b0b72c" -buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -1430,7 +1198,7 @@ callsites@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" -camelcase@^1.0.2, camelcase@^1.2.1: +camelcase@^1.0.2: version "1.2.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" @@ -1455,10 +1223,6 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: version "1.0.30000662" resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000662.tgz#616b17a525b52fec14611f88af3d5a9b5438c050" -caseless@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" - caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -1486,13 +1250,6 @@ chai-http@^3.0.0: qs "^6.2.0" superagent "^2.0.0" -chai-nightwatch@~0.1.x: - version "0.1.1" - resolved "https://registry.yarnpkg.com/chai-nightwatch/-/chai-nightwatch-0.1.1.tgz#1ca56de768d3c0868fe7fc2f4d32c2fe894e6be9" - dependencies: - assertion-error "1.0.0" - deep-eql "0.1.3" - chai@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247" @@ -1728,10 +1485,6 @@ co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" -co@~3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/co/-/co-3.0.6.tgz#1445f226c5eb956138e68c9ac30167ea7d2e6bda" - coa@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.1.tgz#7f959346cfc8719e3f7233cd6852854a7c67d8a3" @@ -1808,17 +1561,13 @@ combined-stream@~0.0.4: dependencies: delayed-stream "0.0.5" -commander@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" - commander@2.8.x: version "2.8.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" dependencies: graceful-readlink ">= 1.0.0" -commander@2.9.0, commander@^2.5.0, commander@^2.8.1, commander@^2.9.0: +commander@2.9.0, commander@^2.8.1, commander@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" dependencies: @@ -1838,20 +1587,6 @@ commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" -commoner@~0.10.3: - version "0.10.8" - resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5" - dependencies: - commander "^2.5.0" - detective "^4.3.1" - glob "^5.0.15" - graceful-fs "^4.1.2" - iconv-lite "^0.4.5" - mkdirp "^0.5.0" - private "^0.1.6" - q "^1.1.2" - recast "^0.11.17" - component-emitter@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" @@ -1907,13 +1642,6 @@ configstore@^1.0.0: write-file-atomic "^1.1.2" xdg-basedir "^2.0.0" -connect-redis@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/connect-redis/-/connect-redis-3.2.0.tgz#2d29ea60c8ae8c2c818a710247fdfed158f43388" - dependencies: - debug "^2.2.0" - redis "^2.1.0" - connect@3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.0.tgz#f09a4f7dcd17324b663b725c815bdb1c4158a46e" @@ -1958,10 +1686,6 @@ content-security-policy-builder@1.1.0: dependencies: dashify "^0.2.0" -content-type-parser@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.1.tgz#c3e56988c53c65127fb46d4032a3a900246fdc94" - content-type@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" @@ -2039,10 +1763,6 @@ cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: os-homedir "^1.0.1" require-from-string "^1.1.0" -crc@3.4.4: - version "3.4.4" - resolved "https://registry.yarnpkg.com/crc/-/crc-3.4.4.tgz#9da1e980e3bd44fc5c93bf5ab3da3378d85e466b" - create-ecdh@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" @@ -2106,14 +1826,6 @@ crypto-browserify@^3.11.0: public-encrypt "^4.0.0" randombytes "^2.0.0" -csrf@~3.0.3: - version "3.0.6" - resolved "https://registry.yarnpkg.com/csrf/-/csrf-3.0.6.tgz#b61120ddceeafc91e76ed5313bb5c0b2667b710a" - dependencies: - rndm "1.2.0" - tsscmp "1.0.5" - uid-safe "2.1.4" - css-color-function@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.0.tgz#72c767baf978f01b8a8a94f42f17ba5d22a776fc" @@ -2236,25 +1948,16 @@ csso@~2.3.1: clap "^1.0.9" source-map "^0.5.3" -cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", "cssom@>= 0.3.2 < 0.4.0": +cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0": version "0.3.2" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" -"cssstyle@>= 0.2.29 < 0.3.0", "cssstyle@>= 0.2.37 < 0.3.0": +"cssstyle@>= 0.2.29 < 0.3.0": version "0.2.37" resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54" dependencies: cssom "0.3.x" -csurf@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/csurf/-/csurf-1.9.0.tgz#49d2c6925ffcec7b7de559597c153fa533364133" - dependencies: - cookie "0.3.1" - cookie-signature "1.0.6" - csrf "~3.0.3" - http-errors "~1.5.0" - 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" @@ -2296,10 +1999,6 @@ dashify@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.2.2.tgz#6a07415a01c91faf4a32e38d9dfba71f61cb20fe" -data-uri-to-buffer@0: - version "0.0.4" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-0.0.4.tgz#46e13ab9da8e309745c8d01ce547213ebdb2fe3f" - dataloader@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.3.0.tgz#6fec5be4b30a712e4afd30b86b4334566b97673b" @@ -2308,7 +2007,7 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" -debug@*, debug@2, debug@2.6.4, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.3: +debug@*, debug@2.6.4, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.3: version "2.6.4" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.4.tgz#7586a9b3c39741c0282ae33445c4e8ac74734fe0" dependencies: @@ -2338,12 +2037,6 @@ debug@2.6.1: dependencies: ms "0.7.2" -debug@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" - dependencies: - ms "0.7.2" - debug@~0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" @@ -2352,7 +2045,7 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" -deep-eql@0.1.3, deep-eql@^0.1.3: +deep-eql@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" dependencies: @@ -2389,29 +2082,6 @@ defined@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" -defs@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/defs/-/defs-1.1.1.tgz#b22609f2c7a11ba7a3db116805c139b1caffa9d2" - dependencies: - alter "~0.2.0" - ast-traverse "~0.1.1" - breakable "~1.0.0" - esprima-fb "~15001.1001.0-dev-harmony-fb" - simple-fmt "~0.1.0" - simple-is "~0.2.0" - stringmap "~0.2.2" - stringset "~0.2.1" - tryor "~0.1.2" - yargs "~3.27.0" - -degenerator@~1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095" - dependencies: - ast-types "0.x.x" - escodegen "1.x.x" - esprima "3.x.x" - del@^2.0.2: version "2.2.2" resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" @@ -2465,21 +2135,10 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" -detective@^4.3.1: - version "4.5.0" - resolved "https://registry.yarnpkg.com/detective/-/detective-4.5.0.tgz#6e5a8c6b26e6c7a254b1c6b6d7490d98ec91edd1" - dependencies: - acorn "^4.0.3" - defined "^1.0.0" - dialog-polyfill@^0.4.4: version "0.4.7" resolved "https://registry.yarnpkg.com/dialog-polyfill/-/dialog-polyfill-0.4.7.tgz#e9995d519f1df349597193198c184ea9402fdbf5" -diff@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" - diff@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" @@ -2537,19 +2196,32 @@ domelementtype@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" -domhandler@2.3, domhandler@^2.3.0: +domhandler@2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738" dependencies: domelementtype "1" -domutils@1.5, domutils@1.5.1, domutils@^1.5.1: +domhandler@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" + dependencies: + domelementtype "1" + +domutils@1.5, domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" dependencies: dom-serializer "0" domelementtype "1" +domutils@^1.5.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" + dependencies: + dom-serializer "0" + domelementtype "1" + dont-sniff-mimetype@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz#5932890dc9f4e2f19e5eb02a20026e5e5efc8f58" @@ -2592,10 +2264,6 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" -ejs@0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-0.8.3.tgz#db8aac47ff80a7df82b4c82c126fe8970870626f" - ejs@^2.5.6: version "2.5.6" resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88" @@ -2636,12 +2304,6 @@ end-of-stream@1.0.0: dependencies: once "~1.3.0" -end-of-stream@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206" - dependencies: - once "^1.4.0" - enhanced-resolve@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.1.0.tgz#9f4b626f577245edcf4b2ad83d86e17f4f421dec" @@ -2663,20 +2325,20 @@ env-rewrite@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/env-rewrite/-/env-rewrite-1.0.2.tgz#3e344a95af1bdaab34a559479b8be3abd0804183" -enzyme@^2.6.0: - version "2.8.2" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-2.8.2.tgz#6c8bcb05012abc4aa4bc3213fb23780b9b5b1714" +enzyme@^2.9.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-2.9.1.tgz#07d5ce691241240fb817bf2c4b18d6e530240df6" dependencies: cheerio "^0.22.0" function.prototype.name "^1.0.0" is-subset "^0.1.1" - lodash "^4.17.2" + lodash "^4.17.4" object-is "^1.0.1" object.assign "^4.0.4" - object.entries "^1.0.3" - object.values "^1.0.3" - prop-types "^15.5.4" - uuid "^2.0.3" + object.entries "^1.0.4" + object.values "^1.0.4" + prop-types "^15.5.10" + uuid "^3.0.1" errno@^0.1.3: version "0.1.4" @@ -2690,7 +2352,17 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.6.1, es-abstract@^1.7.0: +es-abstract@^1.6.1: + version "1.8.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.8.0.tgz#3b00385e85729932beffa9163bbea1234e932914" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.0" + has "^1.0.1" + is-callable "^1.1.3" + is-regex "^1.0.4" + +es-abstract@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c" dependencies: @@ -2783,7 +2455,7 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1 version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -escodegen@1.x.x, escodegen@^1.6.1: +escodegen@^1.6.1: version "1.8.1" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" dependencies: @@ -2918,18 +2590,14 @@ espree@^3.4.0: acorn "^5.0.1" acorn-jsx "^3.0.0" -esprima-fb@~15001.1001.0-dev-harmony-fb: - version "15001.1001.0-dev-harmony-fb" - resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz#43beb57ec26e8cf237d3dd8b33e42533577f2659" - -esprima@3.x.x, esprima@^3.1.1, esprima@~3.1.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - esprima@^2.6.0, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" +esprima@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + esquery@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" @@ -3023,20 +2691,6 @@ exports-loader@^0.6.4: loader-utils "^1.0.2" source-map "0.5.x" -express-session@^1.15.1: - version "1.15.2" - resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.15.2.tgz#d98516443a4ccb8688e1725ae584c02daa4093d4" - dependencies: - cookie "0.3.1" - cookie-signature "1.0.6" - crc "3.4.4" - debug "2.6.3" - depd "~1.1.0" - on-headers "~1.0.1" - parseurl "~1.3.1" - uid-safe "~2.1.4" - utils-merge "1.0.0" - express@^4.12.2, express@^4.15.2: version "4.15.2" resolved "https://registry.yarnpkg.com/express/-/express-4.15.2.tgz#af107fc148504457f2dca9a6f2571d7129b97b35" @@ -3070,14 +2724,14 @@ express@^4.12.2, express@^4.15.2: utils-merge "1.0.0" vary "~1.1.0" -extend@3, extend@^3.0.0, extend@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" - extend@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/extend/-/extend-1.3.0.tgz#d1516fb0ff5624d2ebf9123ea1dac5a1994004f8" +extend@^3.0.0, extend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" + external-editor@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.4.tgz#1ed9199da9cbfe2ef2f7a31b2fde8b0d12368972" @@ -3104,7 +2758,7 @@ fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" -fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.9: +fbjs@^0.8.1, fbjs@^0.8.9: version "0.8.12" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04" dependencies: @@ -3116,20 +2770,6 @@ fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.9: setimmediate "^1.0.5" ua-parser-js "^0.7.9" -fd-slicer@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" - dependencies: - pend "~1.2.0" - -fetch-mock@^5.5.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-5.10.0.tgz#52e29c72800640e48410602fe076ac3615e590ad" - dependencies: - glob-to-regexp "^0.3.0" - node-fetch "^1.3.3" - path-to-regexp "^1.7.0" - figures@^1.3.5: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -3156,10 +2796,6 @@ file-loader@^0.11.2: dependencies: loader-utils "^1.0.2" -file-uri-to-path@0: - version "0.0.2" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-0.0.2.tgz#37cdd1b5b905404b3f05e1b23645be694ff70f82" - filename-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775" @@ -3220,12 +2856,6 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^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" - findup@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/findup/-/findup-0.1.5.tgz#8ad929a3393bac627957a7e5de4623b06b0e2ceb" @@ -3373,24 +3003,17 @@ fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.10: mkdirp ">=0.5 0" rimraf "2" -ftp@~0.3.5: - version "0.3.10" - resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" - dependencies: - readable-stream "1.1.x" - xregexp "2.0.0" - function-bind@^1.0.2, function-bind@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" function.prototype.name@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.0.0.tgz#5f523ca64e491a5f95aba80cc1e391080a14482e" + version "1.0.3" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.0.3.tgz#0099ae5572e9dd6f03c97d023fd92bcc5e639eac" dependencies: define-properties "^1.1.2" function-bind "^1.1.0" - is-callable "^1.1.2" + is-callable "^1.1.3" gauge@~2.7.1: version "2.7.4" @@ -3425,17 +3048,6 @@ get-caller-file@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" -get-uri@1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-1.1.0.tgz#7375d04daf7fcb584b3632679cbdf339b51bb149" - dependencies: - data-uri-to-buffer "0" - debug "2" - extend "3" - file-uri-to-path "0" - ftp "~0.3.5" - readable-stream "2" - getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -3510,21 +3122,6 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" -glob-to-regexp@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" - -glob@7.0.5: - version "7.0.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@7.0.x: version "7.0.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" @@ -3547,7 +3144,7 @@ glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^5.0.15, glob@^5.0.3: +glob@^5.0.3: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" dependencies: @@ -3768,15 +3365,6 @@ har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" -har-validator@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" - dependencies: - chalk "^1.1.1" - commander "^2.9.0" - is-my-json-valid "^2.12.4" - pinkie-promise "^2.0.0" - har-validator@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" @@ -3926,12 +3514,6 @@ html-comment-regex@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" -html-encoding-sniffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.1.tgz#79bf7a785ea495fe66165e734153f363ff5437da" - dependencies: - whatwg-encoding "^1.0.1" - htmlparser2@^3.9.1: version "3.9.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" @@ -3953,14 +3535,6 @@ htmlparser2@~3.8.1: entities "1.0" readable-stream "1.1" -http-errors@~1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" - dependencies: - inherits "2.0.3" - setprototypeof "1.0.2" - statuses ">= 1.3.1 < 2" - http-errors@~1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" @@ -3970,14 +3544,6 @@ http-errors@~1.6.1: setprototypeof "1.0.3" statuses ">= 1.3.1 < 2" -http-proxy-agent@1: - version "1.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-1.0.0.tgz#cc1ce38e453bf984a0f7702d2dd59c73d081284a" - dependencies: - agent-base "2" - debug "2" - extend "3" - http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" @@ -4001,23 +3567,11 @@ https-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" -https-proxy-agent@1: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6" - dependencies: - agent-base "2" - debug "2" - extend "3" - -iconv-lite@0.4.13: - version "0.4.13" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" - iconv-lite@0.4.15: version "0.4.15" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" -iconv-lite@^0.4.17, iconv-lite@^0.4.5, iconv-lite@~0.4.13: +iconv-lite@^0.4.17, iconv-lite@~0.4.13: version "0.4.18" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" @@ -4197,12 +3751,6 @@ is-absolute-url@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" -is-absolute@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.1.7.tgz#847491119fccb5fb436217cc737f7faad50f603f" - dependencies: - is-relative "^0.1.0" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -4223,7 +3771,7 @@ is-builtin-module@^1.0.0: dependencies: builtin-modules "^1.0.0" -is-callable@^1.1.1, is-callable@^1.1.2, is-callable@^1.1.3: +is-callable@^1.1.1, is-callable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" @@ -4309,7 +3857,7 @@ is-isodate@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/is-isodate/-/is-isodate-0.0.1.tgz#4fe2e937d50f3ba68c7b69b0218008b624aa5036" -is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: +is-my-json-valid@^2.10.0: version "2.16.0" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz#f079dd9bfdae65ee2038aae8acbc86ab109e3693" dependencies: @@ -4368,16 +3916,12 @@ is-redirect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" -is-regex@^1.0.3: +is-regex@^1.0.3, is-regex@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" dependencies: has "^1.0.1" -is-relative@^0.1.0: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.1.3.tgz#905fee8ae86f45b3ec614bc3c15c869df0876e82" - is-resolvable@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" @@ -4483,7 +4027,7 @@ istanbul-lib-hook@^1.0.5: dependencies: append-transform "^0.4.0" -istanbul-lib-instrument@^1.6.2, istanbul-lib-instrument@^1.7.0: +istanbul-lib-instrument@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.0.tgz#b8e0dc25709bb44e17336ab47b7bb5c97c23f659" dependencies: @@ -4630,30 +4174,6 @@ jsdom@^7.0.2: whatwg-url-compat "~0.6.5" xml-name-validator ">= 2.0.1 < 3.0.0" -jsdom@^9.8.3: - version "9.12.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.12.0.tgz#e8c546fffcb06c00d4833ca84410fed7f8a097d4" - dependencies: - abab "^1.0.3" - acorn "^4.0.4" - acorn-globals "^3.1.0" - array-equal "^1.0.0" - content-type-parser "^1.0.1" - cssom ">= 0.3.2 < 0.4.0" - cssstyle ">= 0.2.37 < 0.3.0" - escodegen "^1.6.1" - html-encoding-sniffer "^1.0.1" - nwmatcher ">= 1.3.9 < 2.0.0" - parse5 "^1.5.1" - request "^2.79.0" - sax "^1.2.1" - symbol-tree "^3.2.1" - tough-cookie "^2.3.2" - webidl-conversions "^4.0.0" - whatwg-encoding "^1.0.1" - whatwg-url "^4.3.0" - xml-name-validator "^2.0.1" - jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" @@ -4901,25 +4421,10 @@ loader-utils@^1.0.2: emojis-list "^2.0.0" json5 "^0.5.0" -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - lodash-es@^4.2.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" -lodash._arraycopy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" - -lodash._arrayeach@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz#bab156b2a90d3f1bbd5c653403349e5e5933ef9e" - lodash._baseassign@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" @@ -4927,21 +4432,6 @@ lodash._baseassign@^3.0.0: lodash._basecopy "^3.0.0" lodash.keys "^3.0.0" -lodash._baseclone@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz#303519bf6393fe7e42f34d8b630ef7794e3542b7" - dependencies: - lodash._arraycopy "^3.0.0" - lodash._arrayeach "^3.0.0" - lodash._baseassign "^3.0.0" - lodash._basefor "^3.0.0" - lodash.isarray "^3.0.0" - lodash.keys "^3.0.0" - -lodash._baseclone@^4.0.0: - version "4.5.7" - resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-4.5.7.tgz#ce42ade08384ef5d62fa77c30f61a46e686f8434" - lodash._basecopy@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" @@ -4950,10 +4440,6 @@ lodash._basecreate@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" -lodash._basefor@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2" - lodash._bindcallback@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" @@ -4974,10 +4460,6 @@ lodash._isiterateecall@^3.0.0: version "3.0.9" resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" -lodash._stack@^4.0.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/lodash._stack/-/lodash._stack-4.1.3.tgz#751aa76c1b964b047e76d14fc72a093fcb5e2dd0" - lodash.assign@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" @@ -5002,14 +4484,6 @@ lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" -lodash.clone@3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-3.0.3.tgz#84688c73d32b5a90ca25616963f189252a997043" - dependencies: - lodash._baseclone "^3.0.0" - lodash._bindcallback "^3.0.0" - lodash._isiterateecall "^3.0.0" - lodash.cond@^4.3.0: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" @@ -5033,17 +4507,6 @@ lodash.defaults@^4.0.1: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" -lodash.defaultsdeep@4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.3.2.tgz#6c1a586e6c5647b0e64e2d798141b8836158be8a" - dependencies: - lodash._baseclone "^4.0.0" - lodash._stack "^4.0.0" - lodash.isplainobject "^4.0.0" - lodash.keysin "^4.0.0" - lodash.mergewith "^4.0.0" - lodash.rest "^4.0.0" - lodash.filter@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" @@ -5072,10 +4535,6 @@ lodash.isobject@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" -lodash.isplainobject@^4.0.0: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - lodash.isstring@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" @@ -5088,10 +4547,6 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" -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.4.0, lodash.map@^4.5.1: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" @@ -5104,10 +4559,6 @@ lodash.merge@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5" -lodash.mergewith@^4.0.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55" - lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" @@ -5124,10 +4575,6 @@ lodash.reject@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415" -lodash.rest@^4.0.0: - version "4.0.5" - resolved "https://registry.yarnpkg.com/lodash.rest/-/lodash.rest-4.0.5.tgz#954ef75049262038c96d1fc98b28fdaf9f0772aa" - lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" @@ -5144,11 +4591,7 @@ 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@3.9.3: - version "3.9.3" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.9.3.tgz#0159e86832feffc6d61d852b12a953b99496bd32" - -lodash@^4.0.0, lodash@^4.1.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.2, 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" @@ -5170,7 +4613,7 @@ lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" -lru-cache@^2.5.0, lru-cache@~2.6.5: +lru-cache@^2.5.0: version "2.6.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5" @@ -5256,7 +4699,7 @@ methods@1.x, methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" -micromatch@^2.1.5, micromatch@^2.3.11: +micromatch@^2.1.5: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" dependencies: @@ -5317,7 +4760,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" -"minimatch@2 || 3", minimatch@3.0.3, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" dependencies: @@ -5327,30 +4770,16 @@ minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.0.tgz#cdf225e8898f840a258ded44fc91776770afdc93" - minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -mkdirp@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" - dependencies: - minimist "0.0.8" - mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: minimist "0.0.8" -mkpath@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/mkpath/-/mkpath-1.0.0.tgz#ebb3a977e7af1c683ae6fda12b545a6ba6c5853d" - mocha-junit-reporter@^1.12.1: version "1.13.0" resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.13.0.tgz#030db8c530b244667253b03861d4cd336f7e56c8" @@ -5360,22 +4789,6 @@ mocha-junit-reporter@^1.12.1: mkdirp "~0.5.1" xml "^1.0.0" -mocha-nightwatch@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/mocha-nightwatch/-/mocha-nightwatch-3.2.1.tgz#0e810f9c958d91bc3982c5948044a91436182c29" - dependencies: - browser-stdout "1.3.0" - commander "2.9.0" - debug "2.2.0" - diff "1.4.0" - escape-string-regexp "1.0.5" - glob "7.0.5" - growl "1.9.2" - json3 "3.3.2" - lodash.create "3.1.1" - mkdirp "0.5.1" - supports-color "3.1.2" - mocha@^3.1.2: version "3.3.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.3.0.tgz#d29b7428d3f52c82e2e65df1ecb7064e1aabbfb5" @@ -5530,31 +4943,12 @@ nested-error-stacks@^1.0.0: dependencies: inherits "~2.0.1" -netmask@~1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" - nib@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/nib/-/nib-1.1.2.tgz#6a69ede4081b95c0def8be024a4c8ae0c2cbb6c7" dependencies: stylus "0.54.5" -nightwatch@^0.9.11: - version "0.9.14" - resolved "https://registry.yarnpkg.com/nightwatch/-/nightwatch-0.9.14.tgz#897eb2e418b75492c3671e28e8e413abe17cc268" - dependencies: - chai-nightwatch "~0.1.x" - ejs "0.8.3" - lodash.clone "3.0.3" - lodash.defaultsdeep "4.3.2" - minimatch "3.0.3" - mkpath "1.0.0" - mocha-nightwatch "3.2.1" - optimist "0.6.1" - proxy-agent "2.0.0" - q "1.4.1" - no-case@^2.2.0: version "2.3.1" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081" @@ -5577,7 +4971,7 @@ node-emoji@^1.5.1: dependencies: string.prototype.codepointat "^0.2.0" -node-fetch@^1.0.1, node-fetch@^1.3.3, node-fetch@^1.6.3: +node-fetch@^1.0.1, node-fetch@^1.6.3: version "1.6.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.6.3.tgz#dc234edd6489982d58e8f0db4f695029abcd8c04" dependencies: @@ -5776,7 +5170,7 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -"nwmatcher@>= 1.3.7 < 2.0.0", "nwmatcher@>= 1.3.9 < 2.0.0": +"nwmatcher@>= 1.3.7 < 2.0.0": version "1.3.9" resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.3.9.tgz#8bab486ff7fa3dfd086656bbe8b17116d3692d2a" @@ -5808,7 +5202,7 @@ object.assign@^4.0.4: function-bind "^1.1.0" object-keys "^1.0.10" -object.entries@^1.0.3: +object.entries@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.0.4.tgz#1bf9a4dd2288f5b33f3a993d257661f05d161a5f" dependencies: @@ -5824,7 +5218,7 @@ object.omit@^2.0.0: for-own "^0.1.4" is-extendable "^0.1.1" -object.values@^1.0.3: +object.values@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a" dependencies: @@ -5916,40 +5310,6 @@ output-file-sync@^1.1.0: mkdirp "^0.5.1" object-assign "^4.1.0" -p-limit@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - dependencies: - p-limit "^1.1.0" - -pac-proxy-agent@1: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-1.0.0.tgz#dcd5b746581367430a236e88eacfd4e5b8d068a5" - dependencies: - agent-base "2" - debug "2" - extend "3" - get-uri "1" - http-proxy-agent "1" - https-proxy-agent "1" - pac-resolver "~1.2.1" - socks-proxy-agent "2" - stream-to-buffer "0.1.0" - -pac-resolver@~1.2.1: - version "1.2.6" - resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-1.2.6.tgz#ed03af0c5b5933505bdd3f07f75175466d5e7cfb" - dependencies: - co "~3.0.6" - degenerator "~1.0.0" - netmask "~1.0.4" - regenerator "~0.8.13" - thunkify "~2.1.1" - package-json@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/package-json/-/package-json-1.2.0.tgz#c8ecac094227cdf76a316874ed05e27cc939a0e0" @@ -6041,10 +5401,6 @@ path-exists@^2.0.0: dependencies: pinkie-promise "^2.0.0" -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -6061,12 +5417,6 @@ path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" -path-to-regexp@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" - dependencies: - isarray "0.0.1" - path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -6091,10 +5441,6 @@ pbkdf2@^3.0.3: dependencies: create-hmac "^1.1.2" -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" @@ -6637,7 +5983,7 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" -private@^0.1.6, private@~0.1.5: +private@^0.1.6: version "0.1.7" resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" @@ -6649,7 +5995,7 @@ process@^0.11.0: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" -progress@1.1.8, progress@^1.1.8: +progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" @@ -6683,19 +6029,6 @@ proxy-addr@~1.1.3: forwarded "~0.1.0" ipaddr.js "1.3.0" -proxy-agent@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-2.0.0.tgz#57eb5347aa805d74ec681cb25649dba39c933499" - dependencies: - agent-base "2" - debug "2" - extend "3" - http-proxy-agent "1" - https-proxy-agent "1" - lru-cache "~2.6.5" - pac-proxy-agent "1" - socks-proxy-agent "2" - prr@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" @@ -6829,10 +6162,6 @@ q@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/q/-/q-1.1.2.tgz#6357e291206701d99f197ab84e57e8ad196f2a89" -q@1.4.1, q@^1.1.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" - q@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/q/-/q-2.0.3.tgz#75b8db0255a1a5af82f58c3f3aaa1efec7d0d134" @@ -6841,14 +6170,14 @@ q@2.0.3: pop-iterate "^1.0.1" weak-map "^1.0.5" +q@^1.1.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" + qs@6.4.0, qs@^6.1.0, qs@^6.2.0, qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" -qs@~6.3.0: - version "6.3.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" - query-string@^4.1.0, query-string@^4.2.2: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -6882,10 +6211,6 @@ ramda@^0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.23.0.tgz#ccd13fff73497a93974e3e86327bfd87bd6e8e2b" -random-bytes@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" - randomatic@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" @@ -6918,13 +6243,6 @@ rc@^1.0.1, rc@~1.1.6: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-addons-test-utils@^15.4.2: - version "15.5.1" - resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.5.1.tgz#e0d258cda2a122ad0dff69f838260d0c3958f5f7" - dependencies: - fbjs "^0.8.4" - object-assign "^4.1.0" - react-apollo@^1.4.12: version "1.4.12" resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-1.4.12.tgz#0cacbdd335acec4c1079feb48047df1c43f77bdf" @@ -7073,7 +6391,7 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -readable-stream@1.1, readable-stream@1.1.x: +readable-stream@1.1: version "1.1.13" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e" dependencies: @@ -7082,7 +6400,7 @@ readable-stream@1.1, readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@2, readable-stream@2.2.7, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.2.6: +readable-stream@2.2.7, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.2.6: version "2.2.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.7.tgz#07057acbe2467b22042d36f98c5ad507054e95b1" dependencies: @@ -7130,24 +6448,6 @@ readline2@^1.0.1: is-fullwidth-code-point "^1.0.0" mute-stream "0.0.5" -recast@0.10.33: - version "0.10.33" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.33.tgz#942808f7aa016f1fa7142c461d7e5704aaa8d697" - dependencies: - ast-types "0.8.12" - esprima-fb "~15001.1001.0-dev-harmony-fb" - private "~0.1.5" - source-map "~0.5.0" - -recast@^0.11.17: - version "0.11.23" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" - dependencies: - ast-types "0.9.6" - esprima "~3.1.0" - private "~0.1.5" - source-map "~0.5.0" - rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -7175,7 +6475,7 @@ redis@^0.12.1: version "0.12.1" resolved "https://registry.yarnpkg.com/redis/-/redis-0.12.1.tgz#64df76ad0fc8acebaebd2a0645e8a48fac49185e" -redis@^2.1.0, redis@^2.6.3, redis@^2.7.1: +redis@^2.6.3, redis@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/redis/-/redis-2.7.1.tgz#7d56f7875b98b20410b71539f1d878ed58ebf46a" dependencies: @@ -7212,10 +6512,6 @@ reduce-function-call@^1.0.1: dependencies: balanced-match "^0.4.2" -redux-mock-store@^1.2.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.2.3.tgz#1b3ad299da91cb41ba30d68e3b6f024475fb9e1b" - redux-thunk@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5" @@ -7241,10 +6537,6 @@ regenerator-runtime@^0.10.0: version "0.10.4" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.4.tgz#74cb6598d3ba2eb18694e968a40e2b3b4df9cf93" -regenerator-runtime@~0.9.5: - version "0.9.6" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.9.6.tgz#d33eb95d0d2001a4be39659707c51b0cb71ce029" - regenerator-transform@0.9.11: version "0.9.11" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.11.tgz#3a7d067520cb7b7176769eb5ff868691befe1283" @@ -7253,18 +6545,6 @@ regenerator-transform@0.9.11: babel-types "^6.19.0" private "^0.1.6" -regenerator@^0.8.46, regenerator@~0.8.13: - version "0.8.46" - resolved "https://registry.yarnpkg.com/regenerator/-/regenerator-0.8.46.tgz#154c327686361ed52cad69b2545efc53a3d07696" - dependencies: - commoner "~0.10.3" - defs "~1.1.0" - esprima-fb "~15001.1001.0-dev-harmony-fb" - private "~0.1.5" - recast "0.10.33" - regenerator-runtime "~0.9.5" - through "~2.3.8" - regex-cache@^0.4.2: version "0.4.3" resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" @@ -7332,31 +6612,6 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request@2.79.0: - version "2.79.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.11.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~2.0.6" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - qs "~6.3.0" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "~0.4.1" - uuid "^3.0.0" - request@^2.55.0, request@^2.74.0, request@^2.79.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" @@ -7466,10 +6721,6 @@ ripemd160@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e" -rndm@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/rndm/-/rndm-1.2.0.tgz#f33fe9cfb52bbfd520aa18323bc65db110a1b76c" - run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" @@ -7508,7 +6759,7 @@ sax@0.5.x: version "0.5.8" resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1" -sax@^1.1.4, sax@^1.2.1, sax@~1.2.1: +sax@^1.1.4, sax@~1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828" @@ -7516,22 +6767,6 @@ select@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" -selenium-standalone@^5.11.2: - version "5.11.2" - resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-5.11.2.tgz#724ccaa72fb26f3711e0e20989e478c4133df844" - dependencies: - async "1.2.1" - commander "2.6.0" - lodash "3.9.3" - minimist "1.1.0" - mkdirp "0.5.0" - progress "1.1.8" - request "2.79.0" - tar-stream "1.5.2" - urijs "1.16.1" - which "1.1.1" - yauzl "^2.5.0" - semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -7550,10 +6785,6 @@ semver@5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.0.tgz#85f2cf8550465c4df000cf7d86f6b054106ab9e5" -semver@~5.0.1: - version "5.0.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" - send@0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/send/-/send-0.15.1.tgz#8a02354c26e6f5cca700065f5f0cdeba90ec7b5f" @@ -7593,10 +6824,6 @@ setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" -setprototypeof@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" - setprototypeof@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" @@ -7646,14 +6873,6 @@ simple-commit-message@2.2.1: semver "5.1.0" word-wrap "1.1.0" -simple-fmt@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/simple-fmt/-/simple-fmt-0.1.0.tgz#191bf566a59e6530482cb25ab53b4a8dc85c3a6b" - -simple-is@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/simple-is/-/simple-is-0.2.0.tgz#2abb75aade39deb5cc815ce10e6191164850baf0" - simplemde@^1.11.2: version "1.11.2" resolved "https://registry.yarnpkg.com/simplemde/-/simplemde-1.11.2.tgz#a23a35d978d2c40ef07dec008c92f070d8e080e3" @@ -7709,15 +6928,7 @@ sntp@1.x.x: dependencies: hoek "2.x.x" -socks-proxy-agent@2: - version "2.0.0" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-2.0.0.tgz#c674842d70410fb28ae1e92e6135a927854bc275" - dependencies: - agent-base "2" - extend "3" - socks "~1.1.5" - -socks@1.1.9, socks@~1.1.5: +socks@1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/socks/-/socks-1.1.9.tgz#628d7e4d04912435445ac0b6e459376cb3e6d691" dependencies: @@ -7756,7 +6967,7 @@ source-map@0.4.x, source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: +source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" @@ -7813,10 +7024,6 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" -stable@~0.1.3: - version "0.1.6" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.6.tgz#910f5d2aed7b520c6e777499c1f32e139fdecb10" - "statuses@>= 1.3.1 < 2", statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" @@ -7848,16 +7055,6 @@ stream-shift@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" -stream-to-buffer@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/stream-to-buffer/-/stream-to-buffer-0.1.0.tgz#26799d903ab2025c9bd550ac47171b00f8dd80a9" - dependencies: - stream-to "~0.2.0" - -stream-to@~0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/stream-to/-/stream-to-0.2.2.tgz#84306098d85fdb990b9fa300b1b3ccf55e8ef01d" - strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -7901,14 +7098,6 @@ string_decoder@~1.0.0: dependencies: buffer-shims "~1.0.0" -stringmap@~0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/stringmap/-/stringmap-0.2.2.tgz#556c137b258f942b8776f5b2ef582aa069d7d1b1" - -stringset@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/stringset/-/stringset-0.2.1.tgz#ef259c4e349344377fcd1c913dd2e848c9c042b5" - stringstream@~0.0.4: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -8047,7 +7236,7 @@ symbol-observable@^1.0.2, symbol-observable@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" -"symbol-tree@>= 3.1.0 < 4.0.0", symbol-tree@^3.2.1: +"symbol-tree@>= 3.1.0 < 4.0.0": version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" @@ -8079,15 +7268,6 @@ tar-pack@~3.3.0: tar "~2.2.1" uid-number "~0.0.6" -tar-stream@1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.2.tgz#fbc6c6e83c1a19d4cb48c7d96171fc248effc7bf" - dependencies: - bl "^1.0.0" - end-of-stream "^1.0.0" - readable-stream "^2.0.0" - xtend "^4.0.0" - tar@~2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" @@ -8100,21 +7280,11 @@ tcomb@^2.5.1: version "2.7.0" resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-2.7.0.tgz#10d62958041669a5d53567b9a4ee8cde22b1c2b0" -test-exclude@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.0.3.tgz#86a13ce3effcc60e6c90403cf31a27a60ac6c4e7" - dependencies: - arrify "^1.0.1" - micromatch "^2.3.11" - object-assign "^4.1.0" - read-pkg-up "^1.0.1" - require-main-filename "^1.0.1" - text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" -through@2, through@^2.3.6, through@~2.3, through@~2.3.1, through@~2.3.8: +through@2, through@^2.3.6, through@~2.3, through@~2.3.1: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -8124,10 +7294,6 @@ throwback@^1.1.0: dependencies: any-promise "^1.3.0" -thunkify@~2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/thunkify/-/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d" - timeago.js@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-2.0.5.tgz#730c74fbdb0b0917a553675a4460e3a7f80db86c" @@ -8225,13 +7391,13 @@ touch@1.0.0: dependencies: nopt "~1.0.10" -tough-cookie@^2.0.0, tough-cookie@^2.2.0, tough-cookie@^2.3.2, tough-cookie@~2.3.0: +tough-cookie@^2.0.0, tough-cookie@^2.2.0, tough-cookie@~2.3.0: version "2.3.2" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" dependencies: punycode "^1.4.1" -tr46@~0.0.1, tr46@~0.0.3: +tr46@~0.0.1: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -8243,14 +7409,6 @@ tryit@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" -tryor@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/tryor/-/tryor-0.1.2.tgz#8145e4ca7caff40acde3ccf946e8b8bb75b4172b" - -tsscmp@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.5.tgz#7dc4a33af71581ab4337da91d85ca5427ebd9a97" - tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -8261,10 +7419,6 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tunnel-agent@~0.4.1: - version "0.4.3" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" - tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -8323,12 +7477,6 @@ uid-number@~0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" -uid-safe@2.1.4, uid-safe@~2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.4.tgz#3ad6f38368c6d4c8c75ec17623fb79aa1d071d81" - dependencies: - random-bytes "~1.0.0" - ultron@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" @@ -8379,10 +7527,6 @@ update-notifier@0.5.0: semver-diff "^2.0.0" string-length "^1.0.0" -urijs@1.16.1: - version "1.16.1" - resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.16.1.tgz#859ad31890f5f9528727be89f1932c94fb4731e2" - url-loader@^0.5.9: version "0.5.9" resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.5.9.tgz#cc8fea82c7b906e7777019250869e569e995c295" @@ -8425,7 +7569,7 @@ utils-merge@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" -uuid@^2.0.1, uuid@^2.0.2, uuid@^2.0.3: +uuid@^2.0.1, uuid@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" @@ -8500,14 +7644,6 @@ webidl-conversions@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-2.0.1.tgz#3bf8258f7d318c7443c36f2e169402a1a6703506" -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - -webidl-conversions@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.1.tgz#8015a17ab83e7e1b311638486ace81da6ce206a0" - webpack-sources@^0.1.0: version "0.1.5" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.5.tgz#aa1f3abf0f0d74db7111c40e500b84f966640750" @@ -8555,12 +7691,6 @@ webworker-threads@>=0.6.2: bindings "^1.2.1" nan "^2.4.0" -whatwg-encoding@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz#3c6c451a198ee7aec55b1ec61d0920c67801a5f4" - dependencies: - iconv-lite "0.4.13" - whatwg-fetch@>=0.10.0, whatwg-fetch@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" @@ -8571,13 +7701,6 @@ whatwg-url-compat@~0.6.5: dependencies: tr46 "~0.0.1" -whatwg-url@^4.3.0: - version "4.7.1" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.7.1.tgz#df4dc2e3f25a63b1fa5b32ed6d6c139577d690de" - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - whet.extend@~0.9.9: version "0.9.9" resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" @@ -8586,12 +7709,6 @@ which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" -which@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.1.1.tgz#9ce512459946166e12c083f08ec073380fc8cbbb" - dependencies: - is-absolute "^0.1.7" - which@^1.1.1, which@^1.2.9: version "1.2.14" resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" @@ -8608,10 +7725,6 @@ window-size@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" -window-size@^0.1.2: - version "0.1.4" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" - window-size@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" @@ -8681,7 +7794,7 @@ xdg-basedir@^2.0.0: dependencies: os-homedir "^1.0.0" -"xml-name-validator@>= 2.0.1 < 3.0.0", xml-name-validator@^2.0.1: +"xml-name-validator@>= 2.0.1 < 3.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" @@ -8689,15 +7802,11 @@ xml@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" -xregexp@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" - xtend@^4.0.0, xtend@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" -y18n@^3.2.0, y18n@^3.2.1: +y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" @@ -8803,24 +7912,6 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" -yargs@~3.27.0: - version "3.27.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.27.0.tgz#21205469316e939131d59f2da0c6d7f98221ea40" - dependencies: - camelcase "^1.2.1" - cliui "^2.1.0" - decamelize "^1.0.0" - os-locale "^1.4.0" - window-size "^0.1.2" - y18n "^3.2.0" - -yauzl@^2.5.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.8.0.tgz#79450aff22b2a9c5a41ef54e02db907ccfbf9ee2" - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.0.1" - zen-observable-ts@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.4.0.tgz#a74bc9fe59747948a577bd513d438e70fcfae7e2" From d38dbc87b64445097325f80f9a4fd33905ee9137 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sat, 19 Aug 2017 10:22:26 -0600 Subject: [PATCH 007/121] simplified test layout --- test/helpers/index.test.html | 83 -------------------- test/{server => helpers}/kue.js | 0 test/helpers/mongoose.js | 16 ++-- test/helpers/redis.js | 21 +++-- test/mocha.opts | 2 - test/server/graph/context.js | 3 +- test/server/graph/loaders/metrics.js | 3 +- test/server/graph/mutations/addTag.js | 3 +- test/server/graph/mutations/createComment.js | 3 +- test/server/graph/mutations/editComment.js | 9 ++- test/server/graph/mutations/ignoreUser.js | 3 +- test/server/graph/mutations/removeTag.js | 3 +- test/server/graph/queries/asset.js | 3 +- test/server/mongoose.js | 15 ---- test/server/redis.js | 4 - test/server/routes/api/account/index.js | 5 +- test/server/routes/api/assets/index.js | 5 +- test/server/routes/api/auth/index.js | 5 +- test/server/routes/api/settings/index.js | 4 +- test/server/routes/api/user/index.js | 5 +- test/server/services/assets.js | 8 +- test/server/services/comments.js | 4 +- test/server/services/settings.js | 3 +- test/server/services/tags.js | 3 +- test/server/services/tokens.js | 5 +- test/server/services/users.js | 3 +- test/server/services/wordlist.js | 4 +- 27 files changed, 64 insertions(+), 161 deletions(-) delete mode 100644 test/helpers/index.test.html rename test/{server => helpers}/kue.js (100%) delete mode 100644 test/server/mongoose.js delete mode 100644 test/server/redis.js diff --git a/test/helpers/index.test.html b/test/helpers/index.test.html deleted file mode 100644 index 92ff1eb3a..000000000 --- a/test/helpers/index.test.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - Coral - (Beta) - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - diff --git a/test/server/kue.js b/test/helpers/kue.js similarity index 100% rename from test/server/kue.js rename to test/helpers/kue.js diff --git a/test/helpers/mongoose.js b/test/helpers/mongoose.js index 48cea6b3f..567aa8047 100644 --- a/test/helpers/mongoose.js +++ b/test/helpers/mongoose.js @@ -1,8 +1,8 @@ const mongoose = require('../../services/mongoose'); -module.exports = {}; +before(function(done) { + this.timeout(30000); -module.exports.waitTillConnect = function(done) { mongoose.connection.on('open', function(err) { if (err) { return done(err); @@ -10,9 +10,9 @@ module.exports.waitTillConnect = function(done) { return done(); }); -}; +}); -module.exports.clearDB = function(done) { +beforeEach(function(done) { Promise.all(Object.keys(mongoose.connection.collections).map((collection) => { return new Promise((resolve, reject) => { mongoose.connection.collections[collection].remove(function(err) { @@ -30,9 +30,9 @@ module.exports.clearDB = function(done) { .catch((err) => { done(err); }); -}; +}); -module.exports.disconnect = function(done) { +after(function(done) { mongoose.disconnect(); - return done(); -}; + done(); +}); diff --git a/test/helpers/redis.js b/test/helpers/redis.js index 02ab81b51..b013dfe25 100644 --- a/test/helpers/redis.js +++ b/test/helpers/redis.js @@ -1,15 +1,14 @@ const {createClient} = require('../../services/redis'); - -// Create a redis client to use for clearing the database. +const cache = require('../../services/cache'); const client = createClient(); -module.exports.clearDB = () => - new Promise((resolve, reject) => - client.flushdb((err) => { - if (err) { - return reject(err); - } +beforeEach(() => Promise.all([ + new Promise((resolve, reject) => client.flushdb((err) => { + if (err) { + return reject(err); + } - return resolve(); - }) - ); + return resolve(); + })), + cache.init(), +])); diff --git a/test/mocha.opts b/test/mocha.opts index 36be84461..8753f4318 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,6 +1,4 @@ test/helpers/*.js - -test/e2e test/server --compilers js:babel-core/register --require ignore-styles diff --git a/test/server/graph/context.js b/test/server/graph/context.js index 7d30042e4..a12d46a67 100644 --- a/test/server/graph/context.js +++ b/test/server/graph/context.js @@ -1,10 +1,11 @@ -const expect = require('chai').expect; const User = require('../../../models/user'); const Context = require('../../../graph/context'); const errors = require('../../../errors'); const SettingsService = require('../../../services/settings'); +const {expect} = require('chai'); + describe('graph.Context', () => { beforeEach(() => SettingsService.init()); diff --git a/test/server/graph/loaders/metrics.js b/test/server/graph/loaders/metrics.js index 3cc5c4565..e4036e867 100644 --- a/test/server/graph/loaders/metrics.js +++ b/test/server/graph/loaders/metrics.js @@ -1,4 +1,3 @@ -const {expect} = require('chai'); const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); @@ -9,6 +8,8 @@ const SettingsService = require('../../../../services/settings'); const ActionModel = require('../../../../models/action'); const CommentModel = require('../../../../models/comment'); +const {expect} = require('chai'); + describe('graph.loaders.Metrics', () => { beforeEach(() => SettingsService.init()); diff --git a/test/server/graph/mutations/addTag.js b/test/server/graph/mutations/addTag.js index 38a96ad1f..52f20b065 100644 --- a/test/server/graph/mutations/addTag.js +++ b/test/server/graph/mutations/addTag.js @@ -1,4 +1,3 @@ -const expect = require('chai').expect; const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); @@ -8,6 +7,8 @@ const UserModel = require('../../../../models/user'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); +const {expect} = require('chai'); + describe('graph.mutations.addTag', () => { let comment, asset; beforeEach(async () => { diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index e32943720..03d35416c 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -1,4 +1,3 @@ -const expect = require('chai').expect; const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); @@ -11,6 +10,8 @@ const ActionModel = require('../../../../models/action'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); +const {expect} = require('chai'); + describe('graph.mutations.createComment', () => { beforeEach(() => SettingsService.init()); diff --git a/test/server/graph/mutations/editComment.js b/test/server/graph/mutations/editComment.js index 3be892f90..4a6b7f1dd 100644 --- a/test/server/graph/mutations/editComment.js +++ b/test/server/graph/mutations/editComment.js @@ -1,4 +1,3 @@ -const expect = require('chai').expect; const {graphql} = require('graphql'); const timekeeper = require('timekeeper'); @@ -9,6 +8,8 @@ const AssetModel = require('../../../../models/asset'); const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); +const {expect} = require('chai'); + describe('graph.mutations.editComment', () => { let asset; let user; @@ -133,7 +134,7 @@ describe('graph.mutations.editComment', () => { expect(response.errors).to.be.empty; expect(response.data.editComment.errors).to.not.be.empty; expect(response.data.editComment.errors[0].translation_key).to.equal('NOT_AUTHORIZED'); - const commentAfterEdit = await CommentsService.findById(comment.id); + const commentAfterEdit = await CommentsService.findById(comment.id); // it *hasn't* changed from the original expect(commentAfterEdit.body).to.equal(comment.body); @@ -256,7 +257,7 @@ describe('graph.mutations.editComment', () => { expect(response.errors).to.be.empty; const commentAfterEdit = await CommentsService.findById(comment.id); expect(commentAfterEdit.body).to.equal(newBody); - expect(commentAfterEdit.status).to.equal(afterEdit.status); + expect(commentAfterEdit.status).to.equal(afterEdit.status); }); - }); + }); }); diff --git a/test/server/graph/mutations/ignoreUser.js b/test/server/graph/mutations/ignoreUser.js index 256fd732b..3d9db2d00 100644 --- a/test/server/graph/mutations/ignoreUser.js +++ b/test/server/graph/mutations/ignoreUser.js @@ -1,4 +1,3 @@ -const expect = require('chai').expect; const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); @@ -6,6 +5,8 @@ const Context = require('../../../../graph/context'); const UsersService = require('../../../../services/users'); const SettingsService = require('../../../../services/settings'); +const {expect} = require('chai'); + const ignoreUserMutation = ` mutation ignoreUser ($id: ID!) { ignoreUser(id:$id) { diff --git a/test/server/graph/mutations/removeTag.js b/test/server/graph/mutations/removeTag.js index a1521c2ce..4cb4f1e6a 100644 --- a/test/server/graph/mutations/removeTag.js +++ b/test/server/graph/mutations/removeTag.js @@ -1,4 +1,3 @@ -const expect = require('chai').expect; const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); @@ -11,6 +10,8 @@ const SettingsService = require('../../../../services/settings'); const CommentsService = require('../../../../services/comments'); const TagsService = require('../../../../services/tags'); +const {expect} = require('chai'); + describe('graph.mutations.removeTag', () => { let asset, comment; beforeEach(async () => { diff --git a/test/server/graph/queries/asset.js b/test/server/graph/queries/asset.js index a11155c47..0b837a181 100644 --- a/test/server/graph/queries/asset.js +++ b/test/server/graph/queries/asset.js @@ -1,4 +1,3 @@ -const expect = require('chai').expect; const {graphql} = require('graphql'); const schema = require('../../../../graph/schema'); @@ -8,6 +7,8 @@ const SettingsService = require('../../../../services/settings'); const Asset = require('../../../../models/asset'); const CommentsService = require('../../../../services/comments'); +const {expect} = require('chai'); + describe('graph.queries.asset', () => { let asset, users; beforeEach(async () => { diff --git a/test/server/mongoose.js b/test/server/mongoose.js deleted file mode 100644 index a1bac6150..000000000 --- a/test/server/mongoose.js +++ /dev/null @@ -1,15 +0,0 @@ -const mongoose = require('../helpers/mongoose'); - -before(function(done) { - this.timeout(30000); - - mongoose.waitTillConnect(done); -}); - -beforeEach(function(done) { - mongoose.clearDB(done); -}); - -after(function(done) { - mongoose.disconnect(done); -}); diff --git a/test/server/redis.js b/test/server/redis.js deleted file mode 100644 index 454a2afd8..000000000 --- a/test/server/redis.js +++ /dev/null @@ -1,4 +0,0 @@ -const redis = require('../helpers/redis'); -const cache = require('../../services/cache'); - -beforeEach(() => Promise.all([redis.clearDB(), cache.init()])); diff --git a/test/server/routes/api/account/index.js b/test/server/routes/api/account/index.js index 8d4e0057a..a501b4798 100644 --- a/test/server/routes/api/account/index.js +++ b/test/server/routes/api/account/index.js @@ -1,15 +1,14 @@ const passport = require('../../../passport'); const app = require('../../../../../app'); -const chai = require('chai'); -const expect = chai.expect; const SettingsService = require('../../../../../services/settings'); const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}}; -// Setup chai. +const chai = require('chai'); chai.should(); chai.use(require('chai-http')); +const expect = chai.expect; const UsersService = require('../../../../../services/users'); diff --git a/test/server/routes/api/assets/index.js b/test/server/routes/api/assets/index.js index 7fd6e68d1..daccd5511 100644 --- a/test/server/routes/api/assets/index.js +++ b/test/server/routes/api/assets/index.js @@ -1,12 +1,11 @@ const passport = require('../../../passport'); const app = require('../../../../../app'); -const chai = require('chai'); -const expect = chai.expect; -// Setup chai. +const chai = require('chai'); chai.should(); chai.use(require('chai-http')); +const expect = chai.expect; const AssetModel = require('../../../../../models/asset'); const AssetsService = require('../../../../../services/assets'); diff --git a/test/server/routes/api/auth/index.js b/test/server/routes/api/auth/index.js index 1d0ed6b3a..f90b2d646 100644 --- a/test/server/routes/api/auth/index.js +++ b/test/server/routes/api/auth/index.js @@ -1,8 +1,9 @@ const app = require('../../../../../app'); -const chai = require('chai'); -const expect = chai.expect; +const chai = require('chai'); +chai.should(); chai.use(require('chai-http')); +const expect = chai.expect; const UsersService = require('../../../../../services/users'); diff --git a/test/server/routes/api/settings/index.js b/test/server/routes/api/settings/index.js index 54a8f357f..c7704fc7e 100644 --- a/test/server/routes/api/settings/index.js +++ b/test/server/routes/api/settings/index.js @@ -1,11 +1,11 @@ const passport = require('../../../passport'); const app = require('../../../../../app'); -const chai = require('chai'); -const expect = chai.expect; +const chai = require('chai'); chai.should(); chai.use(require('chai-http')); +const expect = chai.expect; const SettingsService = require('../../../../../services/settings'); const defaults = {id: '1', moderation: 'PRE'}; diff --git a/test/server/routes/api/user/index.js b/test/server/routes/api/user/index.js index fc1feb66e..28ab049f9 100644 --- a/test/server/routes/api/user/index.js +++ b/test/server/routes/api/user/index.js @@ -2,15 +2,14 @@ const passport = require('../../../passport'); const app = require('../../../../../app'); const mailer = require('../../../../../services/mailer'); -const chai = require('chai'); -const expect = chai.expect; const SettingsService = require('../../../../../services/settings'); const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}}; -// Setup chai. +const chai = require('chai'); chai.should(); chai.use(require('chai-http')); +const expect = chai.expect; const UsersService = require('../../../../../services/users'); diff --git a/test/server/services/assets.js b/test/server/services/assets.js index 748e177e0..6ab33321b 100644 --- a/test/server/services/assets.js +++ b/test/server/services/assets.js @@ -6,14 +6,12 @@ const SettingsService = require('../../../services/settings'); const url = require('url'); const chai = require('chai'); -const expect = chai.expect; -const chaiAsPromised = require('chai-as-promised'); -chai.use(chaiAsPromised); - -// Use the chai should. +chai.use(require('chai-as-promised')); chai.should(); +const expect = chai.expect; + const settings = {id: '1', moderation: 'PRE', domains: {whitelist: ['new.test.com', 'test.com', 'override.test.com']}}; const defaults = {url:'http://test.com'}; diff --git a/test/server/services/comments.js b/test/server/services/comments.js index 36e77b3ea..6aab7dad4 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -8,7 +8,9 @@ const CommentsService = require('../../../services/comments'); const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}}; -const expect = require('chai').use(require('chai-as-promised')).expect; +const chai = require('chai'); +chai.use(require('chai-as-promised')); +const expect = chai.expect; describe('services.CommentsService', () => { const comments = [{ diff --git a/test/server/services/settings.js b/test/server/services/settings.js index d460ae272..82d28ea41 100644 --- a/test/server/services/settings.js +++ b/test/server/services/settings.js @@ -1,5 +1,6 @@ const SettingsService = require('../../../services/settings'); -const expect = require('chai').expect; +const chai = require('chai'); +const expect = chai.expect; describe('services.SettingsService', () => { diff --git a/test/server/services/tags.js b/test/server/services/tags.js index 1080563f0..69630ec82 100644 --- a/test/server/services/tags.js +++ b/test/server/services/tags.js @@ -5,7 +5,8 @@ const SettingsService = require('../../../services/settings'); const CommentModel = require('../../../models/comment'); -const expect = require('chai').use(require('chai-as-promised')).expect; +const chai = require('chai'); +const expect = chai.expect; describe('services.TagsService', () => { let comment, user; diff --git a/test/server/services/tokens.js b/test/server/services/tokens.js index 2aa1f291d..a89d01bef 100644 --- a/test/server/services/tokens.js +++ b/test/server/services/tokens.js @@ -3,10 +3,7 @@ const UsersService = require('../../../services/users'); const SettingsService = require('../../../services/settings'); const chai = require('chai'); -const chaiAsPromised = require('chai-as-promised'); - -chai.use(chaiAsPromised); - +chai.use(require('chai-as-promised')); const expect = chai.expect; describe('services.TokensService', () => { diff --git a/test/server/services/users.js b/test/server/services/users.js index 53f4bc018..7340cd9cb 100644 --- a/test/server/services/users.js +++ b/test/server/services/users.js @@ -1,7 +1,8 @@ const UsersService = require('../../../services/users'); const SettingsService = require('../../../services/settings'); -const expect = require('chai').expect; +const chai = require('chai'); +const expect = chai.expect; describe('services.UsersService', () => { diff --git a/test/server/services/wordlist.js b/test/server/services/wordlist.js index 417844da4..7b196a299 100644 --- a/test/server/services/wordlist.js +++ b/test/server/services/wordlist.js @@ -1,8 +1,10 @@ -const expect = require('chai').expect; const Errors = require('../../../errors'); const Wordlist = require('../../../services/wordlist'); const SettingsService = require('../../../services/settings'); +const chai = require('chai'); +const expect = chai.expect; + describe('services.Wordlist', () => { const wordlists = { From 3804a7afae0b286c0d894a1c1f5ed9867dfd0a42 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sat, 19 Aug 2017 10:34:52 -0600 Subject: [PATCH 008/121] removed more outdated test code --- .dockerignore | 1 + .eslintignore | 1 - .gitignore | 1 - circle.yml | 2 -- docs/_docs/01-03-install-source.md | 1 - nightwatch.conf.js | 54 ------------------------------ package.json | 1 - test/mocha.opts | 2 -- yarn.lock | 4 --- 9 files changed, 1 insertion(+), 66 deletions(-) delete mode 100644 nightwatch.conf.js diff --git a/.dockerignore b/.dockerignore index 80bb20e2c..44d0bd178 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,6 +14,7 @@ dist # tests are not run in the docker container. test +__tests__ # we won't use the .git folder in production. .git diff --git a/.eslintignore b/.eslintignore index cd0b1a983..2702e0a4d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,5 @@ dist docs -client/lib **/*.html plugins/* !plugins/talk-plugin-facebook-auth diff --git a/.gitignore b/.gitignore index f5482fd36..882c24883 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ client/coral-framework/graphql/introspection.json *.swp *.DS_STORE -test/e2e/reports coverage/ plugins.json diff --git a/circle.yml b/circle.yml index 373b45012..f6b35970c 100644 --- a/circle.yml +++ b/circle.yml @@ -40,8 +40,6 @@ test: override: # Run the tests using the junit reporter. - MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test - # Run the e2e test suite. - # - E2E_REPORT_PATH=$CIRCLE_TEST_REPORTS/e2e yarn e2e deployment: release: diff --git a/docs/_docs/01-03-install-source.md b/docs/_docs/01-03-install-source.md index 66dc94db6..da6fa120d 100644 --- a/docs/_docs/01-03-install-source.md +++ b/docs/_docs/01-03-install-source.md @@ -72,7 +72,6 @@ You can see other scripts we've made available by consulting the `package.json` file under the `scripts` key including: - `yarn test` run unit tests -- `yarn e2e` run end to end tests - `yarn build-watch` watch for changes to client files and build static assets - `yarn dev-start` watch for changes to server files and reload the server while also sourcing a `.env` file in your local directory for configuration diff --git a/nightwatch.conf.js b/nightwatch.conf.js deleted file mode 100644 index b2a2b9baa..000000000 --- a/nightwatch.conf.js +++ /dev/null @@ -1,54 +0,0 @@ -require('babel-core/register'); - -let E2E_REPORT_PATH = './test/e2e/reports'; -if (process.env.E2E_REPORT_PATH && process.env.E2E_REPORT_PATH.length > 0) { - E2E_REPORT_PATH = process.env.E2E_REPORT_PATH; -} - -module.exports = { - 'src_folders': './test/e2e/tests', - 'output_folder': E2E_REPORT_PATH, - 'page_objects_path': './test/e2e/pages', - 'globals_path': './test/e2e/globals', - 'custom_commands_path' : '', - 'custom_assertions_path' : '', - 'selenium': { - 'start_process': true, - 'server_path': 'node_modules/selenium-standalone/.selenium/selenium-server/2.53.1-server.jar', - 'log_path': E2E_REPORT_PATH, - 'host': '127.0.0.1', - 'port': 6666, - 'cli_args': { - 'webdriver.chrome.driver': 'node_modules/selenium-standalone/.selenium/chromedriver/2.25-x64-chromedriver' - } - }, - 'test_settings': { - 'default': { - 'launch_url' : 'http://localhost:3011', - 'selenium_port': 6666, - 'selenium_host': 'localhost', - 'silent': true, - 'desiredCapabilities': { - 'browserName': 'chrome', - 'javascriptEnabled': true, - 'acceptSslCerts': true, - 'webStorageEnabled': true, - 'databaseEnabled': true, - 'applicationCacheEnabled': false, - 'nativeEvents': true - }, - 'screenshots' : { - 'enabled': true, - 'on_failure': true, - 'on_error': true, - 'path': E2E_REPORT_PATH - }, - 'exclude': [ - './tests/e2e/tests/EmbedStreamTests.js' - ] - }, - 'integration': { - 'launch_url': 'http://localhost:3011' - } - } -}; diff --git a/package.json b/package.json index 9801c6ead..29a8bdcaa 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,6 @@ "hammerjs": "^2.0.8", "helmet": "^3.5.0", "history": "^3.0.0", - "ignore-styles": "^5.0.1", "immutability-helper": "^2.2.0", "imports-loader": "^0.7.1", "inquirer": "^3.2.1", diff --git a/test/mocha.opts b/test/mocha.opts index 8753f4318..2b6128133 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,7 +1,5 @@ test/helpers/*.js test/server ---compilers js:babel-core/register ---require ignore-styles --recursive --colors --sort diff --git a/yarn.lock b/yarn.lock index eb513b370..0b4d2f04a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3591,10 +3591,6 @@ ignore-by-default@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" -ignore-styles@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ignore-styles/-/ignore-styles-5.0.1.tgz#b49ef2274bdafcd8a4880a966bfe38d1a0bf4671" - ignore@^3.2.0: version "3.2.7" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.7.tgz#4810ca5f1d8eca5595213a34b94f2eb4ed926bbd" From 77b7ddf337d37e5b4addd8e9a4529cd8e160df39 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sat, 19 Aug 2017 10:40:06 -0600 Subject: [PATCH 009/121] updated dockerfile --- Dockerfile | 6 +++--- Dockerfile.onbuild | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2dbd7bbbb..092cbf57f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,9 @@ EXPOSE 5000 # Bundle app source COPY . /usr/src/app +# Ensure the runtime of the container is in production mode. +ENV NODE_ENV production + # Install app dependencies and build static assets. RUN yarn global add node-gyp && \ yarn install --frozen-lockfile && \ @@ -19,7 +22,4 @@ RUN yarn global add node-gyp && \ yarn build && \ yarn cache clean -# Ensure the runtime of the container is in production mode. -ENV NODE_ENV production - CMD ["yarn", "start"] diff --git a/Dockerfile.onbuild b/Dockerfile.onbuild index 06d006b42..b9cf8ce87 100644 --- a/Dockerfile.onbuild +++ b/Dockerfile.onbuild @@ -7,8 +7,5 @@ ONBUILD COPY . /usr/src/app # we need to have webpack available. We then build the new dependancies and # clear out the development dependancies again. After this we of course need to # clear out the yarn cache, this saves quite a lot of size. -ONBUILD RUN NODE_ENV=development yarn install --frozen-lockfile && \ - NODE_ENV=production cli plugins reconcile && \ - NODE_ENV=production yarn build && \ - NODE_ENV=production yarn install --production --force && \ - yarn cache clean \ No newline at end of file +ONBUILD RUN cli plugins reconcile && \ + yarn build From b2e871f4a66fc02be63320800c9ff9975188d0a9 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sat, 19 Aug 2017 10:49:50 -0600 Subject: [PATCH 010/121] added event for edit to manage reply count --- models/comment.js | 8 ++ package.json | 2 + services/comments.js | 180 +++++++++++++++++++------------ services/events/constants.js | 2 + test/server/services/comments.js | 74 +++++-------- yarn.lock | 71 ++++++++++++ 6 files changed, 218 insertions(+), 119 deletions(-) diff --git a/models/comment.js b/models/comment.js index 972f37e46..e30cd091f 100644 --- a/models/comment.js +++ b/models/comment.js @@ -70,6 +70,9 @@ const CommentSchema = new Schema({ // parent_id is the id of the parent comment (null if there is none). parent_id: String, + // The number of replies to this comment directly. + reply_count: Number, + // Counts to store related to actions taken on the given comment. action_counts: { default: {}, @@ -123,6 +126,11 @@ CommentSchema.virtual('edited').get(function() { return this.body_history.length > 1; }); +// Visable is true when the comment is visible to the public. +CommentSchema.virtual('visible').get(function() { + return ['ACCEPTED', 'NONE'].includes(this.status); +}); + // Comment model. const Comment = mongoose.model('Comment', CommentSchema); diff --git a/package.json b/package.json index 29a8bdcaa..6e57aa221 100644 --- a/package.json +++ b/package.json @@ -198,6 +198,8 @@ "mocha-junit-reporter": "^1.12.1", "nodemon": "^1.11.0", "pre-git": "^3.10.0", + "sinon": "^3.2.1", + "sinon-chai": "^2.13.0", "supertest": "^2.0.1" }, "engines": { diff --git a/services/comments.js b/services/comments.js index 8db14966b..9f3bce0ea 100644 --- a/services/comments.js +++ b/services/comments.js @@ -10,6 +10,8 @@ const events = require('./events'); const { ACTIONS_NEW, ACTIONS_DELETE, + COMMENTS_NEW, + COMMENTS_EDIT, } = require('./events/constants'); module.exports = class CommentsService { @@ -19,7 +21,7 @@ module.exports = class CommentsService { * @param {Mixed} comment either a single comment or an array of comments. * @return {Promise} */ - static publicCreate(comment) { + static async publicCreate(comment) { // Check to see if this is an array of comments, if so map it out. if (Array.isArray(comment)) { @@ -41,7 +43,12 @@ module.exports = class CommentsService { }] }, comment)); - return commentModel.save(); + const savedCommentModel = await commentModel.save(); + + // Emit that the comment was created! + events.emitAsync(COMMENTS_NEW, savedCommentModel); + + return savedCommentModel; } /** @@ -54,7 +61,10 @@ module.exports = class CommentsService { static async edit(id, author_id, {body, status, ignoreEditWindow = false}) { const query = { id, - author_id + author_id, + status: { + $in: ['NONE', 'PREMOD'], + }, }; // Establish the edit window (if it exists) and add the condition to the @@ -68,9 +78,7 @@ module.exports = class CommentsService { }; } - const { - value: comment - } = await CommentModel.findOneAndUpdate(query, { + const originalComment = await CommentModel.findOneAndUpdate(query, { $set: { body, status, @@ -85,12 +93,9 @@ module.exports = class CommentsService { created_at: new Date(), } }, - }, { - new: true, - rawResult: true }); - if (comment === null) { + if (originalComment === null) { // Try to get the comment. const comment = await CommentsService.findById(id); @@ -103,6 +108,11 @@ module.exports = class CommentsService { throw errors.ErrNotAuthorized; } + // Check to see if the comment had a status that was editable. + if (!['NONE', 'PREMOD'].includes(comment.status)) { + throw errors.ErrNotAuthorized; + } + // Check to see if the edit window expired. if (!ignoreEditWindow && comment.created_at <= lastEditableCommentCreatedAt) { throw errors.ErrEditWindowHasEnded; @@ -111,7 +121,22 @@ module.exports = class CommentsService { throw new Error('comment edit failed for an unexpected reason'); } - return comment; + // Mutate the comment like Mongo would have. + const editedComment = originalComment; + editedComment.status = status; + editedComment.body = body; + editedComment.body_history.push({ + body, + created_at: new Date(), + }); + editedComment.status_history.push({ + type: status, + created_at: new Date(), + }); + + events.emitAsync(COMMENTS_EDIT, originalComment, editedComment); + + return editedComment; } /** @@ -211,21 +236,36 @@ module.exports = class CommentsService { * moderation action * @return {Promise} */ - static pushStatus(id, status, assigned_by = null) { - return CommentModel.findOneAndUpdate({id}, { + static async pushStatus(id, status, assigned_by = null) { + const created_at = new Date(); + const originalComment = await CommentModel.findOneAndUpdate({id}, { $push: { status_history: { type: status, - created_at: new Date(), - assigned_by + created_at, + assigned_by, } }, $set: {status} - }, { - - // return modified comment. - new: true, }); + + if (originalComment === null) { + throw errors.ErrNotFound; + } + + const editedComment = new CommentModel(originalComment.toObject()); + editedComment.status_history.push({ + type: status, + created_at, + assigned_by, + }); + editedComment.status = status; + + // Emit that the comment was edited, and pass the original comment and the + // edited comment. + events.emitAsync(COMMENTS_EDIT, originalComment, editedComment); + + return editedComment; } /** @@ -244,57 +284,6 @@ module.exports = class CommentsService { metadata }); } - - /** - * Change the status of a comment. - * @param {String} id identifier of the comment (uuid) - * @param {String} status the new status of the comment - * @return {Promise} - */ - static removeById(id) { - return CommentModel.remove({id}); - } - - /** - * Remove an action from the comment. - * @param {String} id identifier of the comment (uuid) - * @param {String} action_type the type of the action to be removed - * @param {String} user_id the id of the user performing the action - * @return {Promise} - */ - static removeAction(item_id, user_id, action_type) { - return ActionModel.remove({ - action_type, - item_type: 'COMMENTS', - item_id, - user_id - }); - } - - /** - * Returns all the comments in the collection. - * @return {Promise} - */ - static all() { - return CommentModel.find({}); - } - - /** - * Returns all the comments by user - * probably to be paginated at some point in the future - * @return {Promise} array resolves to an array of comments by that user - */ - static findByUserId(author_id, admin = false) { - - // do not return un-published comments for non-admins - let query = {author_id}; - - if (!admin) { - query.$nor = [{status: 'PREMOD'}, {status: 'REJECTED'}]; - } - - return CommentModel.find(query); - } }; //============================================================================== @@ -334,7 +323,7 @@ events.on(ACTIONS_NEW, async (action) => { return incrActionCounts(action, 1); }); -// When an action is deleted, modify the comment. +// When an action is deleted, remove the action count on the comment. events.on(ACTIONS_DELETE, async (action) => { if (!action || action.item_type !== 'COMMENTS') { return; @@ -342,3 +331,52 @@ events.on(ACTIONS_DELETE, async (action) => { return incrActionCounts(action, -1); }); + +const incrReplyCount = async (comment, value) => { + try { + await CommentModel.update({ + id: comment.parent_id, + }, { + $inc: { + reply_count: value, + }, + }); + } catch (err) { + console.error('Can\'t mutate the reply count:', err); + } +}; + +// When a comment is created, if it is a reply, increment the reply count on the +// parent's document. +events.on(COMMENTS_NEW, async (comment) => { + if ( + !comment || // Check that the comment is defined. + (comment.parent_id === null || comment.parent_id.length === 0) || // Check that the comment has a parent (is a reply). + !(comment.status === 'NONE' || comment.status === 'APPROVED') // Check that the comment is visible. + ) { + return; + } + + return incrReplyCount(comment, 1); +}); + +// When a comment is edited, if the visability changed publicly, then modify the +// comment. +events.on(COMMENTS_EDIT, async (originalComment, editedComment) => { + if ( + !editedComment || // Check that the comment is defined. + (editedComment.parent_id === null || editedComment.parent_id.length === 0) // Check that the comment has a parent (is a reply). + ) { + return; + } + + // If the comment was visible before, and now it isn't, decrement the count; + if (originalComment.visible && !editedComment.visible) { + return incrReplyCount(editedComment, -1); + } + + // If the comment was not visible before, and now it is, increment the count. + if (!originalComment.visible && editedComment.visible) { + return incrReplyCount(editedComment, 1); + } +}); diff --git a/services/events/constants.js b/services/events/constants.js index bc6b962b9..a1da8dce6 100644 --- a/services/events/constants.js +++ b/services/events/constants.js @@ -1,2 +1,4 @@ module.exports.ACTIONS_DELETE = 'actions.delete'; module.exports.ACTIONS_NEW = 'actions.new'; +module.exports.COMMENTS_NEW = 'comments.new'; +module.exports.COMMENTS_EDIT = 'comments.edit'; diff --git a/test/server/services/comments.js b/test/server/services/comments.js index 6aab7dad4..8be3e3f00 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -1,7 +1,8 @@ const CommentModel = require('../../../models/comment'); const ActionModel = require('../../../models/action'); -const ActionsService = require('../../../services/actions'); +const events = require('../../../services/events'); +const {COMMENTS_EDIT} = require('../../../services/events/constants'); const UsersService = require('../../../services/users'); const SettingsService = require('../../../services/settings'); const CommentsService = require('../../../services/comments'); @@ -10,8 +11,11 @@ const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], const chai = require('chai'); chai.use(require('chai-as-promised')); +chai.use(require('sinon-chai')); const expect = chai.expect; +const sinon = require('sinon'); + describe('services.CommentsService', () => { const comments = [{ body: 'comment 10', @@ -188,58 +192,32 @@ describe('services.CommentsService', () => { }); - describe('#removeAction', () => { - - it('should remove an action', () => { - return CommentsService - .removeAction('3', '123', 'flag') - .then(() => { - return ActionsService.findByItemIdArray(['123']); - }) - .then((actions) => { - expect(actions.length).to.equal(0); - }); - }); - }); - - describe('#findByUserId', () => { - it('should return all comments if admin', () => { - return CommentsService - .findByUserId('456', true) - .then((comments) => { - expect(comments).to.have.length(4); - }); - }); - - it('should not return premod and rejected comments if not admin', () => { - return CommentsService - .findByUserId('456') - .then((comments) => { - expect(comments).to.have.length(1); - }); - }); - - }); - describe('#changeStatus', () => { - it('should change the status of a comment from no status', () => { + it('should change the status of a comment from no status', async () => { let comment_id = comments[0].id; - return CommentsService.findById(comment_id) - .then((c) => { - expect(c.status).to.be.equal('NONE'); + let c = await CommentsService.findById(comment_id); + expect(c.status).to.be.equal('NONE'); - return CommentsService.pushStatus(comment_id, 'REJECTED', '123'); - }) - .then(() => CommentsService.findById(comment_id)) - .then((c) => { - expect(c).to.have.property('status'); - expect(c.status).to.equal('REJECTED'); - expect(c.status_history).to.have.length(1); - expect(c.status_history[0]).to.have.property('type', 'REJECTED'); - expect(c.status_history[0]).to.have.property('assigned_by', '123'); - }); + const spy = sinon.spy(); + events.once(COMMENTS_EDIT, () => spy()); + + let c2 = await CommentsService.pushStatus(comment_id, 'REJECTED', '123'); + expect(c2).to.have.property('status'); + expect(c2.status).to.equal('REJECTED'); + expect(c2.status_history).to.have.length(1); + expect(c2.status_history[0]).to.have.property('type', 'REJECTED'); + expect(c2.status_history[0]).to.have.property('assigned_by', '123'); + + expect(spy).to.have.been.called; + + let c3 = await CommentsService.findById(comment_id); + expect(c3).to.have.property('status'); + expect(c3.status).to.equal('REJECTED'); + expect(c3.status_history).to.have.length(1); + expect(c3.status_history[0]).to.have.property('type', 'REJECTED'); + expect(c3.status_history[0]).to.have.property('assigned_by', '123'); }); it('should change the status of a comment from accepted', () => { diff --git a/yarn.lock b/yarn.lock index 0b4d2f04a..0420dc269 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2143,6 +2143,10 @@ diff@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" +diff@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9" + diffie-hellman@^5.0.0: version "5.0.2" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" @@ -2918,6 +2922,12 @@ form-data@^2.1.2, form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +formatio@1.2.0, formatio@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb" + dependencies: + samsam "1.x" + formidable@^1.0.17: version "1.1.1" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9" @@ -4260,6 +4270,10 @@ jsx-ast-utils@^1.3.4: version "1.4.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1" +just-extend@^1.1.22: + version "1.1.22" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.22.tgz#3330af756cab6a542700c64b2e4e4aa062d52fff" + jwa@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" @@ -4591,6 +4605,14 @@ lodash@^4.0.0, lodash@^4.1.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lo version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" +lolex@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" + +lolex@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.1.2.tgz#2694b953c9ea4d013e5b8bfba891c991025b2629" + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" @@ -4908,6 +4930,10 @@ nan@^2.0.0, nan@^2.3.0, nan@^2.4.0: version "2.5.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.0.tgz#aa8f1e34531d807e9e27755b234b4a6ec0c152a8" +native-promise-only@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -4945,6 +4971,15 @@ nib@~1.1.2: dependencies: stylus "0.54.5" +nise@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.0.1.tgz#0da92b10a854e97c0f496f6c2845a301280b3eef" + dependencies: + formatio "^1.2.0" + just-extend "^1.1.22" + lolex "^1.6.0" + path-to-regexp "^1.7.0" + no-case@^2.2.0: version "2.3.1" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081" @@ -5413,6 +5448,12 @@ path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -6751,6 +6792,10 @@ safe-buffer@^5.0.1, safe-buffer@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" +samsam@1.x, samsam@^1.1.3: + version "1.2.1" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.2.1.tgz#edd39093a3184370cb859243b2bdf255e7d8ea67" + sax@0.5.x: version "0.5.8" resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1" @@ -6877,6 +6922,24 @@ simplemde@^1.11.2: codemirror-spell-checker "*" marked "*" +sinon-chai@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-2.13.0.tgz#b9a42e801c20234bfc2f43b29e6f4f61b60990c4" + +sinon@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-3.2.1.tgz#d8adabd900730fd497788a027049c64b08be91c2" + dependencies: + diff "^3.1.0" + formatio "1.2.0" + lolex "^2.1.2" + native-promise-only "^0.8.1" + nise "^1.0.1" + path-to-regexp "^1.7.0" + samsam "^1.1.3" + text-encoding "0.6.4" + type-detect "^4.0.0" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -7276,6 +7339,10 @@ tcomb@^2.5.1: version "2.7.0" resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-2.7.0.tgz#10d62958041669a5d53567b9a4ee8cde22b1c2b0" +text-encoding@0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" + text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -7433,6 +7500,10 @@ type-detect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" +type-detect@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea" + type-is@~1.6.14: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" From 5036a2665af828e27466958b26b7d0bdd02875c7 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sun, 20 Aug 2017 12:25:22 -0600 Subject: [PATCH 011/121] fixes to tests and impl edit hooks --- services/comments.js | 14 +++--- test/server/graph/mutations/editComment.js | 51 ++++++++++------------ test/server/services/comments.js | 2 +- 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/services/comments.js b/services/comments.js index 9f3bce0ea..88e44dc6e 100644 --- a/services/comments.js +++ b/services/comments.js @@ -1,6 +1,6 @@ const CommentModel = require('../models/comment'); -const ActionModel = require('../models/action'); +const debug = require('debug')('talk:services:comments'); const ActionsService = require('./actions'); const SettingsService = require('./settings'); @@ -63,7 +63,7 @@ module.exports = class CommentsService { id, author_id, status: { - $in: ['NONE', 'PREMOD'], + $in: ['NONE', 'PREMOD', 'ACCEPTED'], }, }; @@ -100,21 +100,25 @@ module.exports = class CommentsService { // Try to get the comment. const comment = await CommentsService.findById(id); if (comment === null) { + debug('rejecting comment edit because comment was not found'); throw errors.ErrNotFound; } // Check to see if the user was't allowed to edit it. if (comment.author_id !== author_id) { + debug('rejecting comment edit because author id does not match editing user'); throw errors.ErrNotAuthorized; } // Check to see if the comment had a status that was editable. - if (!['NONE', 'PREMOD'].includes(comment.status)) { + if (!['NONE', 'PREMOD', 'ACCEPTED'].includes(comment.status)) { + debug('rejecting comment edit because original comment has a non-editable status'); throw errors.ErrNotAuthorized; } // Check to see if the edit window expired. if (!ignoreEditWindow && comment.created_at <= lastEditableCommentCreatedAt) { + debug('rejecting comment edit because outside edit time window'); throw errors.ErrEditWindowHasEnded; } @@ -351,7 +355,7 @@ const incrReplyCount = async (comment, value) => { events.on(COMMENTS_NEW, async (comment) => { if ( !comment || // Check that the comment is defined. - (comment.parent_id === null || comment.parent_id.length === 0) || // Check that the comment has a parent (is a reply). + (!comment.parent_id || comment.parent_id.length === 0) || // Check that the comment has a parent (is a reply). !(comment.status === 'NONE' || comment.status === 'APPROVED') // Check that the comment is visible. ) { return; @@ -365,7 +369,7 @@ events.on(COMMENTS_NEW, async (comment) => { events.on(COMMENTS_EDIT, async (originalComment, editedComment) => { if ( !editedComment || // Check that the comment is defined. - (editedComment.parent_id === null || editedComment.parent_id.length === 0) // Check that the comment has a parent (is a reply). + (!editedComment.parent_id || editedComment.parent_id.length === 0) // Check that the comment has a parent (is a reply). ) { return; } diff --git a/test/server/graph/mutations/editComment.js b/test/server/graph/mutations/editComment.js index 4a6b7f1dd..c6b13638b 100644 --- a/test/server/graph/mutations/editComment.js +++ b/test/server/graph/mutations/editComment.js @@ -11,20 +11,12 @@ const CommentsService = require('../../../../services/comments'); const {expect} = require('chai'); describe('graph.mutations.editComment', () => { - let asset; - let user; - let settings; + let asset, user; beforeEach(async () => { timekeeper.reset(); - settings = await SettingsService.init(); + await SettingsService.init(); asset = await AssetModel.create({}); - user = await UsersService.createLocalUser( - 'usernameA@example.com', 'password', 'usernameA'); - }); - afterEach(async () => { - await asset.remove(); - await user.remove(); - await settings.remove(); + user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); }); const editCommentMutation = ` @@ -120,8 +112,7 @@ describe('graph.mutations.editComment', () => { body: `hello there! ${String(Math.random()).slice(2)}`, }); - const userB = await UsersService.createLocalUser( - 'usernameB@example.com', 'password', 'usernameB'); + const userB = await UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB'); const newBody = 'This body should never be set'; const context = new Context({user: userB}); const response = await graphql(schema, editCommentMutation, {}, context, { @@ -161,7 +152,7 @@ describe('graph.mutations.editComment', () => { const bannedWord = 'BANNED_WORD'; [ { - description: 'premod: editing a REJECTED comment sets back to PREMOD', + description: 'premod: editing a REJECTED comment is rejected', settings: { moderation: 'PRE', }, @@ -172,9 +163,7 @@ describe('graph.mutations.editComment', () => { edit: { body: 'I have been edited to be less offensive', }, - afterEdit: { - status: 'PREMOD', - }, + error: true }, { description: 'editing an ACCEPTED comment to add a bad word sets status to REJECTED', @@ -196,7 +185,7 @@ describe('graph.mutations.editComment', () => { }, }, { - description: 'postmod: editing a REJECTED comment with banned word to remove banned word sets status to NONE', + description: 'postmod: editing a REJECTED comment with banned word be rejected', settings: { moderation: 'POST', wordlist: { @@ -210,9 +199,7 @@ describe('graph.mutations.editComment', () => { edit: { body: 'I have been edited to remove the bad word' }, - afterEdit: { - status: 'NONE', - }, + error: true }, { description: 'postmod + premodLinksEnable: editing an ACCEPTED comment to add a link sets status to PREMOD', @@ -231,9 +218,8 @@ describe('graph.mutations.editComment', () => { status: 'PREMOD', }, }, - ].forEach(({description, settings, beforeEdit, edit, afterEdit, only}) => { - const test = only ? it.only : it; - test(description, async () => { + ].forEach(({description, settings, beforeEdit, edit, afterEdit, error}) => { + it(description, async () => { await SettingsService.update(settings); const context = new Context({user}); const comment = await CommentsService.publicCreate(Object.assign( @@ -253,11 +239,18 @@ describe('graph.mutations.editComment', () => { body: newBody } }); - if (response.errors && response.errors.length) {console.error(response.errors);} - expect(response.errors).to.be.empty; - const commentAfterEdit = await CommentsService.findById(comment.id); - expect(commentAfterEdit.body).to.equal(newBody); - expect(commentAfterEdit.status).to.equal(afterEdit.status); + + if (error) { + expect(response.data.editComment.errors).to.not.be.empty; + } else { + if (response.data.editComment.errors && response.data.editComment.errors.length) { + console.error(response.data.editComment.errors); + } + expect(response.data.editComment.errors).to.be.null; + const commentAfterEdit = await CommentsService.findById(comment.id); + expect(commentAfterEdit.body).to.equal(newBody); + expect(commentAfterEdit.status).to.equal(afterEdit.status); + } }); }); }); diff --git a/test/server/services/comments.js b/test/server/services/comments.js index 8be3e3f00..e33089325 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -201,7 +201,7 @@ describe('services.CommentsService', () => { expect(c.status).to.be.equal('NONE'); const spy = sinon.spy(); - events.once(COMMENTS_EDIT, () => spy()); + events.once(COMMENTS_EDIT, spy); let c2 = await CommentsService.pushStatus(comment_id, 'REJECTED', '123'); expect(c2).to.have.property('status'); From 7125250cd6cde0bbc16f95b1552880fe0ceb1aaa Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Sun, 20 Aug 2017 12:54:46 -0600 Subject: [PATCH 012/121] updated tests --- graph/mutators/action.js | 2 +- graph/mutators/comment.js | 2 +- services/actions.js | 2 +- services/comments.js | 4 +- services/users.js | 2 +- test/server/services/actions.js | 166 +++++++++++++++++++++++++------- 6 files changed, 138 insertions(+), 40 deletions(-) diff --git a/graph/mutators/action.js b/graph/mutators/action.js index 0b0d76895..794da581a 100644 --- a/graph/mutators/action.js +++ b/graph/mutators/action.js @@ -22,7 +22,7 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id, } } - let action = await ActionsService.insertUserAction({ + let action = await ActionsService.create({ item_id, item_type, user_id: user.id, diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 533032412..b32b8ed79 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -309,7 +309,7 @@ const createPublicComment = async (context, commentInput) => { // 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. - await ActionsService.insertUserAction({ + await ActionsService.create({ item_id: comment.id, item_type: 'COMMENTS', action_type: 'FLAG', diff --git a/services/actions.js b/services/actions.js index 8ee50df61..d2c2350bd 100644 --- a/services/actions.js +++ b/services/actions.js @@ -58,7 +58,7 @@ module.exports = class ActionsService { * @param {String} action the new action to the item * @return {Promise} */ - static async insertUserAction(action) { + static async create(action) { // Actions are made unique by using a query that can be reproducable, i.e., // not containing user inputable values. diff --git a/services/comments.js b/services/comments.js index 88e44dc6e..07c822459 100644 --- a/services/comments.js +++ b/services/comments.js @@ -280,7 +280,7 @@ module.exports = class CommentsService { * @return {Promise} */ static addAction(item_id, user_id, action_type, metadata = {}) { - return ActionsService.insertUserAction({ + return ActionsService.create({ item_id, item_type: 'COMMENTS', user_id, @@ -301,7 +301,7 @@ const incrActionCounts = async (action, value) => { [`action_counts.${ACTION_TYPE}`]: value, }; - if (action.group_id !== null && action.group_id.length > 0) { + if (action.group_id && action.group_id.length > 0) { const GROUP_ID = sc(action.group_id.toLowerCase()); update[`action_counts.${ACTION_TYPE}_${GROUP_ID}`] = value; diff --git a/services/users.js b/services/users.js index 7ed55f712..64e4494a0 100644 --- a/services/users.js +++ b/services/users.js @@ -737,7 +737,7 @@ module.exports = class UsersService { * @return {Promise} */ static addAction(item_id, user_id, action_type, metadata) { - return ActionsService.insertUserAction({ + return ActionsService.create({ item_id, item_type: 'users', user_id, diff --git a/test/server/services/actions.js b/test/server/services/actions.js index 4d22dff6f..aef565908 100644 --- a/test/server/services/actions.js +++ b/test/server/services/actions.js @@ -1,36 +1,134 @@ const ActionModel = require('../../../models/action'); const ActionsService = require('../../../services/actions'); +const CommentModel = require('../../../models/comment'); -const expect = require('chai').expect; +const chai = require('chai'); +chai.use(require('chai-as-promised')); +const expect = chai.expect; + +const events = require('../../../services/events'); +const {ACTIONS_NEW, ACTIONS_DELETE} = require('../../../services/events/constants'); + +const sinon = require('sinon'); describe('services.ActionsService', () => { let mockActions = []; + let comment; - beforeEach(() => ActionModel.create([ - { - action_type: 'FLAG', - item_id: '123', - item_type: 'COMMENTS', - user_id: 'flagginguserid' - }, - { - action_type: 'FLAG', - item_id: '456', - item_type: 'COMMENTS' - }, - { - action_type: 'FLAG', - item_id: '123', - item_type: 'COMMENTS' - }, - { - action_type: 'LIKE', - item_id: '123', - item_type: 'COMMENTS' - } - ]).then((actions) => { - mockActions = actions; - })); + beforeEach(async () => { + comment = await CommentModel.create({ + body: 'comment 10', + asset_id: '123', + status_history: [], + parent_id: '', + author_id: '123', + id: '1' + }); + + mockActions = await ActionModel.create([ + { + action_type: 'FLAG', + item_id: comment.id, + item_type: 'COMMENTS', + user_id: 'flagginguserid' + }, + { + action_type: 'FLAG', + item_id: '456', + item_type: 'COMMENTS', + user_id: '1' + }, + { + action_type: 'FLAG', + item_id: comment.id, + item_type: 'COMMENTS', + user_id: '2' + }, + { + action_type: 'LIKE', + item_id: comment.id, + item_type: 'COMMENTS', + user_id: '3' + } + ]); + }); + + describe('#create', () => { + + it('creates an action', async () => { + const srcAction = { + action_type: 'LIKE', + item_type: 'COMMENTS', + item_id: comment.id, + }; + + const createdAction = await ActionsService.create(srcAction); + + expect(createdAction).is.not.null; + expect(createdAction).has.property('id'); + expect(createdAction).has.property('item_id', comment.id); + + const retrievedAction = await ActionModel.findOne({id: createdAction.id}); + + expect(retrievedAction).is.not.null; + expect(retrievedAction).has.property('id', createdAction.id); + expect(retrievedAction).has.property('item_id', comment.id); + }); + + it('fires the callback sucesfully', async () => { + const srcAction = { + action_type: 'LIKE', + item_type: 'COMMENTS', + item_id: comment.id, + }; + + const spy = sinon.spy(); + events.once(ACTIONS_NEW, spy); + + const createdAction = await ActionsService.create(srcAction); + + expect(createdAction).is.not.null; + expect(createdAction).has.property('id'); + expect(createdAction).has.property('item_id', comment.id); + + expect(spy).to.have.been.calledWith(createdAction); + + const retrievedComment = await CommentModel.findOne({id: comment.id}); + + expect(retrievedComment).to.have.property('action_counts'); + expect(retrievedComment.action_counts).to.have.property('like', 1); + }); + + }); + + describe('#delete', () => { + + it('deletes an action', async () => { + const deletedAction = await ActionsService.delete(mockActions[0]); + + expect(deletedAction).has.property('id', mockActions[0].id); + + const retrievedAction = await ActionModel.findOne({id: deletedAction.id}); + + expect(retrievedAction).is.null; + }); + + it('fires the callback sucesfully', async () => { + const spy = sinon.spy(); + events.once(ACTIONS_DELETE, spy); + + const deletedAction = await ActionsService.delete(mockActions[0]); + + expect(deletedAction).has.property('id', mockActions[0].id); + expect(spy).to.have.been.calledWith(deletedAction); + + const retrievedComment = await CommentModel.findOne({id: comment.id}); + + expect(retrievedComment).to.have.property('action_counts'); + expect(retrievedComment.action_counts).to.have.property('flag', -1); + }); + + }); describe('#findById()', () => { it('should find an action by id', () => { @@ -43,7 +141,7 @@ describe('services.ActionsService', () => { describe('#findByItemIdArray()', () => { it('should find an array of actions from an array of item_ids', () => { - return ActionsService.findByItemIdArray(['123', '456']).then((result) => { + return ActionsService.findByItemIdArray([comment.id, '456']).then((result) => { expect(result).to.have.length(4); }); }); @@ -52,14 +150,14 @@ describe('services.ActionsService', () => { describe('#getActionSummaries()', () => { it('should return properly formatted summaries from an array of item_ids', () => { return ActionsService - .getActionSummaries(['123', '789']) + .getActionSummaries([comment.id, '789']) .then((summaries) => { expect(summaries).to.have.length(2); expect(summaries).to.deep.include({ action_type: 'LIKE', count: 1, - item_id: '123', + item_id: comment.id, item_type: 'COMMENTS', current_user: null }); @@ -67,7 +165,7 @@ describe('services.ActionsService', () => { expect(summaries).to.deep.include({ action_type: 'FLAG', count: 2, - item_id: '123', + item_id: comment.id, item_type: 'COMMENTS', current_user: null }); @@ -76,15 +174,15 @@ describe('services.ActionsService', () => { it('should include a current user when one is passed', () => { return ActionsService - .getActionSummaries(['123'], 'flagginguserid') + .getActionSummaries([comment.id], 'flagginguserid') .then((summaries) => { expect(summaries).to.have.length(2); - let summary = summaries.find((s) => s.item_id === '123' && s.action_type === 'FLAG'); + let summary = summaries.find((s) => s.item_id === comment.id && s.action_type === 'FLAG'); expect(summary).to.not.be.undefined; expect(summary.current_user).to.not.be.null; - expect(summary.current_user).to.have.property('item_id', '123'); + expect(summary.current_user).to.have.property('item_id', comment.id); expect(summary.current_user).to.have.property('item_type', 'COMMENTS'); expect(summary.current_user).to.have.property('user_id', 'flagginguserid'); expect(summary.current_user).to.have.property('action_type', 'FLAG'); @@ -93,7 +191,7 @@ describe('services.ActionsService', () => { it('should not include a current user when one is passed for a user that doesn\'t have an action', () => { return ActionsService - .getActionSummaries(['123'], 'flagginguserid2') + .getActionSummaries([comment.id], 'flagginguserid2') .then((summaries) => { expect(summaries).to.have.length(2); From 0ea2660c9786d46756a4d31cf9b98dc781cc60cb Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 21 Aug 2017 10:44:03 -0300 Subject: [PATCH 013/121] Adding new Slot: authorName - Deleting Old Code --- .../src/components/AuthorName.css | 4 +++ .../src/components/AuthorName.js | 9 +++++ .../src/components/Comment.js | 15 +++++--- client/coral-embed-stream/style/default.css | 10 ++---- client/talk-plugin-author-name/AuthorName.js | 31 ----------------- client/talk-plugin-author-name/styles.css | 34 ------------------- 6 files changed, 26 insertions(+), 77 deletions(-) create mode 100644 client/coral-embed-stream/src/components/AuthorName.css create mode 100644 client/coral-embed-stream/src/components/AuthorName.js delete mode 100644 client/talk-plugin-author-name/AuthorName.js delete mode 100644 client/talk-plugin-author-name/styles.css diff --git a/client/coral-embed-stream/src/components/AuthorName.css b/client/coral-embed-stream/src/components/AuthorName.css new file mode 100644 index 000000000..978c370f7 --- /dev/null +++ b/client/coral-embed-stream/src/components/AuthorName.css @@ -0,0 +1,4 @@ +.authorName { + margin-right: 5px; + font-weight: bold; +} diff --git a/client/coral-embed-stream/src/components/AuthorName.js b/client/coral-embed-stream/src/components/AuthorName.js new file mode 100644 index 000000000..96dc52aee --- /dev/null +++ b/client/coral-embed-stream/src/components/AuthorName.js @@ -0,0 +1,9 @@ +import React from 'react'; +import styles from './AuthorName.css'; + +const AuthorName = ({comment}) => + + {comment.user.username} + ; + +export default AuthorName; diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index bb6fee981..77c41b23d 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import AuthorName from 'talk-plugin-author-name/AuthorName'; import TagLabel from 'talk-plugin-tag-label/TagLabel'; import PubDate from 'talk-plugin-pubdate/PubDate'; import {ReplyBox, ReplyButton} from 'talk-plugin-replies'; @@ -26,6 +25,7 @@ import {EditableCommentContent} from './EditableCommentContent'; import {getActionSummary, iPerformedThisAction, forEachError, isCommentActive, getShallowChanges} from 'coral-framework/utils'; import t from 'coral-framework/services/i18n'; import CommentContainer from '../containers/Comment'; +import AuthorName from './AuthorName'; const isStaff = (tags) => !tags.every((t) => t.tag.name !== 'STAFF'); const hasTag = (tags, lookupTag) => !!tags.filter((t) => t.tag.name === lookupTag).length; @@ -434,10 +434,17 @@ export default class Comment extends React.Component { inline /> -
+
+
+ + -
- {isStaff(comment.tags) ? Staff : null} diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 6ba376096..3fc45f952 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -238,14 +238,8 @@ body { line-height: 1.3; } -.talk-plugin-author-name-text { - display: inline-block; - margin: 10px 5px 10px 0; - font-weight: bold; -} - -.talk-plugin-author-name-bio-flag { - float: right; +.talk-stream-comment-header { + margin: 10px 0; } /* Tag Labels */ diff --git a/client/talk-plugin-author-name/AuthorName.js b/client/talk-plugin-author-name/AuthorName.js deleted file mode 100644 index 89835a742..000000000 --- a/client/talk-plugin-author-name/AuthorName.js +++ /dev/null @@ -1,31 +0,0 @@ -import React, {Component} from 'react'; -const packagename = 'talk-plugin-author-name'; - -export default class AuthorName extends Component { - - state = {showTooltip: false} - - handleClick = () => { - this.setState((state) => ({ - showTooltip: !state.showTooltip - })); - } - - handleMouseLeave = () => { - setTimeout(() => { - this.setState({ - showTooltip: false - }); - }, 500); - } - - render () { - const {author} = this.props; - return ( -
- {author && author.username} -
- ); - } -} diff --git a/client/talk-plugin-author-name/styles.css b/client/talk-plugin-author-name/styles.css deleted file mode 100644 index b7870a862..000000000 --- a/client/talk-plugin-author-name/styles.css +++ /dev/null @@ -1,34 +0,0 @@ -.authorName { - color: black; - display: inline-block; - margin: 10px 8px 10px 0; -} - -.hasBio { - &:hover { - cursor: pointer; - } -} - -.arrowDown { - top: 0; - width: 0; - height: 0; - margin-top: -2px; - margin-left: 2px; - display: inline-block; - vertical-align: middle; - border-bottom: 0; - border-left: 3px solid transparent; - border-right: 3px solid transparent; - border-top: 3px solid #000000; -} - -.arrowUp { - width: 0; - height: 0; - border-top: 0; - border-left: 3px solid transparent; - border-right: 3px solid transparent; - border-bottom: 3px solid black; -} From 84e4091ea6eea48ccf0e38a0f53399ba65bf5b2b Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 21 Aug 2017 20:06:53 +0700 Subject: [PATCH 014/121] Implement TalkProvider --- client/coral-admin/src/index.js | 18 ++++++----- .../src/containers/Embed.js | 9 ++++-- client/coral-embed-stream/src/index.js | 18 ++++++----- .../components/ClickOutside.js | 15 ++++++++-- .../components/EventEmitterProvider.js | 18 ----------- .../components/TalkProvider.js | 30 +++++++++++++++++++ 6 files changed, 70 insertions(+), 38 deletions(-) delete mode 100644 client/coral-framework/components/EventEmitterProvider.js create mode 100644 client/coral-framework/components/TalkProvider.js diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index 14437c51b..ed9d537c0 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -1,9 +1,8 @@ import React from 'react'; import {render} from 'react-dom'; -import {ApolloProvider} from 'react-apollo'; import smoothscroll from 'smoothscroll-polyfill'; import EventEmitter from 'eventemitter2'; -import EventEmitterProvider from 'coral-framework/components/EventEmitterProvider'; +import TalkProvider from 'coral-framework/components/TalkProvider'; import {getClient} from 'coral-framework/services/client'; import store from './services/store'; @@ -13,8 +12,10 @@ import App from './components/App'; import 'react-mdl/extra/material.js'; import './graphql'; import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins'; +import plugins from 'pluginsConfig'; const eventEmitter = new EventEmitter(); +const client = getClient(); // TODO: pass redux actions through the emitter. @@ -23,10 +24,13 @@ injectPluginsReducers(); smoothscroll.polyfill(); render( - - - - - + + + , document.querySelector('#root') ); diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index e21a683d1..6eb0d9f1c 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -10,20 +10,23 @@ import renderComponent from 'recompose/renderComponent'; import {Spinner} from 'coral-ui'; import * as authActions from 'coral-framework/actions/auth'; import * as assetActions from 'coral-framework/actions/asset'; -import pym from 'coral-framework/services/pym'; import {getDefinitionName} from 'coral-framework/utils'; import {withQuery} from 'coral-framework/hocs'; import Embed from '../components/Embed'; import Stream from './Stream'; import {addNotification} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; - +import PropTypes from 'prop-types'; import {setActiveTab} from '../actions/embed'; const {logout, checkLogin, focusSignInDialog, blurSignInDialog, hideSignInDialog} = authActions; const {fetchAssetSuccess} = assetActions; class EmbedContainer extends React.Component { + static contextTypes = { + pym: PropTypes.object, + }; + subscriptions = []; subscribeToUpdates(props = this.props) { @@ -95,7 +98,7 @@ class EmbedContainer extends React.Component { if (!get(prevProps, 'root.asset.comment') && get(this.props, 'root.asset.comment')) { // Scroll to a permalinked comment if one is in the URL once the page is done rendering. - setTimeout(() => pym.scrollParentToChildEl('talk-embed-stream-container'), 0); + setTimeout(() => this.context.pym.scrollParentToChildEl('talk-embed-stream-container'), 0); } } diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index b8b7804dc..e973ca0d4 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -1,6 +1,5 @@ import React from 'react'; import {render} from 'react-dom'; -import {ApolloProvider} from 'react-apollo'; import {checkLogin, handleAuthToken, logout} from 'coral-framework/actions/auth'; import './graphql'; @@ -13,7 +12,8 @@ import AppRouter from './AppRouter'; import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins'; import reducers from './reducers'; import EventEmitter from 'eventemitter2'; -import EventEmitterProvider from 'coral-framework/components/EventEmitterProvider'; +import TalkProvider from 'coral-framework/components/TalkProvider'; +import plugins from 'pluginsConfig'; const store = getStore(); const client = getClient(); @@ -68,10 +68,14 @@ if (!window.opener) { } render( - - - - - + + + , document.querySelector('#talk-embed-stream-container') ); diff --git a/client/coral-framework/components/ClickOutside.js b/client/coral-framework/components/ClickOutside.js index 5348a9af7..054a9e6fe 100644 --- a/client/coral-framework/components/ClickOutside.js +++ b/client/coral-framework/components/ClickOutside.js @@ -1,13 +1,16 @@ import {Component, cloneElement, Children} from 'react'; import PropTypes from 'prop-types'; import {findDOMNode} from 'react-dom'; -import pym from 'coral-framework/services/pym'; export default class ClickOutside extends Component { static propTypes = { onClickOutside: PropTypes.func.isRequired }; + static contextTypes = { + pym: PropTypes.object, + }; + domNode = null; handleClick = (e) => { @@ -18,14 +21,20 @@ export default class ClickOutside extends Component { }; componentDidMount() { + const {pym} = this.context; this.domNode = findDOMNode(this); document.addEventListener('click', this.handleClick, true); - pym.onMessage('click', this.handleClick); + if (pym) { + pym.onMessage('click', this.handleClick); + } } componentWillUnmount() { + const {pym} = this.context; document.removeEventListener('click', this.handleClick, true); - pym.messageHandlers.click = pym.messageHandlers.click.filter((h) => h !== this.handleClick); + if (pym) { + pym.messageHandlers.click = pym.messageHandlers.click.filter((h) => h !== this.handleClick); + } } render() { diff --git a/client/coral-framework/components/EventEmitterProvider.js b/client/coral-framework/components/EventEmitterProvider.js deleted file mode 100644 index 36c0bccdb..000000000 --- a/client/coral-framework/components/EventEmitterProvider.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -const PropTypes = require('prop-types'); - -class EventEmitterProvider extends React.Component { - getChildContext() { - return {eventEmitter: this.props.eventEmitter}; - } - - render() { - return this.props.children; - } -} - -EventEmitterProvider.childContextTypes = { - eventEmitter: PropTypes.object, -}; - -export default EventEmitterProvider; diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js new file mode 100644 index 000000000..275373c26 --- /dev/null +++ b/client/coral-framework/components/TalkProvider.js @@ -0,0 +1,30 @@ +import React from 'react'; +const PropTypes = require('prop-types'); +import {ApolloProvider} from 'react-apollo'; + +class TalkProvider extends React.Component { + getChildContext() { + return { + eventEmitter: this.props.eventEmitter, + pym: this.props.pym, + plugins: this.props.plugins, + }; + } + + render() { + const {children, client, store, plugins} = this.props; + return ( + + {children} + + ); + } +} + +TalkProvider.childContextTypes = { + pym: PropTypes.object, + eventEmitter: PropTypes.object, + plugins: PropTypes.array, +}; + +export default TalkProvider; From 9566f83eac7aee9031bc8ff197ec12af1582b3e3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 21 Aug 2017 21:31:54 +0700 Subject: [PATCH 015/121] Remove coral-framework reducers Some of them were only related to the embed stream. Auth code is too convoluted with the embed stream and can't be reused atm. --- client/coral-admin/src/actions/auth.js | 29 ++++++++++++++++++- client/coral-admin/src/containers/Layout.js | 3 +- .../containers/ConfigureStreamContainer.js | 2 +- .../src}/actions/asset.js | 4 +-- .../src}/actions/auth.js | 8 ++--- .../src}/constants/asset.js | 0 .../src}/constants/auth.js | 0 .../src/containers/Embed.js | 4 +-- .../src/containers/Stream.js | 2 +- client/coral-embed-stream/src/index.js | 2 +- .../src}/reducers/asset.js | 0 .../src}/reducers/auth.js | 0 .../coral-embed-stream/src/reducers/index.js | 6 ++++ .../coral-embed-stream/src/reducers/stream.js | 2 +- client/coral-framework/reducers/index.js | 9 ------ client/coral-framework/services/store.js | 2 -- .../containers/ProfileContainer.js | 5 +++- plugin-api/beta/client/hocs/withReaction.js | 4 ++- .../client/components/ChangeUsername.js | 2 +- .../client/components/SignInButton.js | 2 +- .../client/components/SignInContainer.js | 2 +- .../client/components/UserBox.js | 2 +- 22 files changed, 58 insertions(+), 32 deletions(-) rename client/{coral-framework => coral-embed-stream/src}/actions/asset.js (94%) rename client/{coral-framework => coral-embed-stream/src}/actions/auth.js (97%) rename client/{coral-framework => coral-embed-stream/src}/constants/asset.js (100%) rename client/{coral-framework => coral-embed-stream/src}/constants/auth.js (100%) rename client/{coral-framework => coral-embed-stream/src}/reducers/asset.js (100%) rename client/{coral-framework => coral-embed-stream/src}/reducers/auth.js (100%) delete mode 100644 client/coral-framework/reducers/index.js diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 0c1264e1f..8c84a95ec 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -2,9 +2,9 @@ import bowser from 'bowser'; import * as actions from '../constants/auth'; import coralApi from 'coral-framework/helpers/request'; import * as Storage from 'coral-framework/helpers/storage'; -import {handleAuthToken} from 'coral-framework/actions/auth'; import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; +import jwtDecode from 'jwt-decode'; //============================================================================== // SIGN IN @@ -136,3 +136,30 @@ export const checkLogin = () => (dispatch) => { dispatch(checkLoginFailure(errorMessage)); }); }; + +//============================================================================== +// LOGOUT +//============================================================================== + +export const logout = () => (dispatch) => { + return coralApi('/auth', {method: 'DELETE'}).then(() => { + Storage.removeItem('token'); + + // Reset the websocket. + resetWebsocket(); + + dispatch({type: actions.LOGOUT}); + }); +}; + +//============================================================================== +// AUTH TOKEN +//============================================================================== + +export const handleAuthToken = (token) => (dispatch) => { + Storage.setItem('exp', jwtDecode(token).exp); + Storage.setItem('token', token); + + dispatch({type: 'HANDLE_AUTH_TOKEN'}); +}; + diff --git a/client/coral-admin/src/containers/Layout.js b/client/coral-admin/src/containers/Layout.js index ae022f2ef..2c5e1621a 100644 --- a/client/coral-admin/src/containers/Layout.js +++ b/client/coral-admin/src/containers/Layout.js @@ -3,12 +3,11 @@ import {connect} from 'react-redux'; import Layout from '../components/ui/Layout'; import {fetchConfig} from '../actions/config'; import AdminLogin from '../components/AdminLogin'; -import {logout} from 'coral-framework/actions/auth'; import {FullLoading} from '../components/FullLoading'; import BanUserDialog from './BanUserDialog'; import SuspendUserDialog from './SuspendUserDialog'; import {toggleModal as toggleShortcutModal} from '../actions/moderation'; -import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth'; +import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth'; import {can} from 'coral-framework/services/perms'; import UserDetail from 'coral-admin/src/containers/UserDetail'; diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index 8489c809f..1699c3b45 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -2,7 +2,7 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; import {compose} from 'react-apollo'; -import {updateOpenStatus, updateConfiguration} from 'coral-framework/actions/asset'; +import {updateOpenStatus, updateConfiguration} from 'coral-embed-stream/src/actions/asset'; import CloseCommentsInfo from '../components/CloseCommentsInfo'; import ConfigureCommentStream from '../components/ConfigureCommentStream'; diff --git a/client/coral-framework/actions/asset.js b/client/coral-embed-stream/src/actions/asset.js similarity index 94% rename from client/coral-framework/actions/asset.js rename to client/coral-embed-stream/src/actions/asset.js index aab3caf3a..87b6fdd98 100644 --- a/client/coral-framework/actions/asset.js +++ b/client/coral-embed-stream/src/actions/asset.js @@ -1,6 +1,6 @@ import * as actions from '../constants/asset'; -import coralApi from '../helpers/request'; -import {addNotification} from '../actions/notification'; +import coralApi from 'coral-framework/helpers/request'; +import {addNotification} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; diff --git a/client/coral-framework/actions/auth.js b/client/coral-embed-stream/src/actions/auth.js similarity index 97% rename from client/coral-framework/actions/auth.js rename to client/coral-embed-stream/src/actions/auth.js index 7c6d3bf89..851889a9b 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-embed-stream/src/actions/auth.js @@ -1,10 +1,10 @@ import jwtDecode from 'jwt-decode'; import bowser from 'bowser'; import * as actions from '../constants/auth'; -import * as Storage from '../helpers/storage'; -import coralApi, {base} from '../helpers/request'; -import pym from '../services/pym'; -import {addNotification} from '../actions/notification'; +import * as Storage from 'coral-framework/helpers/storage'; +import coralApi, {base} from 'coral-framework/helpers/request'; +import pym from 'coral-framework/services/pym'; +import {addNotification} from 'coral-framework/actions/notification'; import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; diff --git a/client/coral-framework/constants/asset.js b/client/coral-embed-stream/src/constants/asset.js similarity index 100% rename from client/coral-framework/constants/asset.js rename to client/coral-embed-stream/src/constants/asset.js diff --git a/client/coral-framework/constants/auth.js b/client/coral-embed-stream/src/constants/auth.js similarity index 100% rename from client/coral-framework/constants/auth.js rename to client/coral-embed-stream/src/constants/auth.js diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 6eb0d9f1c..c7400a506 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -8,8 +8,8 @@ import branch from 'recompose/branch'; import renderComponent from 'recompose/renderComponent'; import {Spinner} from 'coral-ui'; -import * as authActions from 'coral-framework/actions/auth'; -import * as assetActions from 'coral-framework/actions/asset'; +import * as authActions from '../actions/auth'; +import * as assetActions from '../actions/asset'; import {getDefinitionName} from 'coral-framework/utils'; import {withQuery} from 'coral-framework/hocs'; import Embed from '../components/Embed'; diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 451b115a9..e0ee02d80 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -8,7 +8,7 @@ import { withDeleteAction, withIgnoreUser, withEditComment } from 'coral-framework/graphql/mutations'; -import * as authActions from 'coral-framework/actions/auth'; +import * as authActions from 'coral-embed-stream/src/actions/auth'; import * as notificationActions from 'coral-framework/actions/notification'; import {setActiveReplyBox, setActiveTab, viewAllComments} from '../actions/stream'; import Stream from '../components/Stream'; diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index e973ca0d4..a14ce8df8 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -1,7 +1,7 @@ import React from 'react'; import {render} from 'react-dom'; -import {checkLogin, handleAuthToken, logout} from 'coral-framework/actions/auth'; +import {checkLogin, handleAuthToken, logout} from 'coral-embed-stream/src/actions/auth'; import './graphql'; import {addExternalConfig} from 'coral-embed-stream/src/actions/config'; import {getStore, injectReducers, addListener} from 'coral-framework/services/store'; diff --git a/client/coral-framework/reducers/asset.js b/client/coral-embed-stream/src/reducers/asset.js similarity index 100% rename from client/coral-framework/reducers/asset.js rename to client/coral-embed-stream/src/reducers/asset.js diff --git a/client/coral-framework/reducers/auth.js b/client/coral-embed-stream/src/reducers/auth.js similarity index 100% rename from client/coral-framework/reducers/auth.js rename to client/coral-embed-stream/src/reducers/auth.js diff --git a/client/coral-embed-stream/src/reducers/index.js b/client/coral-embed-stream/src/reducers/index.js index 61fe950c9..5c553b04f 100644 --- a/client/coral-embed-stream/src/reducers/index.js +++ b/client/coral-embed-stream/src/reducers/index.js @@ -1,8 +1,14 @@ +import auth from './auth'; +import asset from './asset'; import embed from './embed'; import config from './config'; import stream from './stream'; +import {reducer as commentBox} from '../../../talk-plugin-commentbox'; export default { + auth, + asset, + commentBox, embed, config, stream, diff --git a/client/coral-embed-stream/src/reducers/stream.js b/client/coral-embed-stream/src/reducers/stream.js index 86d5301b7..020ca654f 100644 --- a/client/coral-embed-stream/src/reducers/stream.js +++ b/client/coral-embed-stream/src/reducers/stream.js @@ -1,5 +1,5 @@ import * as actions from '../constants/stream'; -import * as authActions from 'coral-framework/constants/auth'; +import * as authActions from '../constants/auth'; function getQueryVariable(variable) { let query = window.location.search.substring(1); diff --git a/client/coral-framework/reducers/index.js b/client/coral-framework/reducers/index.js deleted file mode 100644 index 6b9b730ca..000000000 --- a/client/coral-framework/reducers/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import auth from './auth'; -import asset from './asset'; -import {reducer as commentBox} from '../../talk-plugin-commentbox'; - -export default { - auth, - asset, - commentBox, -}; diff --git a/client/coral-framework/services/store.js b/client/coral-framework/services/store.js index f87a887a3..ec2479d15 100644 --- a/client/coral-framework/services/store.js +++ b/client/coral-framework/services/store.js @@ -1,6 +1,5 @@ import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; import thunk from 'redux-thunk'; -import mainReducer from '../reducers'; import {getClient} from './client'; let listeners = []; @@ -52,7 +51,6 @@ export function getStore() { } const coralReducers = { - ...mainReducer, apollo: getClient().reducer() }; diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index e33c0b6cd..00fd2b6e7 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -11,7 +11,10 @@ import NotLoggedIn from '../components/NotLoggedIn'; import IgnoredUsers from '../components/IgnoredUsers'; import {Spinner} from 'coral-ui'; import CommentHistory from 'talk-plugin-history/CommentHistory'; -import {showSignInDialog, checkLogin} from 'coral-framework/actions/auth'; + +// TODO: Auth logic needs refactoring. +import {showSignInDialog, checkLogin} from 'coral-embed-stream/src/actions/auth'; + import {insertCommentsSorted} from 'plugin-api/beta/client/utils'; import update from 'immutability-helper'; diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index 22c30d639..c06362725 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -7,12 +7,14 @@ import {getDisplayName} from 'coral-framework/helpers/hoc'; import {compose, gql} from 'react-apollo'; import withFragments from 'coral-framework/hocs/withFragments'; import withMutation from 'coral-framework/hocs/withMutation'; -import {showSignInDialog} from 'coral-framework/actions/auth'; import {addNotification} from 'coral-framework/actions/notification'; import {capitalize} from 'coral-framework/helpers/strings'; import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils'; import * as PropTypes from 'prop-types'; +// TODO: Auth logic needs refactoring. +import {showSignInDialog} from 'coral-embed-stream/src/actions/auth'; + export default (reaction) => (WrappedComponent) => { if (typeof reaction !== 'string') { console.error('Reaction must be a valid string'); diff --git a/plugins/talk-plugin-auth/client/components/ChangeUsername.js b/plugins/talk-plugin-auth/client/components/ChangeUsername.js index c1ad081b1..5c5b0891d 100644 --- a/plugins/talk-plugin-auth/client/components/ChangeUsername.js +++ b/plugins/talk-plugin-auth/client/components/ChangeUsername.js @@ -13,7 +13,7 @@ import { invalidForm, validForm, createUsername -} from 'coral-framework/actions/auth'; +} from 'coral-embed-stream/src/actions/auth'; class ChangeUsernameContainer extends React.Component { constructor(props) { diff --git a/plugins/talk-plugin-auth/client/components/SignInButton.js b/plugins/talk-plugin-auth/client/components/SignInButton.js index 9c8a04b11..6641b8dff 100644 --- a/plugins/talk-plugin-auth/client/components/SignInButton.js +++ b/plugins/talk-plugin-auth/client/components/SignInButton.js @@ -2,7 +2,7 @@ import React from 'react'; import {Button} from 'plugin-api/beta/client/components/ui'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {showSignInDialog} from 'coral-framework/actions/auth'; +import {showSignInDialog} from 'coral-embed-stream/src/actions/auth'; import t from 'coral-framework/services/i18n'; const SignInButton = ({loggedIn, showSignInDialog}) => ( diff --git a/plugins/talk-plugin-auth/client/components/SignInContainer.js b/plugins/talk-plugin-auth/client/components/SignInContainer.js index 188651186..9ef6b5988 100644 --- a/plugins/talk-plugin-auth/client/components/SignInContainer.js +++ b/plugins/talk-plugin-auth/client/components/SignInContainer.js @@ -18,7 +18,7 @@ import { facebookCallback, invalidForm, validForm, -} from 'coral-framework/actions/auth'; +} from 'coral-embed-stream/src/actions/auth'; class SignInContainer extends React.Component { constructor(props) { diff --git a/plugins/talk-plugin-auth/client/components/UserBox.js b/plugins/talk-plugin-auth/client/components/UserBox.js index 61540d3c5..eea8e7690 100644 --- a/plugins/talk-plugin-auth/client/components/UserBox.js +++ b/plugins/talk-plugin-auth/client/components/UserBox.js @@ -3,7 +3,7 @@ import styles from './styles.css'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import t from 'coral-framework/services/i18n'; -import {logout} from 'coral-framework/actions/auth'; +import {logout} from 'coral-embed-stream/src/actions/auth'; const UserBox = ({loggedIn, user, logout, onShowProfile}) => (
From d47287fbd500300ba359ac60c2c7e3d40dcfc19f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 21 Aug 2017 21:39:40 +0700 Subject: [PATCH 016/121] Remove unused import --- client/coral-embed-stream/src/containers/Embed.js | 1 - 1 file changed, 1 deletion(-) diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 01690f77e..a8f6c1b18 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -10,7 +10,6 @@ import renderComponent from 'recompose/renderComponent'; import {Spinner} from 'coral-ui'; import * as authActions from '../actions/auth'; import * as assetActions from '../actions/asset'; -import pym from 'coral-framework/services/pym'; import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils'; import {withQuery} from 'coral-framework/hocs'; import Embed from '../components/Embed'; From d43d3590483fc6c9b355905edbc914acdd8f22c1 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 21 Aug 2017 08:49:56 -0600 Subject: [PATCH 017/121] ensure that events are handled sync --- services/actions.js | 4 ++-- services/comments.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/actions.js b/services/actions.js index d2c2350bd..ba9f1fde5 100644 --- a/services/actions.js +++ b/services/actions.js @@ -75,7 +75,7 @@ module.exports = class ActionsService { }); // Emit that there was a new action created. - events.emitAsync(ACTIONS_NEW, foundAction); + await events.emitAsync(ACTIONS_NEW, foundAction); return foundAction; } @@ -230,7 +230,7 @@ module.exports = class ActionsService { } // Emit that the action was deleted. - events.emitAsync(ACTIONS_DELETE, action); + await events.emitAsync(ACTIONS_DELETE, action); return action; } diff --git a/services/comments.js b/services/comments.js index 07c822459..ac7d6ae45 100644 --- a/services/comments.js +++ b/services/comments.js @@ -46,7 +46,7 @@ module.exports = class CommentsService { const savedCommentModel = await commentModel.save(); // Emit that the comment was created! - events.emitAsync(COMMENTS_NEW, savedCommentModel); + await events.emitAsync(COMMENTS_NEW, savedCommentModel); return savedCommentModel; } @@ -138,7 +138,7 @@ module.exports = class CommentsService { created_at: new Date(), }); - events.emitAsync(COMMENTS_EDIT, originalComment, editedComment); + await events.emitAsync(COMMENTS_EDIT, originalComment, editedComment); return editedComment; } @@ -267,7 +267,7 @@ module.exports = class CommentsService { // Emit that the comment was edited, and pass the original comment and the // edited comment. - events.emitAsync(COMMENTS_EDIT, originalComment, editedComment); + await events.emitAsync(COMMENTS_EDIT, originalComment, editedComment); return editedComment; } From 9028c1b4eb96be1dd27a3fa4b0c6047d70aae42c Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 21 Aug 2017 12:15:53 -0300 Subject: [PATCH 018/121] Adding fragment Slot --- client/coral-embed-stream/src/components/AuthorName.css | 2 +- client/coral-embed-stream/src/components/Comment.js | 2 +- client/coral-embed-stream/src/containers/Comment.js | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/coral-embed-stream/src/components/AuthorName.css b/client/coral-embed-stream/src/components/AuthorName.css index 978c370f7..41771bf49 100644 --- a/client/coral-embed-stream/src/components/AuthorName.css +++ b/client/coral-embed-stream/src/components/AuthorName.css @@ -1,4 +1,4 @@ .authorName { margin-right: 5px; font-weight: bold; -} +} \ No newline at end of file diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 77c41b23d..9434339e8 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -439,7 +439,7 @@ export default class Comment extends React.Component { Date: Mon, 21 Aug 2017 13:18:46 -0300 Subject: [PATCH 019/121] Adding Slot to Featured Comments --- .../client/components/Comment.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/talk-plugin-featured-comments/client/components/Comment.js b/plugins/talk-plugin-featured-comments/client/components/Comment.js index 01b5f864c..ddb3b223a 100644 --- a/plugins/talk-plugin-featured-comments/client/components/Comment.js +++ b/plugins/talk-plugin-featured-comments/client/components/Comment.js @@ -22,9 +22,14 @@ class Comment extends React.Component {
- - {comment.user.username} - + + + ,{' '}{timeago(comment.created_at)} From b3e15de9f3cf4835026206780077287f281c8d26 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 21 Aug 2017 10:40:18 -0600 Subject: [PATCH 020/121] extended verify tool with reply count fixing --- bin/cli-verify | 5 +- bin/verifications/database/comments.js | 76 ++++++++++++++++++++++---- models/comment.js | 5 +- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/bin/cli-verify b/bin/cli-verify index be4420a7b..6e67e0016 100755 --- a/bin/cli-verify +++ b/bin/cli-verify @@ -14,10 +14,10 @@ util.onshutdown([ () => mongoose.disconnect() ]); -async function database(program) { +async function database({fix = false, limit = Infinity, batch = 1000}) { try { for (const verification of databaseVerifications) { - await verification(program); + await verification({fix, limit, batch}); } } catch (err) { console.error(`Failed to process all the ${databaseVerifications.length} verifications`, err); @@ -36,6 +36,7 @@ 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); diff --git a/bin/verifications/database/comments.js b/bin/verifications/database/comments.js index 1f9a81951..e80e8ced6 100644 --- a/bin/verifications/database/comments.js +++ b/bin/verifications/database/comments.js @@ -1,16 +1,17 @@ const CommentModel = require('../../../models/comment'); const ActionsService = require('../../../services/actions'); -const {arrayJoinBy} = require('../../../graph/loaders/util'); +const {arrayJoinBy, singleJoinBy} = require('../../../graph/loaders/util'); const sc = require('snake-case'); +const debug = require('debug')('talk:cli:verify'); const getBatch = async (limit, offset) => CommentModel .find({}) - .select({'id': 1, 'action_counts': 1}) + .select({'id': 1, 'action_counts': 1, 'reply_count': 1}) .limit(limit) .skip(offset) .sort('created_at'); -module.exports = async ({fix = false, batch = 1000}) => { +module.exports = async ({fix, limit, batch}) => { let operations = []; // Count how many comments there are to process. @@ -20,7 +21,7 @@ module.exports = async ({fix = false, batch = 1000}) => { let comments = []; let commentIDs = []; - console.log(`Processing ${totalCount} comments...`); + debug(`Processing ${totalCount} comments...`); // Keep processing documents until there are is none left. while (offset < totalCount) { @@ -29,6 +30,31 @@ module.exports = async ({fix = false, batch = 1000}) => { 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)); + // Get their action summaries. let allActionSummaries = await ActionsService .getActionSummaries(commentIDs) @@ -38,17 +64,26 @@ module.exports = async ({fix = false, batch = 1000}) => { for (let i = 0; i < comments.length; i++) { let comment = comments[i]; let actionSummaries = allActionSummaries[i]; + let replyCount = allReplyCounts[i]; // And check to see if the action summaries we just computed match what is // 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, + }); + } + // First we process all the group id's. for (let actionSummary of actionSummaries) { if (actionSummary.group_id === null) { continue; } + // And we generate the group id. const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase()); const GROUP_ID = sc(actionSummary.group_id.toLowerCase()); @@ -56,6 +91,8 @@ module.exports = async ({fix = false, batch = 1000}) => { 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. @@ -82,7 +119,7 @@ module.exports = async ({fix = false, batch = 1000}) => { return acc; }, {}); - Object.keys(groupedActionSummaries).forEach((ACTION_COUNT_FIELD) => { + for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) { const count = groupedActionSummaries[ACTION_COUNT_FIELD]; // Check that the action summaries match the cached counts. @@ -93,7 +130,7 @@ module.exports = async ({fix = false, batch = 1000}) => { [`action_counts.${ACTION_COUNT_FIELD}`]: count, }); } - }); + } // If this comment has action summaries that should be updated, then // perform an update! @@ -111,29 +148,44 @@ module.exports = async ({fix = false, batch = 1000}) => { } } - console.log(`Processed batch of ${comments.length} comments.`); + 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; - console.log(`Processed all ${totalCount} comments.`); - console.log(`${OPERATIONS_LENGTH} documents need fixing.`); + if (limit < Infinity && offset + comments.length < totalCount) { + debug(`Processed ${offset + comments.length}/${totalCount} comments because we reached the update limit of ${limit}.`); + debug(`Fixing ${OPERATIONS_LENGTH} documents.`); + } else { + debug(`Processed all ${totalCount} comments.`); + debug(`${OPERATIONS_LENGTH} documents need fixing.`); + } // If fix was enabled, execute the batch writes. if (OPERATIONS_LENGTH > 0) { if (fix) { - console.log(`Fixing ${OPERATIONS_LENGTH} documents...`); + debug(`Fixing ${OPERATIONS_LENGTH} documents...`); while (operations.length) { let batchOperations = operations.splice(0, batch); let result = await CommentModel.collection.bulkWrite(batchOperations); - console.log(`Fixed batch of ${result.modifiedCount} documents.`); + debug(`Fixed batch of ${result.modifiedCount} documents.`); } - console.log(`Fixed all ${OPERATIONS_LENGTH} documents.`); + debug(`Applied all ${OPERATIONS_LENGTH} fixes.`); } else { console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors'); } diff --git a/models/comment.js b/models/comment.js index e30cd091f..ac1f1bcb1 100644 --- a/models/comment.js +++ b/models/comment.js @@ -71,7 +71,10 @@ const CommentSchema = new Schema({ parent_id: String, // The number of replies to this comment directly. - reply_count: Number, + reply_count: { + type: Number, + default: 0, + }, // Counts to store related to actions taken on the given comment. action_counts: { From 7da7db1eb4e6f698080b594264597901e7004d5b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 21 Aug 2017 10:57:57 -0600 Subject: [PATCH 021/121] package upgrades --- package.json | 51 +++--- webpack.config.js | 5 +- yarn.lock | 451 +++++++++++++++++++++++++++++++++------------- 3 files changed, 350 insertions(+), 157 deletions(-) diff --git a/package.json b/package.json index 6e57aa221..63fb24de3 100644 --- a/package.json +++ b/package.json @@ -78,26 +78,26 @@ "cli-table": "^0.3.1", "clipboard": "^1.7.1", "colors": "^1.1.2", - "commander": "^2.9.0", + "commander": "^2.11.0", "common-tags": "^1.4.0", - "compression": "^1.6.2", + "compression": "^1.7.0", "compression-webpack-plugin": "^0.4.0", "cookie-parser": "^1.4.3", "copy-webpack-plugin": "^4.0.0", "cross-spawn": "^5.1.0", "css-loader": "^0.27.3", "dataloader": "^1.3.0", - "debug": "^2.6.3", + "debug": "^3.0.0", "dialog-polyfill": "^0.4.4", "dotenv": "^4.0.0", - "ejs": "^2.5.6", + "ejs": "^2.5.7", "env-rewrite": "^1.0.2", "eventemitter2": "^4.1.2", "exports-loader": "^0.6.4", - "express": "^4.15.2", + "express": "^4.15.4", "file-loader": "^0.11.2", - "form-data": "^2.1.2", - "fs-extra": "^3.0.1", + "form-data": "^2.2.0", + "fs-extra": "^4.0.1", "gql-merge": "^0.0.4", "graphql": "^0.9.1", "graphql-docs": "^0.2.0", @@ -108,35 +108,35 @@ "graphql-tag": "^1.2.3", "graphql-tools": "^0.10.1", "hammerjs": "^2.0.8", - "helmet": "^3.5.0", + "helmet": "^3.8.1", "history": "^3.0.0", "immutability-helper": "^2.2.0", "imports-loader": "^0.7.1", - "inquirer": "^3.2.1", + "inquirer": "^3.2.2", "istanbul": "^1.1.0-alpha.1", - "joi": "^10.4.1", + "joi": "^10.6.0", "json-loader": "^0.5.4", - "jsonwebtoken": "^7.3.0", + "jsonwebtoken": "^7.4.3", "jwt-decode": "^2.2.0", "keymaster": "^1.6.2", - "kue": "^0.11.5", - "license-webpack-plugin": "^0.4.2", + "kue": "^0.11.6", + "license-webpack-plugin": "^1.0.0", "linkify-it": "^2.0.3", "lodash": "^4.16.6", "marked": "^0.3.6", "material-design-lite": "^1.2.1", - "metascraper": "^1.0.6", + "metascraper": "^1.0.7", "minimist": "^1.2.0", - "mongoose": "^4.9.8", - "morgan": "^1.8.1", + "mongoose": "^4.11.7", + "morgan": "^1.8.2", "ms": "^2.0.0", "murmurhash-js": "^1.0.0", - "natural": "^0.5.0", - "node-emoji": "^1.5.1", - "node-fetch": "^1.6.3", + "natural": "^0.5.4", + "node-emoji": "^1.8.1", + "node-fetch": "^1.7.2", "nodemailer": "^2.6.4", - "passport": "^0.3.2", - "passport-jwt": "^2.2.1", + "passport": "^0.4.0", + "passport-jwt": "^3.0.0", "passport-local": "^1.0.0", "postcss-loader": "^1.3.3", "postcss-modules": "^0.5.2", @@ -160,11 +160,11 @@ "react-toastify": "^1.5.0", "react-transition-group": "^1.1.3", "recompose": "^0.23.1", - "redis": "^2.7.1", + "redis": "^2.8.0", "redux": "^3.6.0", "redux-thunk": "^2.1.0", - "resolve": "^1.3.2", - "semver": "^5.3.0", + "resolve": "^1.4.0", + "semver": "^5.4.1", "simplemde": "^1.11.2", "smoothscroll-polyfill": "^0.3.5", "snake-case": "^2.1.0", @@ -174,8 +174,9 @@ "timekeeper": "^1.0.0", "url-loader": "^0.5.9", "url-search-params": "^0.9.0", - "uuid": "^3.0.1", + "uuid": "^3.1.0", "webpack": "^2.3.1", + "webpack-sources": "^1.0.1", "yaml-loader": "^0.4.0", "yamljs": "^0.2.10" }, diff --git a/webpack.config.js b/webpack.config.js index 873d20472..c555333bc 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,7 +5,7 @@ const autoprefixer = require('autoprefixer'); const precss = require('precss'); const _ = require('lodash'); const Copy = require('copy-webpack-plugin'); -const LicenseWebpackPlugin = require('license-webpack-plugin'); +const {LicenseWebpackPlugin} = require('license-webpack-plugin'); const webpack = require('webpack'); const debug = require('debug')('talk:webpack'); @@ -90,8 +90,7 @@ const config = { plugins: [ new LicenseWebpackPlugin({ pattern: /^(MIT|ISC|BSD.*)$/, - addUrl: true, - suppressErrors: true + suppressErrors: true, }), new Copy([ ...buildEmbeds.map((embed) => ({ diff --git a/yarn.lock b/yarn.lock index 0420dc269..2141ef7ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1180,14 +1180,14 @@ builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" -bytes@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070" - bytes@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" +bytes@2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -1573,6 +1573,10 @@ commander@2.9.0, commander@^2.8.1, commander@^2.9.0: dependencies: graceful-readlink ">= 1.0.0" +commander@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + commander@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.1.0.tgz#d121bbae860d9992a3d517ba96f56588e47c6781" @@ -1591,11 +1595,11 @@ component-emitter@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" -compressible@~2.0.8: - version "2.0.10" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd" +compressible@~2.0.10: + version "2.0.11" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.11.tgz#16718a75de283ed8e604041625a2064586797d8a" dependencies: - mime-db ">= 1.27.0 < 2" + mime-db ">= 1.29.0 < 2" compression-webpack-plugin@^0.4.0: version "0.4.0" @@ -1606,16 +1610,17 @@ compression-webpack-plugin@^0.4.0: optionalDependencies: node-zopfli "^2.0.0" -compression@^1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.6.2.tgz#cceb121ecc9d09c52d7ad0c3350ea93ddd402bc3" +compression@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.0.tgz#030c9f198f1643a057d776a738e922da4373012d" dependencies: accepts "~1.3.3" - bytes "2.3.0" - compressible "~2.0.8" - debug "~2.2.0" + bytes "2.5.0" + compressible "~2.0.10" + debug "2.6.8" on-headers "~1.0.1" - vary "~1.1.0" + safe-buffer "5.1.1" + vary "~1.1.1" concat-map@0.0.1: version "0.0.1" @@ -1642,12 +1647,12 @@ configstore@^1.0.0: write-file-atomic "^1.1.2" xdg-basedir "^2.0.0" -connect@3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.0.tgz#f09a4f7dcd17324b663b725c815bdb1c4158a46e" +connect@3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.2.tgz#694e8d20681bfe490282c8ab886be98f09f42fe7" dependencies: - debug "2.6.1" - finalhandler "1.0.0" + debug "2.6.7" + finalhandler "1.0.3" parseurl "~1.3.1" utils-merge "1.0.0" @@ -1747,7 +1752,7 @@ core-js@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2007,7 +2012,7 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" -debug@*, debug@2.6.4, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.3: +debug@*, debug@2.6.4, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3: version "2.6.4" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.4.tgz#7586a9b3c39741c0282ae33445c4e8ac74734fe0" dependencies: @@ -2037,6 +2042,24 @@ debug@2.6.1: dependencies: ms "0.7.2" +debug@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" + dependencies: + ms "2.0.0" + +debug@2.6.8: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +debug@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.0.tgz#1d2feae53349047b08b264ec41906ba17a8516e4" + dependencies: + ms "2.0.0" + debug@~0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" @@ -2114,6 +2137,10 @@ depd@1.1.0, depd@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" +depd@1.1.1, depd@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + deprecated-decorator@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" @@ -2268,9 +2295,9 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" -ejs@^2.5.6: - version "2.5.6" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88" +ejs@^2.5.7: + version "2.5.7" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a" electron-to-chromium@^1.2.7: version "1.3.8" @@ -2688,6 +2715,10 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" +expect-ct@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/expect-ct/-/expect-ct-0.1.0.tgz#52735678de18530890d8d7b95f0ac63640958094" + exports-loader@^0.6.4: version "0.6.4" resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-0.6.4.tgz#d70fc6121975b35fc12830cf52754be2740fc886" @@ -2695,7 +2726,7 @@ exports-loader@^0.6.4: loader-utils "^1.0.2" source-map "0.5.x" -express@^4.12.2, express@^4.15.2: +express@^4.12.2: version "4.15.2" resolved "https://registry.yarnpkg.com/express/-/express-4.15.2.tgz#af107fc148504457f2dca9a6f2571d7129b97b35" dependencies: @@ -2728,6 +2759,39 @@ express@^4.12.2, express@^4.15.2: utils-merge "1.0.0" vary "~1.1.0" +express@^4.15.4: + version "4.15.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.15.4.tgz#032e2253489cf8fce02666beca3d11ed7a2daed1" + dependencies: + accepts "~1.3.3" + array-flatten "1.1.1" + content-disposition "0.5.2" + content-type "~1.0.2" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.8" + depd "~1.1.1" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + finalhandler "~1.0.4" + fresh "0.5.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.1" + path-to-regexp "0.1.7" + proxy-addr "~1.1.5" + qs "6.5.0" + range-parser "~1.2.0" + send "0.15.4" + serve-static "1.12.4" + setprototypeof "1.0.3" + statuses "~1.3.1" + type-is "~1.6.15" + utils-merge "1.0.0" + vary "~1.1.1" + extend@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/extend/-/extend-1.3.0.tgz#d1516fb0ff5624d2ebf9123ea1dac5a1994004f8" @@ -2821,11 +2885,11 @@ fill-range@^2.1.0: repeat-element "^1.1.2" repeat-string "^1.5.2" -finalhandler@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.0.tgz#b5691c2c0912092f18ac23e9416bde5cd7dc6755" +finalhandler@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89" dependencies: - debug "2.6.1" + debug "2.6.7" encodeurl "~1.0.1" escape-html "~1.0.3" on-finished "~2.3.0" @@ -2845,6 +2909,18 @@ finalhandler@~1.0.0: statuses "~1.3.1" unpipe "~1.0.0" +finalhandler@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.4.tgz#18574f2e7c4b98b8ae3b230c21f201f31bdb3fb7" + dependencies: + debug "2.6.8" + encodeurl "~1.0.1" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.1" + statuses "~1.3.1" + unpipe "~1.0.0" + find-cache-dir@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" @@ -2914,7 +2990,15 @@ form-data@^0.2.0: combined-stream "~0.0.4" mime-types "~2.0.3" -form-data@^2.1.2, form-data@~2.1.1: +form-data@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.2.0.tgz#9a5e3b9295f980b2623cf64fa238b14cebca707b" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +form-data@~2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" dependencies: @@ -2967,9 +3051,9 @@ fs-extra@^0.26.4: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-extra@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" +fs-extra@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.1.tgz#7fc0c6c8957f983f57f306a24e5b9ddd8d0dd880" dependencies: graceful-fs "^4.1.2" jsonfile "^3.0.0" @@ -3421,28 +3505,29 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" -helmet-csp@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.4.0.tgz#7e53a157167a0645aadd7177d12ae6c605c1842e" +helmet-csp@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.5.1.tgz#5f3deec8f922fa7e074dbc3987c168a50573c36d" dependencies: camelize "1.0.0" content-security-policy-builder "1.1.0" dasherize "2.0.0" lodash.reduce "4.6.0" - platform "1.3.3" + platform "1.3.4" -helmet@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.5.0.tgz#e1d6de27d2e3317d3182e00d672df3d0e1e12539" +helmet@^3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.8.1.tgz#bef2b68ffbaa19759e858c19cca7db213bb58b2d" dependencies: - connect "3.6.0" + connect "3.6.2" dns-prefetch-control "0.1.0" dont-sniff-mimetype "1.0.0" + expect-ct "0.1.0" frameguard "3.0.0" - helmet-csp "2.4.0" + helmet-csp "2.5.1" hide-powered-by "1.0.0" hpkp "2.0.0" - hsts "2.0.0" + hsts "2.1.0" ienoopen "1.0.0" nocache "2.0.0" referrer-policy "1.1.0" @@ -3514,11 +3599,9 @@ hr@0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/hr/-/hr-0.1.3.tgz#d9aa30f5929dabfd0b65ba395938a3e184dbcafe" -hsts@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.0.0.tgz#a52234c6070decf214b2b6b70bb144d07e4776c7" - dependencies: - core-util-is "1.0.2" +hsts@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.1.0.tgz#cbd6c918a2385fee1dd5680bfb2b3a194c0121cc" html-comment-regex@^1.1.0: version "1.1.1" @@ -3554,6 +3637,15 @@ http-errors@~1.6.1: setprototypeof "1.0.3" statuses ">= 1.3.1 < 2" +http-errors@~1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + dependencies: + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" @@ -3708,9 +3800,9 @@ inquirer@0.8.2: rx "^2.4.3" through "^2.3.6" -inquirer@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.1.tgz#06ceb0f540f45ca548c17d6840959878265fa175" +inquirer@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.2.tgz#c2aaede1507cc54d826818737742d621bef2e823" dependencies: ansi-escapes "^2.0.0" chalk "^2.0.0" @@ -3753,6 +3845,10 @@ ipaddr.js@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" +ipaddr.js@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" + is-absolute-url@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" @@ -4108,9 +4204,9 @@ jodid25519@^1.0.0: dependencies: jsbn "~0.1.0" -joi@^10.4.1: - version "10.4.1" - resolved "https://registry.yarnpkg.com/joi/-/joi-10.4.1.tgz#a2fca1f0d603d1b843f2c1e086b52461f6be1f36" +joi@^10.6.0: + version "10.6.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-10.6.0.tgz#52587f02d52b8b75cdb0c74f0b164a191a0e1fc2" dependencies: hoek "4.x.x" isemail "2.x.x" @@ -4240,7 +4336,7 @@ jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" -jsonwebtoken@^7.0.0, jsonwebtoken@^7.3.0: +jsonwebtoken@^7.0.0: version "7.4.0" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.4.0.tgz#515bf2bba070ec615bad97fd2e945027eb476946" dependencies: @@ -4250,6 +4346,16 @@ jsonwebtoken@^7.0.0, jsonwebtoken@^7.3.0: ms "^0.7.1" xtend "^4.0.1" +jsonwebtoken@^7.4.3: + version "7.4.3" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.4.3.tgz#77f5021de058b605a1783fa1283e99812e645638" + dependencies: + joi "^6.10.1" + jws "^3.1.4" + lodash.once "^4.0.0" + ms "^2.0.0" + xtend "^4.0.1" + jsprim@^1.2.2: version "1.4.0" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" @@ -4295,9 +4401,9 @@ jwt-decode@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" -kareem@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/kareem/-/kareem-1.4.1.tgz#ed76200044fa041ef32b4da8261e2553f1173531" +kareem@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-1.5.0.tgz#e3e4101d9dcfde299769daf4b4db64d895d17448" keymaster@^1.6.2: version "1.6.2" @@ -4315,9 +4421,9 @@ klaw@^1.0.0: optionalDependencies: graceful-fs "^4.1.9" -kue@^0.11.5: - version "0.11.5" - resolved "https://registry.yarnpkg.com/kue/-/kue-0.11.5.tgz#ac2fa8c8087e4d4c48e5c9c2d4b17687eed0e1df" +kue@^0.11.6: + version "0.11.6" + resolved "https://registry.yarnpkg.com/kue/-/kue-0.11.6.tgz#5b76916bcedd56636a107861471c63c94611860a" dependencies: body-parser "^1.12.2" express "^4.12.2" @@ -4382,11 +4488,11 @@ libqp@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/libqp/-/libqp-1.1.0.tgz#f5e6e06ad74b794fb5b5b66988bf728ef1dedbe8" -license-webpack-plugin@^0.4.2: - version "0.4.3" - resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-0.4.3.tgz#f9d88d4ebc04407a0061e8ccac26571f88e51a16" +license-webpack-plugin@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-1.0.0.tgz#9515229075bacce8ec420cadf99a54a5f78cc7df" dependencies: - object-assign "^4.1.0" + ejs "^2.5.7" linkify-it@^1.2.0: version "1.2.4" @@ -4593,6 +4699,10 @@ lodash.some@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" +lodash.toarray@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -4702,9 +4812,9 @@ merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" -metascraper@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/metascraper/-/metascraper-1.0.6.tgz#d7f9ec99bda3ca6870ee5b730c1b44f41d2d7fa4" +metascraper@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/metascraper/-/metascraper-1.0.7.tgz#2343f9108f34e4d2b55b7b4f543c96dfc52e1478" dependencies: cheerio "^0.20.0" chrono-node "^1.2.3" @@ -4742,14 +4852,18 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.27.0 < 2", mime-db@~1.27.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" +"mime-db@>= 1.29.0 < 2": + version "1.29.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" mime-db@~1.12.0: version "1.12.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.12.0.tgz#3d0c63180f458eb10d325aaa37d7c58ae312e9d7" +mime-db@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" + mime-types@^2.1.10, mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: version "2.1.15" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" @@ -4831,62 +4945,62 @@ moment@2.x.x, moment@^2.10.3: version "2.18.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" -mongodb-core@2.1.10: - version "2.1.10" - resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-2.1.10.tgz#eb290681d196d3346a492161aa2ea0905e63151b" +mongodb-core@2.1.15: + version "2.1.15" + resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-2.1.15.tgz#841f53b87ffff4c7458189c35c8ae827e1169764" dependencies: bson "~1.0.4" require_optional "~1.0.0" -mongodb@2.2.26: - version "2.2.26" - resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-2.2.26.tgz#1bd50c557c277c98e1a05da38c9839c4922b034a" +mongodb@2.2.31: + version "2.2.31" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-2.2.31.tgz#1940445c661e19217bb3bf8245d9854aaef548db" dependencies: es6-promise "3.2.1" - mongodb-core "2.1.10" + mongodb-core "2.1.15" readable-stream "2.2.7" -mongoose@^4.9.8: - version "4.9.9" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-4.9.9.tgz#8671fbe06c945f55fba7ad03797bc02f19516762" +mongoose@^4.11.7: + version "4.11.7" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-4.11.7.tgz#39e6f60880146eadf2efc37257e628fad9ceed8d" dependencies: async "2.1.4" bson "~1.0.4" hooks-fixed "2.0.0" - kareem "1.4.1" - mongodb "2.2.26" - mpath "0.2.1" + kareem "1.5.0" + mongodb "2.2.31" + mpath "0.3.0" mpromise "0.5.5" - mquery "2.3.0" - ms "0.7.2" - muri "1.2.1" + mquery "2.3.1" + ms "2.0.0" + muri "1.2.2" regexp-clone "0.0.1" sliced "1.0.1" -morgan@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.8.1.tgz#f93023d3887bd27b78dfd6023cea7892ee27a4b1" +morgan@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.8.2.tgz#784ac7734e4a453a9c6e6e8680a9329275c8b687" dependencies: basic-auth "~1.1.0" - debug "2.6.1" + debug "2.6.8" depd "~1.1.0" on-finished "~2.3.0" on-headers "~1.0.1" -mpath@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.2.1.tgz#3a4e829359801de96309c27a6b2e102e89f9e96e" +mpath@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.3.0.tgz#7a58f789e9b5fd3c94520634157960f26bd5ef44" mpromise@0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/mpromise/-/mpromise-0.5.5.tgz#f5b24259d763acc2257b0a0c8c6d866fd51732e6" -mquery@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/mquery/-/mquery-2.3.0.tgz#3d1717ad8958d0c99e42ea2461a109f3e5f3e458" +mquery@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-2.3.1.tgz#9ab36749714800ff0bb53a681ce4bc4d5f07c87b" dependencies: bluebird "2.10.2" - debug "2.2.0" + debug "2.6.8" regexp-clone "0.0.1" sliced "0.0.5" @@ -4902,13 +5016,13 @@ ms@0.7.3, ms@^0.7.1: version "0.7.3" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.3.tgz#708155a5e44e33f5fd0fc53e81d0d40a91be1fff" -ms@^2.0.0: +ms@2.0.0, ms@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -muri@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/muri/-/muri-1.2.1.tgz#ec7ea5ce6ca6a523eb1ab35bacda5fa816c9aa3c" +muri@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/muri/-/muri-1.2.2.tgz#63198132650db08a04cc79ccd00dd389afd2631c" murmurhash-js@^1.0.0: version "1.0.0" @@ -4946,13 +5060,14 @@ natural@^0.2.0: sylvester ">= 0.0.12" underscore ">=1.3.1" -natural@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/natural/-/natural-0.5.1.tgz#99a5c8b7f1be0b2d7f9a7803f596f8cc846b5aaf" +natural@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/natural/-/natural-0.5.4.tgz#ace41c1655daca2912dfbf99ad7b05314e205f54" dependencies: apparatus ">= 0.0.9" sylvester ">= 0.0.12" underscore ">=1.3.1" + optionalDependencies: webworker-threads ">=0.6.2" negotiator@0.6.1: @@ -4996,19 +5111,26 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" -node-emoji@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.5.1.tgz#fd918e412769bf8c448051238233840b2aff16a1" +node-emoji@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.8.1.tgz#6eec6bfb07421e2148c75c6bba72421f8530a826" dependencies: - string.prototype.codepointat "^0.2.0" + lodash.toarray "^4.4.0" -node-fetch@^1.0.1, node-fetch@^1.6.3: +node-fetch@^1.0.1: version "1.6.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.6.3.tgz#dc234edd6489982d58e8f0db4f695029abcd8c04" dependencies: encoding "^0.1.11" is-stream "^1.0.1" +node-fetch@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.2.tgz#c54e9aac57e432875233525f3c891c4159ffefd7" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + node-libs-browser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646" @@ -5398,9 +5520,9 @@ parseurl@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" -passport-jwt@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-2.2.1.tgz#0e004c94071319d673d9d9bcfd1574a868011527" +passport-jwt@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-3.0.0.tgz#7d9e4ff0fa0522108540ce1fcfa83bbd8faa29c9" dependencies: jsonwebtoken "^7.0.0" passport-strategy "^1.0.0" @@ -5415,9 +5537,9 @@ passport-strategy@1.x.x, passport-strategy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" -passport@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/passport/-/passport-0.3.2.tgz#9dd009f915e8fe095b0124a01b8f82da07510102" +passport@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.0.tgz#c5095691347bd5ad3b5e180238c3914d16f05811" dependencies: passport-strategy "1.x.x" pause "0.0.1" @@ -5518,9 +5640,9 @@ pkg-up@^1.0.0: dependencies: find-up "^1.0.0" -platform@1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.3.tgz#646c77011899870b6a0903e75e997e8e51da7461" +platform@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.4.tgz#6f0fb17edaaa48f21442b3a975c063130f1c3ebd" pluralize@^1.2.1: version "1.2.1" @@ -6066,6 +6188,13 @@ proxy-addr@~1.1.3: forwarded "~0.1.0" ipaddr.js "1.3.0" +proxy-addr@~1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918" + dependencies: + forwarded "~0.1.0" + ipaddr.js "1.4.0" + prr@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" @@ -6215,6 +6344,10 @@ qs@6.4.0, qs@^6.1.0, qs@^6.2.0, qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +qs@6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49" + query-string@^4.1.0, query-string@^4.2.2: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -6504,7 +6637,7 @@ redis-commands@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.1.tgz#81d826f45fa9c8b2011f4cd7a0fe597d241d442b" -redis-parser@^2.0.0, redis-parser@^2.5.0: +redis-parser@^2.0.0, redis-parser@^2.5.0, redis-parser@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" @@ -6512,7 +6645,7 @@ redis@^0.12.1: version "0.12.1" resolved "https://registry.yarnpkg.com/redis/-/redis-0.12.1.tgz#64df76ad0fc8acebaebd2a0645e8a48fac49185e" -redis@^2.6.3, redis@^2.7.1: +redis@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/redis/-/redis-2.7.1.tgz#7d56f7875b98b20410b71539f1d878ed58ebf46a" dependencies: @@ -6520,6 +6653,14 @@ redis@^2.6.3, redis@^2.7.1: redis-commands "^1.2.0" redis-parser "^2.5.0" +redis@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02" + dependencies: + double-ended-queue "^2.1.0-0" + redis-commands "^1.2.0" + redis-parser "^2.6.0" + redis@~2.6.0-2: version "2.6.5" resolved "https://registry.yarnpkg.com/redis/-/redis-2.6.5.tgz#87c1eff4a489f94b70871f3d08b6988f23a95687" @@ -6714,12 +6855,18 @@ resolve-from@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.1.7: version "1.3.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" dependencies: path-parse "^1.0.5" +resolve@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86" + 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" @@ -6788,6 +6935,10 @@ 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: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + safe-buffer@^5.0.1, safe-buffer@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" @@ -6826,6 +6977,10 @@ semver@5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.0.tgz#85f2cf8550465c4df000cf7d86f6b054106ab9e5" +semver@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + send@0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/send/-/send-0.15.1.tgz#8a02354c26e6f5cca700065f5f0cdeba90ec7b5f" @@ -6844,6 +6999,24 @@ send@0.15.1: range-parser "~1.2.0" statuses "~1.3.1" +send@0.15.4: + version "0.15.4" + resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9" + dependencies: + debug "2.6.8" + depd "~1.1.1" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + fresh "0.5.0" + http-errors "~1.6.2" + mime "1.3.4" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + serve-static@1.12.1: version "1.12.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.1.tgz#7443a965e3ced647aceb5639fa06bf4d1bbe0039" @@ -6853,6 +7026,15 @@ serve-static@1.12.1: parseurl "~1.3.1" send "0.15.1" +serve-static@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.4.tgz#9b6aa98eeb7253c4eedc4c1f6fdbca609901a961" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.15.4" + set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -7008,6 +7190,10 @@ source-list-map@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-1.1.1.tgz#1a33ac210ca144d1e561f906ebccab5669ff4cb4" +source-list-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" + source-map-support@^0.4.2: version "0.4.14" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.14.tgz#9d4463772598b86271b4f523f6c1f4e02a7d6aef" @@ -7143,10 +7329,6 @@ string-width@^2.0.0, string-width@^2.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string.prototype.codepointat@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78" - string_decoder@^0.10.25, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -7504,7 +7686,7 @@ type-detect@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea" -type-is@~1.6.14: +type-is@~1.6.14, type-is@~1.6.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" dependencies: @@ -7644,6 +7826,10 @@ uuid@^3.0.0, uuid@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" +uuid@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + v8flags@^2.0.10: version "2.1.1" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" @@ -7665,7 +7851,7 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" -vary@~1.1.0: +vary@~1.1.0, vary@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" @@ -7725,6 +7911,13 @@ webpack-sources@^0.2.3: source-list-map "^1.1.1" source-map "~0.5.3" +webpack-sources@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" + dependencies: + source-list-map "^2.0.0" + source-map "~0.5.3" + webpack@^2.3.1: version "2.4.1" resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.4.1.tgz#15a91dbe34966d8a4b99c7d656efd92a2e5a6f6a" From cea6f7b8e805d62da0651d84b621bdede2892fd3 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 21 Aug 2017 14:26:22 -0300 Subject: [PATCH 022/121] Deleting Reference to FakeComment --- .../talk-plugin-auth/client/components/FakeComment.css | 9 +++++---- .../talk-plugin-auth/client/components/FakeComment.js | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/talk-plugin-auth/client/components/FakeComment.css b/plugins/talk-plugin-auth/client/components/FakeComment.css index 8b5521aea..bc751b115 100644 --- a/plugins/talk-plugin-auth/client/components/FakeComment.css +++ b/plugins/talk-plugin-auth/client/components/FakeComment.css @@ -6,10 +6,6 @@ position: relative; } -.body { - -} - .footer { display: flex; justify-content: space-between; @@ -35,3 +31,8 @@ padding: 0 2px 0 5px; vertical-align: middle; } + +.authorName { + margin-right: 5px; + font-weight: bold; +} \ No newline at end of file diff --git a/plugins/talk-plugin-auth/client/components/FakeComment.js b/plugins/talk-plugin-auth/client/components/FakeComment.js index eefada587..9f1d90ae7 100644 --- a/plugins/talk-plugin-auth/client/components/FakeComment.js +++ b/plugins/talk-plugin-auth/client/components/FakeComment.js @@ -2,13 +2,14 @@ import React from 'react'; import t from 'coral-framework/services/i18n'; import {ReplyButton} from 'talk-plugin-replies'; import PubDate from 'talk-plugin-pubdate/PubDate'; -import AuthorName from 'talk-plugin-author-name/AuthorName'; import styles from './FakeComment.css'; import {Icon} from 'plugin-api/beta/client/components/ui'; export const FakeComment = ({username, created_at, body}) => (
- + + {username} + ;
{body} From 15111c81a278fdd58f37009476c6a6471eed5bfa Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 21 Aug 2017 11:32:16 -0600 Subject: [PATCH 023/121] improved messaging --- config.js | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/config.js b/config.js index 11c8b5077..0a9912654 100644 --- a/config.js +++ b/config.js @@ -9,6 +9,7 @@ require('env-rewrite').rewrite(); const uniq = require('lodash/uniq'); const ms = require('ms'); +const debug = require('debug')('talk:config'); //============================================================================== // CONFIG INITIALIZATION @@ -240,33 +241,23 @@ CONFIG.RECAPTCHA_ENABLED = CONFIG.RECAPTCHA_SECRET.length > 0 && CONFIG.RECAPTCHA_PUBLIC && CONFIG.RECAPTCHA_PUBLIC.length > 0; -if (!CONFIG.RECAPTCHA_ENABLED) { - console.warn( - 'Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.' - ); -} + +debug(`reCAPTCHA is ${CONFIG.RECAPTCHA_ENABLED ? 'enabled' : 'disabled, required config is not present'}`); //------------------------------------------------------------------------------ // SMTP Server configuration //------------------------------------------------------------------------------ -{ - const requiredProps = [ - 'SMTP_FROM_ADDRESS', - 'SMTP_USERNAME', - 'SMTP_PASSWORD', - 'SMTP_HOST' - ]; +CONFIG.EMAIL_ENABLED = + CONFIG.SMTP_FROM_ADDRESS && + CONFIG.SMTP_FROM_ADDRESS.length > 0 && + CONFIG.SMTP_USERNAME && + CONFIG.SMTP_USERNAME.length > 0 && + CONFIG.SMTP_PASSWORD && + CONFIG.SMTP_PASSWORD.length > 0 && + CONFIG.SMTP_HOST && + CONFIG.SMTP_HOST.length > 0; - if (requiredProps.some((prop) => !CONFIG[prop])) { - console.warn( - `${requiredProps - .map((v) => `TALK_${v}`) - .join( - ', ' - )} should be defined in the environment if you would like to send password reset emails from Talk` - ); - } -} +debug(`Email is ${CONFIG.EMAIL_ENABLED ? 'enabled' : 'disabled, required config is not present'}`); module.exports = CONFIG; From 19dfe86118f8c44571645730730242fe455d84bc Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 21 Aug 2017 14:53:24 -0300 Subject: [PATCH 024/121] Updates --- client/coral-embed-stream/src/components/AuthorName.js | 9 --------- client/coral-embed-stream/src/components/Comment.css | 5 +++++ client/coral-embed-stream/src/components/Comment.js | 6 +++--- client/coral-embed-stream/style/default.css | 4 ---- .../components/CommentAuthorName.css} | 1 - client/coral-framework/components/CommentAuthorName.js | 9 +++++++++ client/coral-framework/components/index.js | 1 + plugin-api/beta/client/components/index.js | 1 + .../client/components/Comment.css | 2 +- .../client/components/Comment.js | 6 ++++-- 10 files changed, 24 insertions(+), 20 deletions(-) delete mode 100644 client/coral-embed-stream/src/components/AuthorName.js rename client/{coral-embed-stream/src/components/AuthorName.css => coral-framework/components/CommentAuthorName.css} (63%) create mode 100644 client/coral-framework/components/CommentAuthorName.js diff --git a/client/coral-embed-stream/src/components/AuthorName.js b/client/coral-embed-stream/src/components/AuthorName.js deleted file mode 100644 index 96dc52aee..000000000 --- a/client/coral-embed-stream/src/components/AuthorName.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import styles from './AuthorName.css'; - -const AuthorName = ({comment}) => - - {comment.user.username} - ; - -export default AuthorName; diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index 32a034f51..578df2d80 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -163,6 +163,7 @@ .header { display: flex; align-items: center; + margin: 10px 0; } .content { @@ -171,3 +172,7 @@ .footer { min-height: 10px; } + +.username { + margin-right: 5px; +} \ No newline at end of file diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 9434339e8..85dee9392 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -25,7 +25,7 @@ import {EditableCommentContent} from './EditableCommentContent'; import {getActionSummary, iPerformedThisAction, forEachError, isCommentActive, getShallowChanges} from 'coral-framework/utils'; import t from 'coral-framework/services/i18n'; import CommentContainer from '../containers/Comment'; -import AuthorName from './AuthorName'; +import {CommentAuthorName} from 'coral-framework/components'; const isStaff = (tags) => !tags.every((t) => t.tag.name !== 'STAFF'); const hasTag = (tags, lookupTag) => !!tags.filter((t) => t.tag.name === lookupTag).length; @@ -438,9 +438,9 @@ export default class Comment extends React.Component {
diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 3fc45f952..d4078bf27 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -238,10 +238,6 @@ body { line-height: 1.3; } -.talk-stream-comment-header { - margin: 10px 0; -} - /* Tag Labels */ .talk-plugin-tag-label { diff --git a/client/coral-embed-stream/src/components/AuthorName.css b/client/coral-framework/components/CommentAuthorName.css similarity index 63% rename from client/coral-embed-stream/src/components/AuthorName.css rename to client/coral-framework/components/CommentAuthorName.css index 41771bf49..c06360000 100644 --- a/client/coral-embed-stream/src/components/AuthorName.css +++ b/client/coral-framework/components/CommentAuthorName.css @@ -1,4 +1,3 @@ .authorName { - margin-right: 5px; font-weight: bold; } \ No newline at end of file diff --git a/client/coral-framework/components/CommentAuthorName.js b/client/coral-framework/components/CommentAuthorName.js new file mode 100644 index 000000000..5a2e60c70 --- /dev/null +++ b/client/coral-framework/components/CommentAuthorName.js @@ -0,0 +1,9 @@ +import React from 'react'; +import styles from './CommentAuthorName.css'; + +const CommentAuthorName = ({comment}) => + + {comment.user.username} + ; + +export default CommentAuthorName; diff --git a/client/coral-framework/components/index.js b/client/coral-framework/components/index.js index b7aaef665..8ad08aa09 100644 --- a/client/coral-framework/components/index.js +++ b/client/coral-framework/components/index.js @@ -1 +1,2 @@ export {default as Slot} from './Slot'; +export {default as CommentAuthorName} from './CommentAuthorName'; diff --git a/plugin-api/beta/client/components/index.js b/plugin-api/beta/client/components/index.js index 6466b9ffc..8b0448f4e 100644 --- a/plugin-api/beta/client/components/index.js +++ b/plugin-api/beta/client/components/index.js @@ -1,2 +1,3 @@ export {Slot} from 'coral-framework/components'; export {default as ClickOutside} from 'coral-framework/components/ClickOutside'; +export {default as CommentAuthorName} from 'coral-framework/components/CommentAuthorName'; diff --git a/plugins/talk-plugin-featured-comments/client/components/Comment.css b/plugins/talk-plugin-featured-comments/client/components/Comment.css index 36f3e790e..b800401e3 100644 --- a/plugins/talk-plugin-featured-comments/client/components/Comment.css +++ b/plugins/talk-plugin-featured-comments/client/components/Comment.css @@ -64,4 +64,4 @@ .actionsContainer { text-align: right; -} +} \ No newline at end of file diff --git a/plugins/talk-plugin-featured-comments/client/components/Comment.js b/plugins/talk-plugin-featured-comments/client/components/Comment.js index ddb3b223a..ada7ae7b6 100644 --- a/plugins/talk-plugin-featured-comments/client/components/Comment.js +++ b/plugins/talk-plugin-featured-comments/client/components/Comment.js @@ -2,7 +2,7 @@ import React from 'react'; import cn from 'classnames'; import styles from './Comment.css'; import {t, timeago} from 'plugin-api/beta/client/services'; -import {Slot} from 'plugin-api/beta/client/components'; +import {Slot, CommentAuthorName} from 'plugin-api/beta/client/components'; import {Icon} from 'plugin-api/beta/client/components/ui'; import {pluginName} from '../../package.json'; @@ -26,7 +26,9 @@ class Comment extends React.Component { From a540dfe8d4ba499de90bc7c31f092be019f77a2e Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 21 Aug 2017 16:21:39 -0300 Subject: [PATCH 025/121] Adding hasParent on Comment to the Graph --- client/coral-admin/src/routes/Moderation/components/Comment.js | 2 +- client/coral-admin/src/routes/Moderation/containers/Comment.js | 1 + graph/resolvers/comment.js | 3 +++ graph/typeDefs.graphql | 3 +++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index 1335a8c52..71642e3c2 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -69,7 +69,7 @@ class Comment extends React.Component { let selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'; const queryData = {root, comment, asset: comment.asset}; - + console.log(comment); return (
  • Date: Mon, 21 Aug 2017 17:09:26 -0300 Subject: [PATCH 026/121] Adding Reply Badge --- .../routes/Moderation/components/Comment.js | 4 +++- .../Moderation/components/ReplyBadge.js | 11 +++++++++++ .../routes/Moderation/containers/Comment.js | 3 +++ client/coral-ui/components/Badge.css | 19 +++++++++++++++++++ client/coral-ui/components/Badge.js | 13 +++++++++++++ client/coral-ui/index.js | 1 + locales/en.yml | 1 + locales/es.yml | 1 + 8 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 client/coral-admin/src/routes/Moderation/components/ReplyBadge.js create mode 100644 client/coral-ui/components/Badge.css create mode 100644 client/coral-ui/components/Badge.js diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index 71642e3c2..2ca18d928 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -15,6 +15,7 @@ import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighli import IfHasLink from 'coral-admin/src/components/IfHasLink'; import cn from 'classnames'; import {getCommentType} from 'coral-admin/src/utils/comment'; +import ReplyBadge from './ReplyBadge'; import t, {timeago} from 'coral-framework/services/i18n'; @@ -69,7 +70,7 @@ class Comment extends React.Component { let selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'; const queryData = {root, comment, asset: comment.asset}; - console.log(comment); + return (
  • }
    + hasParent ? + + {t('modqueue.reply')} + + : null; + +export default ReplyBadge; diff --git a/client/coral-admin/src/routes/Moderation/containers/Comment.js b/client/coral-admin/src/routes/Moderation/containers/Comment.js index 11b648a0b..cc127ef4b 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Comment.js +++ b/client/coral-admin/src/routes/Moderation/containers/Comment.js @@ -37,6 +37,7 @@ export default withFragments({ count ... on FlagActionSummary { reason + __typename } } actions { @@ -48,7 +49,9 @@ export default withFragments({ id username } + __typename } + __typename } editing { edited diff --git a/client/coral-ui/components/Badge.css b/client/coral-ui/components/Badge.css new file mode 100644 index 000000000..fd725cf14 --- /dev/null +++ b/client/coral-ui/components/Badge.css @@ -0,0 +1,19 @@ +.badge { + display: inline-block; + color: white; + background-color: #3D73D5; + box-sizing: border-box; + padding: 0px 5px; + font-size: 12px; + height: 24px; + letter-spacing: 0.4px; + margin-bottom: 1px; + margin-right: 2px; +} + +.icon { + font-size: 14px; + vertical-align: text-top; + margin: 0; + margin-right: 4px; +} \ No newline at end of file diff --git a/client/coral-ui/components/Badge.js b/client/coral-ui/components/Badge.js new file mode 100644 index 000000000..c1ba8100b --- /dev/null +++ b/client/coral-ui/components/Badge.js @@ -0,0 +1,13 @@ +import React from 'react'; +import styles from './Badge.css'; +import Icon from './Icon'; +import cn from 'classnames'; + +const Badge = ({className, children, icon, props}) => ( + + {icon && } + {children} + +); + +export default Badge; diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js index 8a538d120..8f261bcca 100644 --- a/client/coral-ui/index.js +++ b/client/coral-ui/index.js @@ -26,3 +26,4 @@ export {default as Option} from './components/Option'; export {default as SnackBar} from './components/SnackBar'; export {default as TextArea} from './components/TextArea'; export {default as Drawer} from './components/Drawer'; +export {default as Badge} from './components/Badge'; diff --git a/locales/en.yml b/locales/en.yml index 54d5315d5..2ec0c816c 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -281,6 +281,7 @@ en: prev_comment: "Go to the previous comment" reject: "Reject" rejected: "Rejected" + reply: "Reply" select_stream: "Select Stream" shift_key: "⇧" shortcuts: "Shortcuts" diff --git a/locales/es.yml b/locales/es.yml index 8c3e7927b..877a42841 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -272,6 +272,7 @@ es: prev_comment: "Ir al comentario anterior" reject: "Rechazar" rejected: "rechazado" + reply: "Respuesta" select_stream: "Seleccionar hilo de comentarios" shift_key: ⇧ shortcuts: Atajos From c4026272807602fe35e3d48aa1b7e765bcc9d7e9 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 21 Aug 2017 17:27:32 -0300 Subject: [PATCH 027/121] Adding Reply Badge to User History --- client/coral-admin/src/components/CommentType.css | 5 ++--- .../src/{routes/Moderation => }/components/ReplyBadge.js | 0 client/coral-admin/src/components/UserDetailComment.css | 2 +- client/coral-admin/src/components/UserDetailComment.js | 7 ++++++- client/coral-admin/src/containers/UserDetailComment.js | 1 + .../src/routes/Moderation/components/Comment.js | 2 +- client/coral-ui/components/Badge.css | 9 +++++---- 7 files changed, 16 insertions(+), 10 deletions(-) rename client/coral-admin/src/{routes/Moderation => }/components/ReplyBadge.js (100%) diff --git a/client/coral-admin/src/components/CommentType.css b/client/coral-admin/src/components/CommentType.css index e4851275e..969d9511d 100644 --- a/client/coral-admin/src/components/CommentType.css +++ b/client/coral-admin/src/components/CommentType.css @@ -3,12 +3,11 @@ color: white; background: grey; box-sizing: border-box; - padding: 0px 5px; - border-radius: 2px; + padding: 2px 5px; font-size: 12px; height: 24px; letter-spacing: 0.4px; - margin-bottom: 1px; + line-height: 22px; > i { font-size: 14px; diff --git a/client/coral-admin/src/routes/Moderation/components/ReplyBadge.js b/client/coral-admin/src/components/ReplyBadge.js similarity index 100% rename from client/coral-admin/src/routes/Moderation/components/ReplyBadge.js rename to client/coral-admin/src/components/ReplyBadge.js diff --git a/client/coral-admin/src/components/UserDetailComment.css b/client/coral-admin/src/components/UserDetailComment.css index b042aea39..2dfd61cf3 100644 --- a/client/coral-admin/src/components/UserDetailComment.css +++ b/client/coral-admin/src/components/UserDetailComment.css @@ -55,7 +55,7 @@ position: relative; } -.commentType { +.badgeBar { position: absolute; right: 0px; } diff --git a/client/coral-admin/src/components/UserDetailComment.js b/client/coral-admin/src/components/UserDetailComment.js index 9b4936847..3d92c0365 100644 --- a/client/coral-admin/src/components/UserDetailComment.js +++ b/client/coral-admin/src/components/UserDetailComment.js @@ -3,6 +3,7 @@ import {Link} from 'react-router'; import {Icon} from 'coral-ui'; import FlagBox from './FlagBox'; +import ReplyBadge from './ReplyBadge'; import styles from './UserDetailComment.css'; import CommentType from './CommentType'; import {getActionSummary} from 'coral-framework/utils'; @@ -56,7 +57,11 @@ class UserDetailComment extends React.Component { ?  ({t('comment.edited')}) : null } - + +
    + + +
    Story: {comment.asset.title} diff --git a/client/coral-admin/src/containers/UserDetailComment.js b/client/coral-admin/src/containers/UserDetailComment.js index a70a9394d..9f0e3a793 100644 --- a/client/coral-admin/src/containers/UserDetailComment.js +++ b/client/coral-admin/src/containers/UserDetailComment.js @@ -9,6 +9,7 @@ export default withFragments({ body created_at status + hasParent asset { id title diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index 2ca18d928..6980105c3 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -2,6 +2,7 @@ import React, {PropTypes} from 'react'; import {Link} from 'react-router'; import {Icon} from 'coral-ui'; +import ReplyBadge from 'coral-admin/src/components/ReplyBadge'; import FlagBox from 'coral-admin/src/components/FlagBox'; import styles from './styles.css'; import CommentType from 'coral-admin/src/components/CommentType'; @@ -15,7 +16,6 @@ import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighli import IfHasLink from 'coral-admin/src/components/IfHasLink'; import cn from 'classnames'; import {getCommentType} from 'coral-admin/src/utils/comment'; -import ReplyBadge from './ReplyBadge'; import t, {timeago} from 'coral-framework/services/i18n'; diff --git a/client/coral-ui/components/Badge.css b/client/coral-ui/components/Badge.css index fd725cf14..db68ebd58 100644 --- a/client/coral-ui/components/Badge.css +++ b/client/coral-ui/components/Badge.css @@ -1,14 +1,15 @@ .badge { display: inline-block; color: white; - background-color: #3D73D5; + background: grey; box-sizing: border-box; - padding: 0px 5px; + padding: 2px 5px; font-size: 12px; height: 24px; letter-spacing: 0.4px; - margin-bottom: 1px; - margin-right: 2px; + line-height: 22px; + background-color: #3D73D5; + margin-right: 4px; } .icon { From 01ed9880f4e6ddd4b69c19d7d7b7ad5172848b51 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 21 Aug 2017 15:26:04 -0600 Subject: [PATCH 028/121] comment count optim, prep for sortBy --- .../src/containers/Comment.js | 2 +- .../src/containers/Stream.js | 6 +- graph/loaders/comments.js | 204 +----------------- graph/resolvers/asset.js | 40 ++-- graph/resolvers/comment.js | 30 +-- graph/typeDefs.graphql | 59 +++-- models/comment.js | 2 +- .../client/containers/Tab.js | 2 +- .../client/containers/TabPane.js | 2 +- services/mongoose.js | 22 +- test/server/graph/queries/asset.js | 6 +- 11 files changed, 93 insertions(+), 282 deletions(-) diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 005a24ae1..bbf43bd4a 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -103,7 +103,7 @@ const withCommentFragments = withFragments({ fragment CoralEmbedStream_Comment_comment on Comment { ...CoralEmbedStream_Comment_SingleComment ${nest(` - replies(limit: 3, excludeIgnored: $excludeIgnored) { + replies(query: {limit: 3, excludeIgnored: $excludeIgnored}) { nodes { ...CoralEmbedStream_Comment_SingleComment ...nest diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index be6c000e0..8b02e793f 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -253,9 +253,9 @@ const fragments = { charCount requireEmailConfirmation } - commentCount(excludeIgnored: $excludeIgnored) @skip(if: $hasComment) - totalCommentCount(excludeIgnored: $excludeIgnored) @skip(if: $hasComment) - comments(limit: 10, excludeIgnored: $excludeIgnored) @skip(if: $hasComment) { + commentCount @skip(if: $hasComment) + totalCommentCount @skip(if: $hasComment) + comments(query: {limit: 10, excludeIgnored: $excludeIgnored}) @skip(if: $hasComment) { nodes { ...CoralEmbedStream_Stream_comment } diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index 0df73e970..56b389528 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -1,7 +1,6 @@ const { SharedCounterDataLoader, singleJoinBy, - arrayJoinBy } = require('./util'); const DataLoader = require('dataloader'); const { @@ -14,7 +13,6 @@ const { const ms = require('ms'); const CommentModel = require('../../models/comment'); -const UsersService = require('../../services/users'); /** * Returns the comment count for all comments that are public based on their @@ -48,38 +46,6 @@ const getCountsByAssetID = (context, asset_ids) => { .then((results) => results.map((result) => result ? result.count : 0)); }; -/** - * Returns the count of all public comments on an asset id, also filtering by personalization options. - * - * @param {Array} id The ID of the asset - * @param {Array} excludeIgnored Exclude comments ignored by the requesting user - */ -const getCountsByAssetIDPersonalized = async (context, {assetId, excludeIgnored, tags}) => { - const query = { - asset_id: assetId, - status: { - $in: ['NONE', 'ACCEPTED'], - }, - }; - - if (tags) { - query['tags.tag.name'] = { - $in: tags, - }; - } - - const user = context.user; - if (excludeIgnored && user) { - - // load afresh, as `user` may be from cache and not have recent ignores - const freshUser = await UsersService.findById(user.id); - const ignoredUsers = freshUser.ignoresUsers; - query.author_id = {$nin: ignoredUsers}; - } - const count = await CommentModel.where(query).count(); - return count; -}; - /** * Returns the comment count for all comments that are public based on their * asset ids. @@ -113,39 +79,6 @@ const getParentCountsByAssetID = (context, asset_ids) => { .then((results) => results.map((result) => result ? result.count : 0)); }; -/** - * Returns the count of top-level comments on an asset id, also filtering by personalization options. - * - * @param {Array} id The ID of the asset - * @param {Array} excludeIgnored Exclude comments ignored by the requesting user - */ -const getParentCountByAssetIDPersonalized = async (context, {assetId, excludeIgnored, tags}) => { - const query = { - asset_id: assetId, - parent_id: null, - status: { - $in: ['NONE', 'ACCEPTED'], - }, - }; - - if (tags) { - query['tags.tag.name'] = { - $in: tags, - }; - } - - const user = context.user; - if (excludeIgnored && user) { - - // load afresh, as `user` may be from cache and not have recent ignores - const freshUser = await UsersService.findById(user.id); - const ignoredUsers = freshUser.ignoresUsers; - query.author_id = {$nin: ignoredUsers}; - } - - return CommentModel.where(query).count(); -}; - /** * Returns the comment count for all comments that are public based on their * parent ids. @@ -178,33 +111,6 @@ const getCountsByParentID = (context, parent_ids) => { .then((results) => results.map((result) => result ? result.count : 0)); }; -/** - * Returns the count of comments for the provided parent_id, also filtering by personalization options. - * - * @param {Array} id The ID of the parent comment - * @param {Array} excludeIgnored Exclude comments ignored by context.user - */ -const getCountByParentIDPersonalized = async (context, {id, excludeIgnored}) => { - const query = { - parent_id: { - $in: [id] - }, - status: { - $in: ['NONE', 'ACCEPTED'] - } - }; - const user = context.user; - if (excludeIgnored && user) { - - // load afresh, as `user` may be from cache and not have recent ignores - const freshUser = await UsersService.findById(user.id); - const ignoredUsers = freshUser.ignoresUsers; - query.author_id = {$nin: ignoredUsers}; - } - const count = await CommentModel.where(query).count(); - return count; -}; - /** * Retrieves the count of comments based on the passed in query. * @param {Object} context graph context @@ -260,7 +166,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a // Only administrators can search for comments with statuses that are not // `null`, or `'ACCEPTED'`. - if (user != null && user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) && statuses) { + if (user != null && user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) && statuses && statuses.length > 0) { comments = comments.where({ status: { $in: statuses @@ -305,7 +211,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a comments = comments.where({parent_id}); } - if (excludeIgnored && user && user.ignoresUsers) { + if (excludeIgnored && user && user.ignoresUsers && user.ignoresUsers.length > 0) { comments = comments.where({ author_id: {$nin: user.ignoresUsers} }); @@ -347,107 +253,6 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a }); }; -/** - * Gets the recent replies. - * @param {Object} context graph context - * @param {Array} ids ids of parent ids - * @return {Promise} resolves to recent replies - */ -const genRecentReplies = (context, ids) => { - return CommentModel.aggregate([ - - // get all the replies for the comments in question - {$match: { - parent_id: { - $in: ids - } - }}, - - // sort these by their created at timestamp, ASC'y as these are - // replies - {$sort: { - created_at: 1 - }}, - - // group these replies by their parent_id - {$group: { - _id: '$parent_id', - replies: { - $push: '$$ROOT' - } - }}, - - // project it so that we only retain the first 3 replies of each parent - // comment - {$project: { - _id: '$_id', - replies: { - $slice: [ - '$replies', - 0, - 3 - ] - } - }}, - - {$unwind: '$replies'}, - - ]) - .then((replies) => replies.map((reply) => reply.replies)) - .then(arrayJoinBy(ids, 'parent_id')); -}; - -/** - * Gets the recent comments. - * @param {Object} context graph context - * @param {Array} ids ids of asset ids - * @return {Promise} resolves to recent comments from assets - */ -const genRecentComments = (_, ids) => { - return CommentModel.aggregate([ - - // get all the replies for the comments in question - {$match: { - asset_id: { - $in: ids - } - }}, - - // sort these by their created at timestamp, ASC'y as these are - // replies - {$sort: { - created_at: 1 - }}, - - // group these replies by their parent_id - {$group: { - _id: '$asset_id', - comments: { - $push: '$$ROOT' - } - }}, - - // project it so that we only retain the first 3 replies of each parent - // comment - {$project: { - _id: '$_id', - comments: { - $slice: [ - '$comments', - 0, - 3 - ] - } - }}, - - // Unwind these comments. - {$unwind: '$comments'}, - - ]) - .then((replies) => replies.map((reply) => reply.comments)) - .then(arrayJoinBy(ids, 'asset_id')); -}; - /** * getComments returns the comments by the id's. Only admins can see non-public comments. * @param {Object} context graph context @@ -486,12 +291,7 @@ module.exports = (context) => ({ getByQuery: (query) => getCommentsByQuery(context, query), getCountByQuery: (query) => getCommentCountByQuery(context, query), countByAssetID: new SharedCounterDataLoader('Comments.totalCommentCount', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getCountsByAssetID(context, ids)), - countByAssetIDPersonalized: (query) => getCountsByAssetIDPersonalized(context, query), parentCountByAssetID: new SharedCounterDataLoader('Comments.countByAssetID', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getParentCountsByAssetID(context, ids)), - parentCountByAssetIDPersonalized: (query) => getParentCountByAssetIDPersonalized(context, query), countByParentID: new SharedCounterDataLoader('Comments.countByParentID', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getCountsByParentID(context, ids)), - countByParentIDPersonalized: (query) => getCountByParentIDPersonalized(context, query), - genRecentReplies: new DataLoader((ids) => genRecentReplies(context, ids)), - genRecentComments: new DataLoader((ids) => genRecentComments(context, ids)) } }); diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js index fb2f6cac5..f5b3f04bc 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -4,9 +4,6 @@ const { } = require('../../perms/constants'); const Asset = { - recentComments({id}, _, {loaders: {Comments}}) { - return Comments.genRecentComments.load(id); - }, async comment({id}, {id: commentId}, {loaders: {Comments}, user}) { const statuses = user && user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) ? ['NONE', 'ACCEPTED', 'PREMOD', 'REJECTED'] @@ -20,7 +17,7 @@ const Asset = { return comments.nodes[0]; }, - comments({id}, {sort, limit, deep, excludeIgnored, tags}, {loaders: {Comments}}) { + comments({id}, {query: {sort, limit, excludeIgnored, tags}, deep}, {loaders: {Comments}}) { return Comments.getByQuery({ asset_id: id, sort, @@ -30,26 +27,37 @@ const Asset = { excludeIgnored, }); }, - commentCount({id, commentCount}, {excludeIgnored, tags}, {user, loaders: {Comments}}) { - - // TODO: remove - if ((user && excludeIgnored) || tags) { - return Comments.parentCountByAssetIDPersonalized({assetId: id, excludeIgnored, tags}); - } + commentCount({id, commentCount}, {tags}, {loaders: {Comments}}) { if (commentCount != null) { return commentCount; } + + // If we are filtering by a tag. + if (tags && tags.length > 0) { + + // Then count the comments with those tags. + return Comments.getCountByQuery({ + tags, + asset_id: id, + parent_id: id, + statuses: ['NONE', 'ACCEPTED'], + }); + } + return Comments.parentCountByAssetID.load(id); }, - totalCommentCount({id, totalCommentCount}, {excludeIgnored, tags}, {user, loaders: {Comments}}) { - - // TODO: remove - if ((user && excludeIgnored) || tags) { - return Comments.countByAssetIDPersonalized({assetId: id, excludeIgnored, tags}); - } + totalCommentCount({id, totalCommentCount}, {tags}, {loaders: {Comments}}) { if (totalCommentCount != null) { return totalCommentCount; } + + // If we are filtering by a tag. + if (tags && tags.length > 0) { + + // Then count the comments with those tags. + return Comments.getCountByQuery({tags, asset_id: id, statuses: ['NONE', 'ACCEPTED']}); + } + return Comments.countByAssetID.load(id); }, async settings({settings = null}, _, {loaders: {Settings}}) { diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 0fd3a730c..f9924f90c 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -11,10 +11,16 @@ const Comment = { user({author_id}, _, {loaders: {Users}}) { return Users.getByID.load(author_id); }, - recentReplies({id}, _, {loaders: {Comments}}) { - return Comments.genRecentReplies.load(id); - }, - replies({id, asset_id}, {sort, limit, excludeIgnored}, {loaders: {Comments}}) { + replies({id, asset_id, reply_count}, {query: {sort, limit, excludeIgnored}}, {loaders: {Comments}}) { + + // Don't bother looking up replies if there aren't any there! + if (reply_count === 0) { + return { + nodes: [], + hasNextPage: false, + }; + } + return Comments.getByQuery({ asset_id, parent_id: id, @@ -23,21 +29,17 @@ const Comment = { excludeIgnored, }); }, - replyCount({id}, {excludeIgnored}, {user, loaders: {Comments}}) { + replyCount({reply_count}) { - // TODO: remove - if (user && excludeIgnored) { - return Comments.countByParentIDPersonalized({id, excludeIgnored}); - } - return Comments.countByParentID.load(id); + // A simple remap from the underlying database model to the graph model. + return reply_count; }, actions({id}, _, {user, loaders: {Actions}}) { - - if (user && user.can('SEARCH_ACTIONS')) { - return Actions.getByID.load(id); + if (!user || !user.can('SEARCH_ACTIONS')) { + return null; } - return null; + return Actions.getByID.load(id); }, action_summaries({id, action_summaries}, _, {loaders: {Actions}}) { if (action_summaries) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 505026389..4555131f4 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -20,7 +20,7 @@ type Reliability { # `null` if the reliability cannot be determined. flagger: Boolean - # commenter will be `true` when the commenter is reliable, `false` if not, or + # Commenter will be `true` when the commenter is reliable, `false` if not, or # `null` if the reliability cannot be determined. commenter: Boolean } @@ -211,7 +211,7 @@ enum COMMENT_STATUS { PREMOD } -# The types of action there are as enum's. +# The types of action there are as enums. enum ACTION_TYPE { # Represents a FlagAction. @@ -246,7 +246,7 @@ input CommentsQuery { # Skip results from the last created_at timestamp. cursor: Cursor - # Sort the results by created_at. + # Sort the results by from largest first. sort: SORT_ORDER = DESC # Filter by a specific tag name. @@ -256,6 +256,18 @@ input CommentsQuery { excludeIgnored: Boolean } +input RepliesQuery { + + # Sort the results by from smallest first. + sort: SORT_ORDER = ASC + + # Limit the number of results to be returned. + limit: Int = 3 + + # Exclude comments ignored by the requesting user + excludeIgnored: Boolean +} + # CommentCountQuery allows the ability to query comment counts by specific # methods. input CommentCountQuery { @@ -313,14 +325,11 @@ type Comment { # the user who authored the comment. user: User - # the recent replies made against this comment. - recentReplies: [Comment!] - # the replies that were made to the comment. - replies(sort: SORT_ORDER = ASC, limit: Int = 3, excludeIgnored: Boolean): CommentConnection! + replies(query: RepliesQuery!): CommentConnection! # The count of replies on a comment. - replyCount(excludeIgnored: Boolean): Int + replyCount: Int # Actions completed on the parent. Requires the `ADMIN` role. actions: [Action] @@ -569,28 +578,18 @@ type Asset { # The URL that the asset is located on. url: String - # Returns recent comments - recentComments: [Comment!] - - # The comments that are attached to the asset. - # If `deep` is true, it will return comments of all depths, - # otherwise only top-level comments are returned. - comments( - sort: SORT_ORDER = DESC, - limit: Int = 10, - excludeIgnored: Boolean, - tags: [String!] - deep: Boolean, - ): CommentConnection! + # The comments that are attached to the asset. When `deep` is true, the + # comments returned will be at all depths. + comments(query: CommentsQuery!, deep: Boolean = false): CommentConnection! # A Comment from the Asset by comment's ID comment(id: ID!): Comment # The count of top level comments on the asset. - commentCount(excludeIgnored: Boolean, tags: [String!]): Int + commentCount(tags: [String!]): Int # The total count of all comments made on the asset. - totalCommentCount(excludeIgnored: Boolean, tags: [String!]): Int + totalCommentCount(tags: [String!]): Int # The settings (rectified with the global settings) that should be applied to # this asset. @@ -921,19 +920,19 @@ input ModifyTagInput { # Response to the addTag or removeTag mutations. type ModifyTagResponse implements Response { - # An array of errors relating to the mutation that occured. + # An array of errors relating to the mutation that occurred. errors: [UserError!] } # Response to ignoreUser mutation type IgnoreUserResponse implements Response { - # An array of errors relating to the mutation that occured. + # An array of errors relating to the mutation that occurred. errors: [UserError!] } # Response to stopIgnoringUser mutation type StopIgnoringUserResponse implements Response { - # An array of errors relating to the mutation that occured. + # An array of errors relating to the mutation that occurred. errors: [UserError!] } @@ -944,13 +943,13 @@ input EditCommentInput { body: String! } -# EditCommentResponse contains the updated comment and any errors that occured. +# EditCommentResponse contains the updated comment and any errors that occurred. type EditCommentResponse implements Response { # The edited comment. comment: Comment - # An array of errors relating to the mutation that occured. + # An array of errors relating to the mutation that occurred. errors: [UserError!] } @@ -967,7 +966,7 @@ type CreateTokenResponse implements Response { # Token is the Token that was created, or null if it failed. token: Token - # An array of errors relating to the mutation that occured. + # An array of errors relating to the mutation that occurred. errors: [UserError!] } @@ -981,7 +980,7 @@ input RevokeTokenInput { # RevokeTokenResponse contains the errors related to revoking a token. type RevokeTokenResponse implements Response { - # An array of errors relating to the mutation that occured. + # An array of errors relating to the mutation that occurred. errors: [UserError!] } diff --git a/models/comment.js b/models/comment.js index ac1f1bcb1..73aa22d0b 100644 --- a/models/comment.js +++ b/models/comment.js @@ -55,7 +55,7 @@ const CommentSchema = new Schema({ body: { type: String, required: [true, 'The body is required.'], - minlength: 2 + minlength: 2, }, body_history: [BodyHistoryItemSchema], asset_id: String, diff --git a/plugins/talk-plugin-featured-comments/client/containers/Tab.js b/plugins/talk-plugin-featured-comments/client/containers/Tab.js index fee7f5da6..cbe430be2 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/Tab.js +++ b/plugins/talk-plugin-featured-comments/client/containers/Tab.js @@ -17,7 +17,7 @@ const enhance = compose( withFragments({ asset: gql` fragment TalkFeaturedComments_Tab_asset on Asset { - featuredCommentsCount: totalCommentCount(tags: ["FEATURED"], excludeIgnored: $excludeIgnored) @skip(if: $hasComment) + featuredCommentsCount: totalCommentCount(tags: ["FEATURED"]) @skip(if: $hasComment) }`, }), excludeIf((props) => props.asset.featuredCommentsCount === 0), diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js index a5fd3b7e8..31f2769a0 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js @@ -79,7 +79,7 @@ const enhance = compose( asset: gql` fragment TalkFeaturedComments_TabPane_asset on Asset { id - featuredComments: comments(tags: ["FEATURED"], excludeIgnored: $excludeIgnored, deep: true) @skip(if: $hasComment) { + featuredComments: comments(query: {tags: ["FEATURED"]}, deep: true) @skip(if: $hasComment) { nodes { ...${getDefinitionName(Comment.fragments.comment)} } diff --git a/services/mongoose.js b/services/mongoose.js index 07f3b2189..43966eb0d 100644 --- a/services/mongoose.js +++ b/services/mongoose.js @@ -1,7 +1,7 @@ const mongoose = require('mongoose'); const debug = require('debug')('talk:db'); const enabled = require('debug').enabled; -const queryDebuger = require('debug')('talk:db:query'); +const queryDebugger = require('debug')('talk:db:query'); const { MONGO_URL, @@ -27,14 +27,14 @@ function debugQuery(name, i, ...args) { let params = `(${_args.join(', ')})`; - queryDebuger(functionCall + params); + queryDebugger(functionCall + params); } // Use native promises mongoose.Promise = global.Promise; // Check if debugging is enabled on the talk:db prefix. -if (enabled('talk:db')) { +if (enabled('talk:db:query')) { // Enable the mongoose debugger, here we wrap the similar print function // provided by setting the debug parameter. @@ -53,14 +53,16 @@ if (WEBPACK) { } else { // Connect to the Mongo instance. - mongoose.connect(MONGO_URL, (err) => { - if (err) { + mongoose + .connect(MONGO_URL, { + useMongoClient: true, + }) + .then(() => { + debug('connection established'); + }) + .catch((err) => { throw err; - } - - debug('connection established'); - }); - + }); } module.exports = mongoose; diff --git a/test/server/graph/queries/asset.js b/test/server/graph/queries/asset.js index 0b837a181..68270f9ba 100644 --- a/test/server/graph/queries/asset.js +++ b/test/server/graph/queries/asset.js @@ -45,7 +45,7 @@ describe('graph.queries.asset', () => { const assetCommentsQuery = ` query assetCommentsQuery($id: ID!) { asset(id: $id) { - comments(limit: 10) { + comments(query: {limit: 10}) { nodes { id body @@ -59,7 +59,7 @@ describe('graph.queries.asset', () => { } `; const res = await graphql(schema, assetCommentsQuery, {}, context, {id: asset.id}); - expect(res.erros).is.empty; + expect(res.errors).is.empty; const {nodes, startCursor, endCursor, hasNextPage} = res.data.asset.comments; expect(nodes.length).to.equal(2); expect(startCursor).to.equal(nodes[0].created_at); @@ -82,7 +82,7 @@ describe('graph.queries.asset', () => { const query = ` query assetCommentsQuery($id: ID!, $url: String!, $excludeIgnored: Boolean!) { asset(id: $id, url: $url) { - comments(limit: 10, excludeIgnored: $excludeIgnored) { + comments(query: {limit: 10, excludeIgnored: $excludeIgnored}) { nodes { id body From 78d4740abe3782544dc5e43b93c0ce6931dc5e18 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 21 Aug 2017 15:54:14 -0600 Subject: [PATCH 029/121] drastically improved queries involving actions + comments --- graph/loaders/comments.js | 24 ++++++++++++++++++++++-- graph/resolvers/root_query.js | 20 ++++---------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index 56b389528..ffb52ea86 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -119,7 +119,7 @@ const getCountsByParentID = (context, parent_ids) => { * @return {Promise} resolves to the counts of the comments from the * query */ -const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, author_id, tags}) => { +const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, author_id, tags, action_type}) => { let query = CommentModel.find(); if (ids) { @@ -142,6 +142,16 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au query = query.where({author_id}); } + if (context.user != null && context.user.can(SEARCH_OTHERS_COMMENTS) && action_type) { + query = query.where({ + action_counts: { + [action_type.toLowerCase()]: { + $gt: 0, + } + }, + }); + } + if (tags) { query = query.find({ 'tags.tag.name': { @@ -161,7 +171,7 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au * @param {Object} context graph context * @param {Object} query query terms to apply to the comments query */ -const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, excludeIgnored, tags}) => { +const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, excludeIgnored, tags, action_type}) => { let comments = CommentModel.find(); // Only administrators can search for comments with statuses that are not @@ -180,6 +190,16 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a }); } + if (user != null && user.can(SEARCH_OTHERS_COMMENTS) && action_type) { + comments = comments.where({ + action_counts: { + [action_type.toLowerCase()]: { + $gt: 0, + } + }, + }); + } + if (ids) { comments = comments.find({ id: { diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 8bb9d31d3..c88184dc9 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -26,13 +26,7 @@ const RootQuery = { // This endpoint is used for loading moderation queues, so hide it in the // event that we aren't an admin. - async comments(_, {query}, {user, loaders: {Comments, Actions}}) { - let {action_type} = query; - - if (user != null && user.can(SEARCH_OTHERS_COMMENTS) && action_type) { - query.ids = await Actions.getByTypes({action_type, item_type: 'COMMENTS'}); - } - + async comments(_, {query}, {loaders: {Comments}}) { return Comments.getByQuery(query); }, @@ -40,19 +34,13 @@ const RootQuery = { return Comments.get.load(id); }, - async commentCount(_, {query}, {user, loaders: {Actions, Comments, Assets}}) { - + async commentCount(_, {query}, {user, loaders: {Comments, Assets}}) { if (user == null || !user.can(SEARCH_OTHERS_COMMENTS)) { return null; } - - const {action_type, asset_url} = query; - if (action_type) { - query.ids = await Actions.getByTypes({action_type, item_type: 'COMMENTS'}); - } - - if (asset_url) { + const {asset_url} = query; + if (asset_url && asset_url.length > 0) { let asset = await Assets.findByUrl(asset_url); if (asset) { query.asset_id = asset.id; From eec0e18600da1a3b1cbe64313cfe039992ef6888 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 21 Aug 2017 16:05:35 -0600 Subject: [PATCH 030/121] bug fixes, removed dead code --- graph/loaders/comments.js | 35 +---------------------------------- graph/mutators/comment.js | 8 ++------ graph/resolvers/asset.js | 8 ++++++-- 3 files changed, 9 insertions(+), 42 deletions(-) diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index ffb52ea86..170f0ed4b 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -79,38 +79,6 @@ const getParentCountsByAssetID = (context, asset_ids) => { .then((results) => results.map((result) => result ? result.count : 0)); }; -/** - * Returns the comment count for all comments that are public based on their - * parent ids. - * - * @param {Array} parent_ids the ids of parents for which there are - * comments that we want to get - */ -const getCountsByParentID = (context, parent_ids) => { - return CommentModel.aggregate([ - { - $match: { - parent_id: { - $in: parent_ids - }, - status: { - $in: ['NONE', 'ACCEPTED'] - } - } - }, - { - $group: { - _id: '$parent_id', - count: { - $sum: 1 - } - } - } - ]) - .then(singleJoinBy(parent_ids, '_id')) - .then((results) => results.map((result) => result ? result.count : 0)); -}; - /** * Retrieves the count of comments based on the passed in query. * @param {Object} context graph context @@ -311,7 +279,6 @@ module.exports = (context) => ({ getByQuery: (query) => getCommentsByQuery(context, query), getCountByQuery: (query) => getCommentCountByQuery(context, query), countByAssetID: new SharedCounterDataLoader('Comments.totalCommentCount', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getCountsByAssetID(context, ids)), - parentCountByAssetID: new SharedCounterDataLoader('Comments.countByAssetID', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getParentCountsByAssetID(context, ids)), - countByParentID: new SharedCounterDataLoader('Comments.countByParentID', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getCountsByParentID(context, ids)), + parentCountByAssetID: new SharedCounterDataLoader('Comments.countByAssetID', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getParentCountsByAssetID(context, ids)) } }); diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index b32b8ed79..3f9e72fc3 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -174,9 +174,7 @@ const createComment = async (context, {tags = [], body, asset_id, parent_id = nu // perform these increments in the event that we do have a new comment that // is approved or without a comment. if (status === 'NONE' || status === 'APPROVED') { - if (parent_id != null) { - Comments.countByParentID.incr(parent_id); - } else { + if (parent_id === null) { Comments.parentCountByAssetID.incr(asset_id); } Comments.countByAssetID.incr(asset_id); @@ -338,9 +336,7 @@ const setStatus = async ({user, loaders: {Comments}, pubsub}, {id, status}) => { // be nice if we could decrement the counters here, but that would result // in us having to know the initial state of the comment, which would // require another database query. - if (comment.parent_id != null) { - Comments.countByParentID.clear(comment.parent_id); - } else { + if (comment.parent_id === null) { Comments.parentCountByAssetID.clear(comment.asset_id); } diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js index f5b3f04bc..48403ddd0 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -39,7 +39,7 @@ const Asset = { return Comments.getCountByQuery({ tags, asset_id: id, - parent_id: id, + parent_id: null, statuses: ['NONE', 'ACCEPTED'], }); } @@ -55,7 +55,11 @@ const Asset = { if (tags && tags.length > 0) { // Then count the comments with those tags. - return Comments.getCountByQuery({tags, asset_id: id, statuses: ['NONE', 'ACCEPTED']}); + return Comments.getCountByQuery({ + tags, + asset_id: id, + statuses: ['NONE', 'ACCEPTED'], + }); } return Comments.countByAssetID.load(id); From e0f9bb4165f61831c7517f7d92e7be3493f84acd Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 21 Aug 2017 16:08:30 -0600 Subject: [PATCH 031/121] optimized to use new query path --- graph/loaders/assets.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/graph/loaders/assets.js b/graph/loaders/assets.js index d3d1e40a8..956d573ef 100644 --- a/graph/loaders/assets.js +++ b/graph/loaders/assets.js @@ -54,12 +54,8 @@ const findOrCreateAssetByURL = async (context, asset_url) => { return asset; }; -const getAssetsForMetrics = async ({loaders: {Actions, Comments}}) => { - let actions = await Actions.getByTypes({action_type: 'FLAG', item_type: 'COMMENT'}); - - const ids = actions.map(({item_id}) => item_id); - - return Comments.getByQuery({ids}) +const getAssetsForMetrics = async ({loaders: {Comments}}) => { + return Comments.getByQuery({action_type: 'FLAG'}) .then((connection) => connection.nodes); }; @@ -86,7 +82,7 @@ module.exports = (context) => ({ // TODO: decide whether we want to move these to mutators or not, as in fact // this operation create a new asset if one isn't found. getByURL: (url) => findOrCreateAssetByURL(context, url), - + findByUrl: (url) => findByUrl(context, url), search: (query) => getAssetsByQuery(context, query), getByID: new DataLoader((ids) => genAssetsByID(context, ids)), From cb4d2998f608aa32c02ea573a290a3f4ba9d63bc Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 22 Aug 2017 09:40:49 -0300 Subject: [PATCH 032/121] Removing logic from ReplyBadge --- client/coral-admin/src/components/ReplyBadge.js | 4 ++-- client/coral-admin/src/components/UserDetailComment.js | 2 +- .../coral-admin/src/routes/Moderation/components/Comment.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/coral-admin/src/components/ReplyBadge.js b/client/coral-admin/src/components/ReplyBadge.js index ccdb79b3a..5c8bb88ba 100644 --- a/client/coral-admin/src/components/ReplyBadge.js +++ b/client/coral-admin/src/components/ReplyBadge.js @@ -2,10 +2,10 @@ import React from 'react'; import {Badge} from 'coral-ui'; import t from 'coral-framework/services/i18n'; -const ReplyBadge = ({hasParent}) => hasParent ? +const ReplyBadge = () => ( {t('modqueue.reply')} - : null; +); export default ReplyBadge; diff --git a/client/coral-admin/src/components/UserDetailComment.js b/client/coral-admin/src/components/UserDetailComment.js index 3d92c0365..dc809b9c7 100644 --- a/client/coral-admin/src/components/UserDetailComment.js +++ b/client/coral-admin/src/components/UserDetailComment.js @@ -59,7 +59,7 @@ class UserDetailComment extends React.Component { }
    - + {comment.hasParent && }
    diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index 6980105c3..c73d15eaa 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -108,7 +108,7 @@ class Comment extends React.Component { }
    - + {comment.hasParent && } Date: Tue, 22 Aug 2017 21:59:41 +0700 Subject: [PATCH 033/121] Remove global dependency on store, rest, pym and apollo client --- client/coral-admin/src/actions/assets.js | 9 +- client/coral-admin/src/actions/auth.js | 24 +++-- client/coral-admin/src/actions/community.js | 13 ++- client/coral-admin/src/actions/install.js | 9 +- client/coral-admin/src/actions/settings.js | 9 +- client/coral-admin/src/actions/users.js | 13 ++- client/coral-admin/src/index.js | 21 +--- .../coral-embed-stream/src/actions/asset.js | 9 +- client/coral-embed-stream/src/actions/auth.js | 56 +++++------ .../coral-embed-stream/src/actions/stream.js | 9 +- client/coral-embed-stream/src/index.js | 30 +----- .../components/TalkProvider.js | 4 +- client/coral-framework/helpers/plugins.js | 13 +-- client/coral-framework/helpers/request.js | 84 ---------------- client/coral-framework/services/bootstrap.js | 92 +++++++++++++++++ client/coral-framework/services/client.js | 99 ++++++++++--------- client/coral-framework/services/events.js | 6 +- client/coral-framework/services/rest.js | 62 ++++++++++++ client/coral-framework/services/store.js | 61 ++---------- client/coral-framework/services/transport.js | 37 ------- 20 files changed, 304 insertions(+), 356 deletions(-) delete mode 100644 client/coral-framework/helpers/request.js create mode 100644 client/coral-framework/services/bootstrap.js create mode 100644 client/coral-framework/services/rest.js delete mode 100644 client/coral-framework/services/transport.js diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js index b9ebe0ada..1ce6ad6ac 100644 --- a/client/coral-admin/src/actions/assets.js +++ b/client/coral-admin/src/actions/assets.js @@ -8,7 +8,6 @@ import { UPDATE_ASSETS } from '../constants/assets'; -import coralApi from '../../../coral-framework/helpers/request'; import t from 'coral-framework/services/i18n'; /** @@ -17,9 +16,9 @@ import t from 'coral-framework/services/i18n'; // Fetch a page of assets // Get comments to fill each of the three lists on the mod queue -export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => { +export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch, _, {rest}) => { dispatch({type: FETCH_ASSETS_REQUEST}); - return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`) + return rest(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`) .then(({result, count}) => dispatch({type: FETCH_ASSETS_SUCCESS, assets: result, @@ -34,9 +33,9 @@ export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filte // Update an asset state // Get comments to fill each of the three lists on the mod queue -export const updateAssetState = (id, closedAt) => (dispatch) => { +export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => { dispatch({type: UPDATE_ASSET_STATE_REQUEST, id, closedAt}); - return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}}) + return rest(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}}) .then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS})) .catch((error) => { console.error(error); diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 8c84a95ec..3433fc95a 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -1,8 +1,6 @@ import bowser from 'bowser'; import * as actions from '../constants/auth'; -import coralApi from 'coral-framework/helpers/request'; import * as Storage from 'coral-framework/helpers/storage'; -import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; import jwtDecode from 'jwt-decode'; @@ -10,7 +8,7 @@ import jwtDecode from 'jwt-decode'; // SIGN IN //============================================================================== -export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => { +export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client}) => { dispatch({type: actions.LOGIN_REQUEST}); const params = { @@ -27,7 +25,7 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => }; } - return coralApi('/auth/local', params) + return rest('/auth/local', params) .then(({user, token}) => { if (!user) { @@ -38,7 +36,7 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => } dispatch(handleAuthToken(token)); - resetWebsocket(); + client.resetWebsocket(); dispatch(checkLoginSuccess(user)); }) .catch((error) => { @@ -84,11 +82,11 @@ const forgotPasswordFailure = (error) => ({ error, }); -export const requestPasswordReset = (email) => (dispatch) => { +export const requestPasswordReset = (email) => (dispatch, _, {rest}) => { dispatch(forgotPasswordRequest(email)); const redirectUri = location.href; - return coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}}) + return rest('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}}) .then(() => dispatch(forgotPasswordSuccess())) .catch((error) => { console.error(error); @@ -116,9 +114,9 @@ const checkLoginFailure = (error) => ({ error }); -export const checkLogin = () => (dispatch) => { +export const checkLogin = () => (dispatch, _, {rest, client}) => { dispatch(checkLoginRequest()); - return coralApi('/auth') + return rest('/auth') .then(({user}) => { if (!user) { if (!bowser.safari && !bowser.ios) { @@ -127,7 +125,7 @@ export const checkLogin = () => (dispatch) => { return dispatch(checkLoginFailure('not logged in')); } - resetWebsocket(); + client.resetWebsocket(); dispatch(checkLoginSuccess(user)); }) .catch((error) => { @@ -141,12 +139,12 @@ export const checkLogin = () => (dispatch) => { // LOGOUT //============================================================================== -export const logout = () => (dispatch) => { - return coralApi('/auth', {method: 'DELETE'}).then(() => { +export const logout = () => (dispatch, _, {rest, client}) => { + return rest('/auth', {method: 'DELETE'}).then(() => { Storage.removeItem('token'); // Reset the websocket. - resetWebsocket(); + client.resetWebsocket(); dispatch({type: actions.LOGOUT}); }); diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 70c9b7592..402a32e5d 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -14,13 +14,12 @@ import { HIDE_REJECT_USERNAME_DIALOG } from '../constants/community'; -import coralApi from '../../../coral-framework/helpers/request'; import t from 'coral-framework/services/i18n'; -export const fetchAccounts = (query = {}) => (dispatch) => { +export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => { dispatch(requestFetchAccounts()); - coralApi(`/users?${qs.stringify(query)}`) + rest(`/users?${qs.stringify(query)}`) .then(({result, page, count, limit, totalPages}) =>{ dispatch({ type: FETCH_COMMENTERS_SUCCESS, @@ -51,15 +50,15 @@ export const newPage = () => ({ type: COMMENTERS_NEW_PAGE }); -export const setRole = (id, role) => (dispatch) => { - return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}}) +export const setRole = (id, role) => (dispatch, _, {rest}) => { + return rest(`/users/${id}/role`, {method: 'POST', body: {role}}) .then(() => { return dispatch({type: SET_ROLE, id, role}); }); }; -export const setCommenterStatus = (id, status) => (dispatch) => { - return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}}) +export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => { + return rest(`/users/${id}/status`, {method: 'POST', body: {status}}) .then(() => { return dispatch({type: SET_COMMENTER_STATUS, id, status}); }); diff --git a/client/coral-admin/src/actions/install.js b/client/coral-admin/src/actions/install.js index 6393c58dd..d7210782a 100644 --- a/client/coral-admin/src/actions/install.js +++ b/client/coral-admin/src/actions/install.js @@ -1,4 +1,3 @@ -import coralApi from 'coral-framework/helpers/request'; import * as actions from '../constants/install'; import validate from 'coral-framework/helpers/validate'; import errorMsj from 'coral-framework/helpers/error'; @@ -106,10 +105,10 @@ export const submitUser = () => (dispatch, getState) => { }); }; -export const finishInstall = () => (dispatch, getState) => { +export const finishInstall = () => (dispatch, getState, {rest}) => { const data = getState().install.data; dispatch(installRequest()); - return coralApi('/setup', {method: 'POST', body: data}) + return rest('/setup', {method: 'POST', body: data}) .then(() => { dispatch(installSuccess()); dispatch(nextStep()); @@ -129,9 +128,9 @@ const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST}); const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed}); const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error}); -export const checkInstall = (next) => (dispatch) => { +export const checkInstall = (next) => (dispatch, _, {rest}) => { dispatch(checkInstallRequest()); - coralApi('/setup') + rest('/setup') .then(({installed}) => { dispatch(checkInstallSuccess(installed)); if (installed) { diff --git a/client/coral-admin/src/actions/settings.js b/client/coral-admin/src/actions/settings.js index ca9062b9e..35f2013c9 100644 --- a/client/coral-admin/src/actions/settings.js +++ b/client/coral-admin/src/actions/settings.js @@ -1,4 +1,3 @@ -import coralApi from '../../../coral-framework/helpers/request'; import t from 'coral-framework/services/i18n'; export const SETTINGS_LOADING = 'SETTINGS_LOADING'; @@ -14,9 +13,9 @@ export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED'; export const WORDLIST_UPDATED = 'WORDLIST_UPDATED'; export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED'; -export const fetchSettings = () => (dispatch) => { +export const fetchSettings = () => (dispatch, _, {rest}) => { dispatch({type: SETTINGS_LOADING}); - coralApi('/settings') + rest('/settings') .then((settings) => { dispatch({type: SETTINGS_RECEIVED, settings}); }) @@ -41,13 +40,13 @@ export const updateDomainlist = (listName, list) => { return {type: DOMAINLIST_UPDATED, listName, list}; }; -export const saveSettingsToServer = () => (dispatch, getState) => { +export const saveSettingsToServer = () => (dispatch, getState, {rest}) => { let settings = getState().settings; if (settings.charCount) { settings.charCount = parseInt(settings.charCount); } dispatch({type: SAVE_SETTINGS_LOADING}); - coralApi('/settings', {method: 'PUT', body: settings}) + rest('/settings', {method: 'PUT', body: settings}) .then(() => { dispatch({type: SAVE_SETTINGS_SUCCESS, settings}); }) diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js index 7ba051a90..994a79a79 100644 --- a/client/coral-admin/src/actions/users.js +++ b/client/coral-admin/src/actions/users.js @@ -1,4 +1,3 @@ -import coralApi from '../../../coral-framework/helpers/request'; import * as userTypes from '../constants/users'; import t from 'coral-framework/services/i18n'; @@ -7,9 +6,9 @@ import t from 'coral-framework/services/i18n'; */ // change status of a user export const userStatusUpdate = (status, userId, commentId) => { - return (dispatch) => { + return (dispatch, _, {rest}) => { dispatch({type: userTypes.UPDATE_STATUS_REQUEST}); - return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) + return rest(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) .then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res})) .catch((error) => { console.error(error); @@ -21,8 +20,8 @@ export const userStatusUpdate = (status, userId, commentId) => { // change status of a user export const sendNotificationEmail = (userId, subject, body) => { - return (dispatch) => { - return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}}) + return (dispatch, _, {rest}) => { + return rest(`/users/${userId}/email`, {method: 'POST', body: {subject, body}}) .catch((error) => { console.error(error); const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); @@ -33,8 +32,8 @@ export const sendNotificationEmail = (userId, subject, body) => { // let a user edit their username export const enableUsernameEdit = (userId) => { - return (dispatch) => { - return coralApi(`/users/${userId}/username-enable`, {method: 'POST'}) + return (dispatch, _, {rest}) => { + return rest(`/users/${userId}/username-enable`, {method: 'POST'}) .catch((error) => { console.error(error); const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index ed9d537c0..e85cbed8c 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -1,34 +1,21 @@ import React from 'react'; import {render} from 'react-dom'; import smoothscroll from 'smoothscroll-polyfill'; -import EventEmitter from 'eventemitter2'; import TalkProvider from 'coral-framework/components/TalkProvider'; - -import {getClient} from 'coral-framework/services/client'; -import store from './services/store'; - +import {createContext} from 'coral-framework/services/bootstrap'; +import reducers from './reducers'; import App from './components/App'; - import 'react-mdl/extra/material.js'; import './graphql'; -import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins'; import plugins from 'pluginsConfig'; -const eventEmitter = new EventEmitter(); -const client = getClient(); +const context = createContext(reducers, plugins); -// TODO: pass redux actions through the emitter. - -loadPluginsTranslations(); -injectPluginsReducers(); smoothscroll.polyfill(); render( diff --git a/client/coral-embed-stream/src/actions/asset.js b/client/coral-embed-stream/src/actions/asset.js index 87b6fdd98..6acebeafb 100644 --- a/client/coral-embed-stream/src/actions/asset.js +++ b/client/coral-embed-stream/src/actions/asset.js @@ -1,5 +1,4 @@ import * as actions from '../constants/asset'; -import coralApi from 'coral-framework/helpers/request'; import {addNotification} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; @@ -12,10 +11,10 @@ const updateAssetSettingsRequest = () => ({type: actions.UPDATE_ASSET_SETTINGS_R const updateAssetSettingsSuccess = (settings) => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings}); const updateAssetSettingsFailure = (error) => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE, error}); -export const updateConfiguration = (newConfig) => (dispatch, getState) => { +export const updateConfiguration = (newConfig) => (dispatch, getState, {rest}) => { const assetId = getState().asset.id; dispatch(updateAssetSettingsRequest()); - coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig}) + rest(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig}) .then(() => { dispatch(addNotification('success', t('framework.success_update_settings'))); dispatch(updateAssetSettingsSuccess(newConfig)); @@ -26,10 +25,10 @@ export const updateConfiguration = (newConfig) => (dispatch, getState) => { }); }; -export const updateOpenStream = (closedBody) => (dispatch, getState) => { +export const updateOpenStream = (closedBody) => (dispatch, getState, {rest}) => { const assetId = getState().asset.id; dispatch(fetchAssetRequest()); - coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody}) + rest(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody}) .then(() => { dispatch(addNotification('success', t('framework.success_update_settings'))); dispatch(fetchAssetSuccess(closedBody)); diff --git a/client/coral-embed-stream/src/actions/auth.js b/client/coral-embed-stream/src/actions/auth.js index 851889a9b..55f7eb546 100644 --- a/client/coral-embed-stream/src/actions/auth.js +++ b/client/coral-embed-stream/src/actions/auth.js @@ -2,11 +2,8 @@ import jwtDecode from 'jwt-decode'; import bowser from 'bowser'; import * as actions from '../constants/auth'; import * as Storage from 'coral-framework/helpers/storage'; -import coralApi, {base} from 'coral-framework/helpers/request'; -import pym from 'coral-framework/services/pym'; import {addNotification} from 'coral-framework/actions/notification'; -import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; export const showSignInDialog = () => ({ @@ -59,9 +56,9 @@ export const updateUsername = ({username}) => ({ username }); -export const createUsername = (userId, formData) => (dispatch) => { +export const createUsername = (userId, formData) => (dispatch, _, {rest}) => { dispatch(createUsernameRequest()); - coralApi('/account/username', {method: 'PUT', body: formData}) + rest('/account/username', {method: 'PUT', body: formData}) .then(() => { dispatch(createUsernameSuccess()); dispatch(hideCreateUsernameDialog()); @@ -123,10 +120,10 @@ export const handleAuthToken = (token) => (dispatch) => { //============================================================================== export const fetchSignIn = (formData) => { - return (dispatch) => { + return (dispatch, _, {rest}) => { dispatch(signInRequest()); - return coralApi('/auth/local', {method: 'POST', body: formData}) + return rest('/auth/local', {method: 'POST', body: formData}) .then(({token}) => { if (!bowser.safari && !bowser.ios) { dispatch(handleAuthToken(token)); @@ -171,10 +168,10 @@ const signInFacebookFailure = (error) => ({ error }); -export const fetchSignInFacebook = () => (dispatch) => { +export const fetchSignInFacebook = () => (dispatch, _, {rest}) => { dispatch(signInFacebookRequest()); window.open( - `${base}/auth/facebook`, + `${rest.uri}/auth/facebook`, 'Continue with Facebook', 'menubar=0,resizable=0,width=500,height=500,top=200,left=500' ); @@ -188,10 +185,10 @@ const signUpFacebookRequest = () => ({ type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST }); -export const fetchSignUpFacebook = () => (dispatch) => { +export const fetchSignUpFacebook = () => (dispatch, _, {rest}) => { dispatch(signUpFacebookRequest()); window.open( - `${base}/auth/facebook`, + `${rest.uri}/auth/facebook`, 'Continue with Facebook', 'menubar=0,resizable=0,width=500,height=500,top=200,left=500' ); @@ -220,11 +217,11 @@ const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST}); const signUpSuccess = (user) => ({type: actions.FETCH_SIGNUP_SUCCESS, user}); const signUpFailure = (error) => ({type: actions.FETCH_SIGNUP_FAILURE, error}); -export const fetchSignUp = (formData) => (dispatch, getState) => { +export const fetchSignUp = (formData) => (dispatch, getState, {rest}) => { const redirectUri = getState().auth.redirectUri; dispatch(signUpRequest()); - coralApi('/users', { + rest('/users', { method: 'POST', body: formData, headers: {'X-Pym-Url': redirectUri} @@ -256,10 +253,10 @@ const forgotPasswordFailure = (error) => ({ error, }); -export const fetchForgotPassword = (email) => (dispatch, getState) => { +export const fetchForgotPassword = (email) => (dispatch, getState, {rest}) => { dispatch(forgotPasswordRequest(email)); const redirectUri = getState().auth.redirectUri; - coralApi('/account/password/reset', { + rest('/account/password/reset', { method: 'POST', body: {email, loc: redirectUri} }) @@ -275,16 +272,15 @@ export const fetchForgotPassword = (email) => (dispatch, getState) => { // LOGOUT //============================================================================== -export const logout = () => (dispatch) => { - return coralApi('/auth', {method: 'DELETE'}).then(() => { - Storage.removeItem('token'); +export const logout = () => async (dispatch, _, {rest, client, pym}) => { + await rest('/auth', {method: 'DELETE'}); + Storage.removeItem('token'); - // Reset the websocket. - resetWebsocket(); + // Reset the websocket. + client.resetWebsocket(); - dispatch({type: actions.LOGOUT}); - pym.sendMessage('coral-auth-changed'); - }); + dispatch({type: actions.LOGOUT}); + pym.sendMessage('coral-auth-changed'); }; //============================================================================== @@ -300,9 +296,9 @@ const checkLoginSuccess = (user, isAdmin) => ({ isAdmin }); -export const checkLogin = () => (dispatch) => { +export const checkLogin = () => (dispatch, _, {rest, client, pym}) => { dispatch(checkLoginRequest()); - coralApi('/auth') + rest('/auth') .then((result) => { if (!result.user) { Storage.removeItem('token'); @@ -310,7 +306,7 @@ export const checkLogin = () => (dispatch) => { } // Reset the websocket. - resetWebsocket(); + client.resetWebsocket(); dispatch(checkLoginSuccess(result.user)); pym.sendMessage('coral-auth-changed', JSON.stringify(result.user)); @@ -351,10 +347,10 @@ const verifyEmailFailure = () => ({ type: actions.VERIFY_EMAIL_FAILURE }); -export const requestConfirmEmail = (email) => (dispatch, getState) => { +export const requestConfirmEmail = (email) => (dispatch, getState, {rest}) => { const redirectUri = getState().auth.redirectUri; dispatch(verifyEmailRequest()); - return coralApi('/users/resend-verify', { + return rest('/users/resend-verify', { method: 'POST', body: {email}, headers: {'X-Pym-Url': redirectUri} @@ -387,8 +383,8 @@ export const setRedirectUri = (uri) => ({ const editUsernameFailure = (error) => ({type: actions.EDIT_USERNAME_FAILURE, error}); const editUsernameSuccess = () => ({type: actions.EDIT_USERNAME_SUCCESS}); -export const editName = (username) => (dispatch) => { - return coralApi('/account/username', {method: 'PUT', body: {username}}) +export const editName = (username) => (dispatch, _, {rest}) => { + return rest('/account/username', {method: 'PUT', body: {username}}) .then(() => { dispatch(editUsernameSuccess()); dispatch(addNotification('success', t('framework.success_name_update'))); diff --git a/client/coral-embed-stream/src/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js index c20760259..75f353b19 100644 --- a/client/coral-embed-stream/src/actions/stream.js +++ b/client/coral-embed-stream/src/actions/stream.js @@ -1,11 +1,10 @@ -import pym from 'coral-framework/services/pym'; import * as actions from '../constants/stream'; import {buildUrl} from 'coral-framework/utils/url'; import queryString from 'query-string'; export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id}); -export const viewAllComments = () => { +export const viewAllComments = () => (dispatch, _, {pym}) => { const search = queryString.stringify({ ...queryString.parse(location.search), @@ -24,10 +23,10 @@ export const viewAllComments = () => { pym.sendMessage('coral-view-all-comments'); } catch (e) { /* not sure if we're worried about old browsers */ } - return {type: actions.VIEW_ALL_COMMENTS}; + dispatch({type: actions.VIEW_ALL_COMMENTS}); }; -export const viewComment = (id) => { +export const viewComment = (id) => (dispatch, _, {pym}) => { const search = queryString.stringify({ ...queryString.parse(location.search), @@ -46,7 +45,7 @@ export const viewComment = (id) => { pym.sendMessage('coral-view-comment', id); } catch (e) { /* not sure if we're worried about old browsers */ } - return {type: actions.VIEW_COMMENT, id}; + dispatch({type: actions.VIEW_COMMENT, id}); }; export const addCommentClassName = (className) => ({ diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index a14ce8df8..ac38d2eab 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -4,24 +4,16 @@ import {render} from 'react-dom'; import {checkLogin, handleAuthToken, logout} from 'coral-embed-stream/src/actions/auth'; import './graphql'; import {addExternalConfig} from 'coral-embed-stream/src/actions/config'; -import {getStore, injectReducers, addListener} from 'coral-framework/services/store'; -import {getClient} from 'coral-framework/services/client'; -import {createReduxEmitter} from 'coral-framework/services/events'; -import pym from 'coral-framework/services/pym'; +import {createContext} from 'coral-framework/services/bootstrap'; import AppRouter from './AppRouter'; -import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins'; import reducers from './reducers'; -import EventEmitter from 'eventemitter2'; import TalkProvider from 'coral-framework/components/TalkProvider'; import plugins from 'pluginsConfig'; -const store = getStore(); -const client = getClient(); -const eventEmitter = new EventEmitter({wildcard: true}); +const context = createContext(reducers, plugins); -loadPluginsTranslations(); -injectPluginsReducers(); -injectReducers(reducers); +// TODO: move init code into `bootstrap` service after auth has been refactored. +const {store, pym} = context; function inIframe() { try { @@ -57,23 +49,11 @@ if (!window.opener) { } else { init(); } - - // Pass any events through our parent. - eventEmitter.onAny((eventName, value) => { - pym.sendMessage('event', JSON.stringify({eventName, value})); - }); - - // Add a redux listener to pass through all actions to our event emitter. - addListener(createReduxEmitter(eventEmitter)); } render( diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index 275373c26..0f44af43f 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -12,9 +12,9 @@ class TalkProvider extends React.Component { } render() { - const {children, client, store, plugins} = this.props; + const {children, client, store} = this.props; return ( - + {children} ); diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 3173115c2..b73c89c64 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -6,8 +6,6 @@ import flattenDeep from 'lodash/flattenDeep'; import isEmpty from 'lodash/isEmpty'; import flatten from 'lodash/flatten'; import mapValues from 'lodash/mapValues'; -import {loadTranslations} from 'coral-framework/services/i18n'; -import {injectReducers} from 'coral-framework/services/store'; import {getDisplayName} from 'coral-framework/helpers/hoc'; import camelize from './camelize'; import plugins from 'pluginsConfig'; @@ -133,23 +131,18 @@ export function getModQueueConfigs() { .filter((o) => o)); } -function getTranslations() { +export function getTranslations(plugins) { return plugins .map((o) => o.module.translations) .filter((o) => o); } -export function loadPluginsTranslations() { - getTranslations().forEach((t) => loadTranslations(t)); -} - -export function injectPluginsReducers() { - const reducers = merge( +export function getReducers(plugins) { + return merge( ...plugins .filter((o) => o.module.reducer) .map((o) => ({[camelize(o.name)] : o.module.reducer})) ); - injectReducers(reducers); } function addMetaDataToSlotComponents() { diff --git a/client/coral-framework/helpers/request.js b/client/coral-framework/helpers/request.js deleted file mode 100644 index ffddc9c5e..000000000 --- a/client/coral-framework/helpers/request.js +++ /dev/null @@ -1,84 +0,0 @@ -import bowser from 'bowser'; -import * as Storage from './storage'; -import merge from 'lodash/merge'; -import {getStore} from 'coral-framework/services/store'; -import {BASE_PATH} from 'coral-framework/constants/url'; - -/** - * getAuthToken returns the active auth token or null - * Note: this method does not have access to the cookie based token used by - * browsers that don't allow us to use cross domain iframe local storage. - * @return {string|null} - */ -export const getAuthToken = () => { - let state = getStore().getState(); - - if (state.config.auth_token) { - - // if an auth_token exists in config, use it. - return state.config.auth_token; - - } else if (!bowser.safari && !bowser.ios) { - - // Use local storage auth tokens where there's a stable api. - return Storage.getItem('token'); - } - - return null; -}; - -const buildOptions = (inputOptions = {}) => { - const defaultOptions = { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - credentials: 'same-origin' - }; - - let options = merge({}, defaultOptions, inputOptions); - - // Apply authToken header - let authToken = getAuthToken(); - if (authToken !== null) { - options.headers.Authorization = `Bearer ${authToken}`; - } - - if (options.method.toLowerCase() !== 'get') { - options.body = JSON.stringify(options.body); - } - - return options; -}; - -const handleResp = (res) => { - if (res.status > 399) { - return res.json().then((err) => { - let message = err.message || res.status; - const error = new Error(); - - if (err.error && err.error.metadata && err.error.metadata.message) { - error.metadata = err.error.metadata.message; - } - - if (err.error && err.error.translation_key) { - error.translation_key = err.error.translation_key; - } - - error.message = message; - error.status = res.status; - throw error; - }); - } else if (res.status === 204) { - return res.text(); - } else { - return res.json(); - } -}; - -export const base = `${BASE_PATH}api/v1`; - -export default (url, options) => { - return fetch(`${base}${url}`, buildOptions(options)).then(handleResp); -}; diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js new file mode 100644 index 000000000..6a18695f7 --- /dev/null +++ b/client/coral-framework/services/bootstrap.js @@ -0,0 +1,92 @@ +import {createStore} from './store'; +import {createClient, apolloErrorReporter} from './client'; +import pym from './pym'; +import EventEmitter from 'eventemitter2'; +import {createReduxEmitter} from './events'; +import {createRestClient} from './rest'; +import thunk from 'redux-thunk'; +import {loadTranslations} from './i18n'; +import {getTranslations, getReducers} from '../helpers/plugins'; +import bowser from 'bowser'; +import * as Storage from '../helpers/storage'; +import {BASE_PATH} from 'coral-framework/constants/url'; + +/** + * getAuthToken returns the active auth token or null + * Note: this method does not have access to the cookie based token used by + * browsers that don't allow us to use cross domain iframe local storage. + * @return {string|null} + */ +const getAuthToken = (store) => { + let state = store.getState(); + + if (state.config.auth_token) { + + // if an auth_token exists in config, use it. + return state.config.auth_token; + + } else if (!bowser.safari && !bowser.ios) { + + // Use local storage auth tokens where there's a stable api. + return Storage.getItem('token'); + } + + return null; +}; + +export function createContext(reducers, plugins) { + const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; + const eventEmitter = new EventEmitter({wildcard: true}); + let store = null; + const token = () => { + + // Try to get the token from localStorage. If it isn't here, it may + // be passed as a cookie. + + // NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT + // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. + return getAuthToken(store); + }; + const rest = createRestClient({ + uri: `${BASE_PATH}api/v1`, + token, + }); + const client = createClient({ + uri: `${BASE_PATH}api/v1/graph/ql`, + liveUri: `${protocol}://${location.host}${BASE_PATH}api/v1/live`, + token, + }); + const context = { + client, + pym, + plugins, + eventEmitter, + rest, + }; + + // Load plugin translations. + getTranslations(plugins).forEach((t) => loadTranslations(t)); + + // Pass any events through our parent. + eventEmitter.onAny((eventName, value) => { + pym.sendMessage('event', JSON.stringify({eventName, value})); + }); + + const finalReducers = { + ...reducers, + ...getReducers(plugins), + apollo: client.reducer(), + }; + + store = createStore(finalReducers, [ + client.middleware(), + thunk.withExtraArgument(context), + apolloErrorReporter, + createReduxEmitter(eventEmitter), + ]); + + return { + ...context, + store, + }; +} diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index 6f044f621..a354d68bc 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -1,64 +1,55 @@ -import ApolloClient, {addTypename, IntrospectionFragmentMatcher} from 'apollo-client'; -import {networkInterface} from './transport'; +import ApolloClient, {addTypename, IntrospectionFragmentMatcher, createNetworkInterface} from 'apollo-client'; import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws'; import MessageTypes from 'subscriptions-transport-ws/dist/message-types'; -import {getAuthToken} from '../helpers/request'; import introspectionQueryResultData from '../graphql/introspection.json'; -import {BASE_PATH} from 'coral-framework/constants/url'; -let client, wsClient = null, wsClientToken = null; - -export function resetWebsocket() { - if (wsClient === null) { - - // Nothing to reset! - return; +// Redux middleware to report any errors to the console. +export const apolloErrorReporter = () => (next) => (action) => { + if (action.type === 'APOLLO_QUERY_ERROR') { + console.error(action.error); } + return next(action); +}; - // Close socket connection which will also unregister subscriptions on the server-side. - wsClient.close(); - - // Reconnect to the server. - wsClient.connect(); - - // Reregister all subscriptions (uses non public api). - // See: https://github.com/apollographql/subscriptions-transport-ws/issues/171 - Object.keys(wsClient.operations).forEach((id) => { - wsClient.sendMessage(id, MessageTypes.GQL_START, wsClient.operations[id].options); - }); -} - -export function getClient(options = {}) { - if (client) { - return client; - } - - const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; - wsClient = new SubscriptionClient(`${protocol}://${location.host}${BASE_PATH}api/v1/live`, { +export function createClient(options = {}) { + const {token, uri, liveUri, ...apolloOptions} = options; + const wsClient = new SubscriptionClient(liveUri, { reconnect: true, lazy: true, connectionParams: { - get token() { - - wsClientToken = getAuthToken(); - - // Try to get the token from localStorage. If it isn't here, it may - // be passed as a cookie. - - // NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT - // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. - return wsClientToken; - } + token, } }); + const networkInterface = createNetworkInterface({ + uri, + opts: { + credentials: 'same-origin' + } + }); + + networkInterface.use([{ + applyMiddleware(req, next) { + if (!req.options.headers) { + req.options.headers = {}; // Create the header object if needed. + } + + let authToken = typeof token === 'function' ? token() : token; + if (authToken) { + req.options.headers['authorization'] = `Bearer ${authToken}`; + } + + next(); + } + }]); + const networkInterfaceWithSubscriptions = addGraphQLSubscriptions( networkInterface, wsClient, ); - client = new ApolloClient({ - ...options, + const client = new ApolloClient({ + ...apolloOptions, connectToDevTools: true, addTypename: true, fragmentMatcher: new IntrospectionFragmentMatcher({introspectionQueryResultData}), @@ -72,5 +63,25 @@ export function getClient(options = {}) { networkInterface: networkInterfaceWithSubscriptions, }); + client.resetWebsocket = () => { + if (wsClient === null) { + + // Nothing to reset! + return; + } + + // Close socket connection which will also unregister subscriptions on the server-side. + wsClient.close(); + + // Reconnect to the server. + wsClient.connect(); + + // Reregister all subscriptions (uses non public api). + // See: https://github.com/apollographql/subscriptions-transport-ws/issues/171 + Object.keys(wsClient.operations).forEach((id) => { + wsClient.sendMessage(id, MessageTypes.GQL_START, wsClient.operations[id].options); + }); + }; + return client; } diff --git a/client/coral-framework/services/events.js b/client/coral-framework/services/events.js index a1a2f4448..f6a62763d 100644 --- a/client/coral-framework/services/events.js +++ b/client/coral-framework/services/events.js @@ -1,5 +1,5 @@ export function createReduxEmitter(eventEmitter) { - return (action) => { + return () => (next) => (action) => { // Handle apollo actions. if (action.type.startsWith('APOLLO_')) { @@ -7,8 +7,10 @@ export function createReduxEmitter(eventEmitter) { const {operationName, variables, result: {data}} = action; eventEmitter.emit(`subscription.${operationName}.data`, {variables, data}); } - return; + return next(action); } eventEmitter.emit(`action.${action.type}`, {action}); + + return next(action); }; } diff --git a/client/coral-framework/services/rest.js b/client/coral-framework/services/rest.js new file mode 100644 index 000000000..f879659ca --- /dev/null +++ b/client/coral-framework/services/rest.js @@ -0,0 +1,62 @@ +import merge from 'lodash/merge'; + +const buildOptions = (inputOptions = {}) => { + const defaultOptions = { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + credentials: 'same-origin' + }; + + const options = merge({}, defaultOptions, inputOptions); + if (options.method.toLowerCase() !== 'get') { + options.body = JSON.stringify(options.body); + } + + return options; +}; + +const handleResp = (res) => { + if (res.status > 399) { + return res.json().then((err) => { + let message = err.message || res.status; + const error = new Error(); + + if (err.error && err.error.metadata && err.error.metadata.message) { + error.metadata = err.error.metadata.message; + } + + if (err.error && err.error.translation_key) { + error.translation_key = err.error.translation_key; + } + + error.message = message; + error.status = res.status; + throw error; + }); + } else if (res.status === 204) { + return res.text(); + } else { + return res.json(); + } +}; + +export function createRestClient(options) { + const {token, uri} = options; + const client = (path, options) => { + const authToken = typeof token === 'function' ? token() : token; + let opts = options; + if (authToken) { + opts = merge({}, options, { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }); + } + return fetch(`${uri}${path}`, buildOptions(opts)).then(handleResp); + }; + client.uri = uri; + return client; +} diff --git a/client/coral-framework/services/store.js b/client/coral-framework/services/store.js index ec2479d15..e5fc52503 100644 --- a/client/coral-framework/services/store.js +++ b/client/coral-framework/services/store.js @@ -1,66 +1,21 @@ -import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; -import thunk from 'redux-thunk'; -import {getClient} from './client'; +import {createStore as reduxCreateStore, combineReducers, applyMiddleware, compose} from 'redux'; -let listeners = []; - -export function injectReducers(reducers) { - const store = getStore(); - store.coralReducers = {...store.coralReducers, ...reducers}; - store.replaceReducer(combineReducers(store.coralReducers)); -} - -/** - * Add a action listener to the redux store. - * The action is passed as the first argument to the callback. - */ -export function addListener(cb) { - listeners.push(cb); -} - -export function getStore() { - if (window.coralStore) { - return window.coralStore; - } - - const apolloErrorReporter = () => (next) => (action) => { - if (action.type === 'APOLLO_QUERY_ERROR') { - console.error(action.error); - } - return next(action); - }; - - const customListener = () => (next) => (action) => { - listeners.forEach((cb) => {cb(action);}); - return next(action); - }; - - const middlewares = [ +export function createStore(reducers, middlewares = []) { + const enhancers = [ applyMiddleware( - getClient().middleware(), - thunk, - apolloErrorReporter, - customListener, + ...middlewares, ), ]; if (window.devToolsExtension) { // we can't have the last argument of compose() be undefined - middlewares.push(window.devToolsExtension()); + enhancers.push(window.devToolsExtension()); } - const coralReducers = { - apollo: getClient().reducer() - }; - - window.coralStore = createStore( - combineReducers(coralReducers), + return reduxCreateStore( + combineReducers(reducers), {}, - compose(...middlewares) + compose(...enhancers) ); - - window.coralStore.coralReducers = coralReducers; - - return window.coralStore; } diff --git a/client/coral-framework/services/transport.js b/client/coral-framework/services/transport.js deleted file mode 100644 index 060bfc34b..000000000 --- a/client/coral-framework/services/transport.js +++ /dev/null @@ -1,37 +0,0 @@ -import {createNetworkInterface} from 'apollo-client'; -import {getAuthToken} from '../helpers/request'; -import {BASE_PATH} from 'coral-framework/constants/url'; - -//============================================================================== -// NETWORK INTERFACE -//============================================================================== - -const networkInterface = createNetworkInterface({ - uri: `${BASE_PATH}api/v1/graph/ql`, - opts: { - credentials: 'same-origin' - } -}); - -//============================================================================== -// MIDDLEWARES -//============================================================================== - -networkInterface.use([{ - applyMiddleware(req, next) { - if (!req.options.headers) { - req.options.headers = {}; // Create the header object if needed. - } - - let authToken = getAuthToken(); - if (authToken) { - req.options.headers['authorization'] = `Bearer ${authToken}`; - } - - next(); - } -}]); - -export { - networkInterface -}; From b719168b183f245ddd2d8de376152e260c3f16e5 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 22 Aug 2017 10:11:47 -0600 Subject: [PATCH 034/121] Implemented sortBy --- .../coral-embed-stream/src/graphql/index.js | 2 +- graph/context.js | 22 +- graph/loaders/comments.js | 190 ++++++++++++++---- graph/resolvers/asset.js | 3 +- graph/resolvers/comment.js | 3 +- graph/typeDefs.graphql | 20 ++ package.json | 1 + plugin-api/beta/server/getReactionConfig.js | 31 +++ services/mongoose.js | 3 +- yarn.lock | 4 + 10 files changed, 227 insertions(+), 52 deletions(-) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index b4c5ce163..9a3718b0b 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -56,7 +56,7 @@ const extension = { fragment CoralEmbedStream_CreateCommentResponse on CreateCommentResponse { comment { ...CoralEmbedStream_CreateCommentResponse_Comment - replies { + replies(query: {}) { nodes { ...CoralEmbedStream_CreateCommentResponse_Comment } diff --git a/graph/context.js b/graph/context.js index 8d7c740b6..cd3f5ddbe 100644 --- a/graph/context.js +++ b/graph/context.js @@ -1,6 +1,7 @@ const loaders = require('./loaders'); const mutators = require('./mutators'); const uuid = require('uuid'); +const merge = require('lodash/merge'); const plugins = require('../services/plugins'); const pubsub = require('../services/pubsub'); @@ -17,17 +18,24 @@ const contextPlugins = plugins.get('server', 'context').map(({plugin, context}) }); /** - * This should itterate over the passed in plugins and load them all with the + * This should iterate over the passed in plugins and load them all with the * current graph context. * @return {Object} the saturated plugins object */ -const decorateContextPlugins = (context, contextPlugins) => contextPlugins.reduce((acc, plugin) => { - Object.keys(plugin.context).forEach((service) => { - acc[service] = plugin.context[service](context); - }); +const decorateContextPlugins = (context, contextPlugins) => { - return acc; -}, {}); + // For each of the plugins, we execute with the context to get the context + // based plugin. We then merge that into an object for the plugin. Once the + // plugin is assembled, we merge that object with all the other objects + // provided from the other plugins. + return merge(...contextPlugins.map((plugin) => { + return Object.keys(plugin.context).reduce((services, serviceName) => { + services[serviceName] = plugin.context[serviceName](context); + + return services; + }, {}); + })); +}; /** * Stores the request context. diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index 170f0ed4b..d637a9ef0 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -133,18 +133,156 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au .count(); }; +/** + * getStartCursor will retrieve the start cursor based on the sortBy field. + * + * @param {Object} ctx the graph context + * @param {Object} nodes the result set of retrieved comments + * @param {Object} params the params from the client describing the query + */ +const getStartCursor = (ctx, nodes, {cursor, sortBy}) => { + switch (sortBy) { + case 'CREATED_AT': + return nodes.length ? nodes[0].created_at : null; + case 'REPLIES': + + // The cursor is the start! This is using numeric pagination. + return cursor != null ? cursor : 0; + } + + const SORT_KEY = sortBy.toLowerCase(); + if (!ctx.plugins || !ctx.plugins.CommentSort || !ctx.plugins.CommentSort[SORT_KEY] || !ctx.plugins.CommentSort[SORT_KEY].startCursor) { + throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`); + } + + return ctx.plugins.CommentSort[SORT_KEY].startCursor(ctx, nodes, {cursor}); +}; + +/** + * getEndCursor will fetch the end cursor based on the desired sortBy parameter. + * + * @param {Object} ctx the graph context + * @param {Object} nodes the result set of retrieved comments + * @param {Object} params the params from the client describing the query + */ +const getEndCursor = (ctx, nodes, {cursor, sortBy}) => { + switch (sortBy) { + case 'CREATED_AT': + return nodes.length ? nodes[nodes.length - 1].created_at : null; + case 'REPLIES': + return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null; + } + + const SORT_KEY = sortBy.toLowerCase(); + if (!ctx.plugins || !ctx.plugins.CommentSort || !ctx.plugins.CommentSort[SORT_KEY] || !ctx.plugins.CommentSort[SORT_KEY].endCursor) { + throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`); + } + + return ctx.plugins.CommentSort[SORT_KEY].endCursor(ctx, nodes, {cursor}); +}; + +/** + * applySort will add the actual `.sort` and `.skip/.where` clauses to the query + * to apply the desired sort. + * + * @param {Object} ctx the graph context + * @param {Object} query the current mongoose query object + * @param {Object} params the params from the client describing the query + */ +const applySort = (ctx, query, {cursor, sort, sortBy}) => { + switch (sortBy) { + case 'CREATED_AT': { + if (cursor) { + if (sort === 'DESC') { + query = query.where({ + created_at: { + $lt: cursor, + }, + }); + } else { + query = query.where({ + created_at: { + $gt: cursor, + }, + }); + } + } + + return query.sort({created_at: sort === 'DESC' ? -1 : 1}); + } + case 'REPLIES': { + if (cursor) { + query = query.skip(cursor); + } + + return query.sort({reply_count: sort === 'DESC' ? -1 : 1, created_at: sort === 'DESC' ? -1 : 1}); + } + } + + const SORT_KEY = sortBy.toLowerCase(); + if (!ctx.plugins || !ctx.plugins.CommentSort || !ctx.plugins.CommentSort[SORT_KEY] || !ctx.plugins.CommentSort[SORT_KEY].sort) { + throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`); + } + + return ctx.plugins.CommentSort[SORT_KEY].sort(ctx, query, {cursor, sort}); +}; + +/** + * executeWithSort will actually retrieve the comments based on the pre-assembled + * query and will compose on top the sort operators necessary to get the desired + * result. + * + * @param {Object} ctx the graph context + * @param {Object} query the current mongoose query object + * @param {Object} params the params from the client describing the query + */ +const executeWithSort = async (ctx, query, {cursor, sort, sortBy, limit}) => { + + // Apply the sort to the query. + query = applySort(ctx, query, {cursor, sort, sortBy}); + + // Apply the limit (if it exists, as it's applied universally). + if (limit) { + query = query.limit(limit + 1); + } + + // Fetch the nodes based on the source query. + const nodes = await query.exec(); + + // The hasNextPage is always handled the same (ask for one more than we need, + // if there is one more, than there is more). + let hasNextPage = false; + if (limit && nodes.length > limit) { + + // There was one more than we expected! Set hasNextPage = true and remove + // the last item from the array that we requested. + hasNextPage = true; + nodes.splice(limit, 1); + } + + // Use the generator functions below to extract the cursor details based on + // the current sortBy parameter. + return { + startCursor: getStartCursor(ctx, nodes, {cursor, sort, sortBy, limit}), + endCursor: getEndCursor(ctx, nodes, {cursor, sort, sortBy, limit}), + hasNextPage, + nodes, + }; +}; + /** * Retrieves comments based on the passed in query that is filtered by the * current used passed in via the context. + * * @param {Object} context graph context * @param {Object} query query terms to apply to the comments query */ -const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, excludeIgnored, tags, action_type}) => { +const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, sortBy, excludeIgnored, tags, action_type}) => { let comments = CommentModel.find(); // Only administrators can search for comments with statuses that are not // `null`, or `'ACCEPTED'`. - if (user != null && user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) && statuses && statuses.length > 0) { + if (ctx.user != null && ctx.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) && statuses && statuses.length > 0) { comments = comments.where({ status: { $in: statuses @@ -158,7 +296,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a }); } - if (user != null && user.can(SEARCH_OTHERS_COMMENTS) && action_type) { + if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) { comments = comments.where({ action_counts: { [action_type.toLowerCase()]: { @@ -185,7 +323,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a } // Only let an admin request any user or the current user request themself. - if (user && (user.can(SEARCH_OTHERS_COMMENTS) || user.id === author_id) && author_id != null) { + if (ctx.user && (ctx.user.can(SEARCH_OTHERS_COMMENTS) || ctx.user.id === author_id) && author_id != null) { comments = comments.where({author_id}); } @@ -199,50 +337,19 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a comments = comments.where({parent_id}); } - if (excludeIgnored && user && user.ignoresUsers && user.ignoresUsers.length > 0) { + if (excludeIgnored && ctx.user && ctx.user.ignoresUsers && ctx.user.ignoresUsers.length > 0) { comments = comments.where({ - author_id: {$nin: user.ignoresUsers} + author_id: {$nin: ctx.user.ignoresUsers} }); } - if (cursor) { - if (sort === 'DESC') { - comments = comments.where({ - created_at: { - $lt: cursor - } - }); - } else { - comments = comments.where({ - created_at: { - $gt: cursor - } - }); - } - } - - let query = comments - .sort({created_at: sort === 'DESC' ? -1 : 1}); - if (limit) { - query = query.limit(limit + 1); - } - return query.then((nodes) => { - let hasNextPage = false; - if (limit && nodes.length > limit) { - hasNextPage = true; - nodes.splice(limit, 1); - } - return Promise.resolve({ - startCursor: nodes.length ? nodes[0].created_at : null, - endCursor: nodes.length ? nodes[nodes.length - 1].created_at : null, - hasNextPage, - nodes, - }); - }); + return executeWithSort(ctx, comments, {cursor, sort, sortBy, limit}); }; /** - * getComments returns the comments by the id's. Only admins can see non-public comments. + * getComments returns the comments by the id's. Only admins can see non-public + * comments. + * * @param {Object} context graph context * @param {Array} ids the comment id's to fetch * @return {Promise} resolves to the comments @@ -270,6 +377,7 @@ const getComments = ({user}, ids) => { /** * Creates a set of loaders based on a GraphQL context. + * * @param {Object} context the context of the GraphQL request * @return {Object} object of loaders */ diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js index 48403ddd0..51234fd5a 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -17,10 +17,11 @@ const Asset = { return comments.nodes[0]; }, - comments({id}, {query: {sort, limit, excludeIgnored, tags}, deep}, {loaders: {Comments}}) { + comments({id}, {query: {sort, sortBy, limit, excludeIgnored, tags}, deep}, {loaders: {Comments}}) { return Comments.getByQuery({ asset_id: id, sort, + sortBy, limit, parent_id: deep ? undefined : null, tags, diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index f9924f90c..80ee63532 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -11,7 +11,7 @@ const Comment = { user({author_id}, _, {loaders: {Users}}) { return Users.getByID.load(author_id); }, - replies({id, asset_id, reply_count}, {query: {sort, limit, excludeIgnored}}, {loaders: {Comments}}) { + replies({id, asset_id, reply_count}, {query: {sort, sortBy, limit, excludeIgnored}}, {loaders: {Comments}}) { // Don't bother looking up replies if there aren't any there! if (reply_count === 0) { @@ -25,6 +25,7 @@ const Comment = { asset_id, parent_id: id, sort, + sortBy, limit, excludeIgnored, }); diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 4555131f4..024cbe8fb 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -249,6 +249,10 @@ input CommentsQuery { # Sort the results by from largest first. sort: SORT_ORDER = DESC + # The order to sort the comments by, sorting by default the created at + # timestamp. + sortBy: SORT_COMMENTS_BY = CREATED_AT + # Filter by a specific tag name. tags: [String!] @@ -261,6 +265,10 @@ input RepliesQuery { # Sort the results by from smallest first. sort: SORT_ORDER = ASC + # The order to sort the comments by, sorting by default the created at + # timestamp. + sortBy: SORT_COMMENTS_BY = CREATED_AT + # Limit the number of results to be returned. limit: Int = 3 @@ -660,6 +668,18 @@ enum SORT_ORDER { ASC } +# SORT_COMMENTS_BY selects the means for which comments are ordered when +# sorting. +enum SORT_COMMENTS_BY { + + # Comments will be sorted by their created at date. + CREATED_AT + + # Comments will be sorted by their immediate reply count (replies to the comment + # in question only, not including descendants). + REPLIES +} + # All queries that can be executed. enum USER_STATUS { ACTIVE diff --git a/package.json b/package.json index 63fb24de3..e89c40829 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "passport": "^0.4.0", "passport-jwt": "^3.0.0", "passport-local": "^1.0.0", + "pluralize": "^7.0.0", "postcss-loader": "^1.3.3", "postcss-modules": "^0.5.2", "postcss-smart-import": "^0.5.1", diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js index 243b8915b..f0aa23324 100644 --- a/plugin-api/beta/server/getReactionConfig.js +++ b/plugin-api/beta/server/getReactionConfig.js @@ -1,12 +1,15 @@ const wrapResponse = require('../../../graph/helpers/response'); const {SEARCH_OTHER_USERS} = require('../../../perms/constants'); const errors = require('../../../errors'); +const pluralize = require('pluralize'); function getReactionConfig(reaction) { reaction = reaction.toLowerCase(); + const reactionPlural = pluralize(reaction); const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1); const REACTION = reaction.toUpperCase(); + const REACTION_PLURAL = reactionPlural.toUpperCase(); const typeDefs = ` enum ACTION_TYPE { @@ -26,6 +29,13 @@ function getReactionConfig(reaction) { item_id: ID! } + enum SORT_COMMENTS_BY { + + # Comments will be sorted by their count of ${reactionPlural} + # on the comment. + ${REACTION_PLURAL} + } + input Delete${Reaction}ActionInput { # The item's id for which we are deleting a ${reaction}. @@ -107,6 +117,27 @@ function getReactionConfig(reaction) { return { typeDefs, + context: { + CommentSort: () => ({ + [reactionPlural]: { + startCursor(ctx, nodes, {cursor}) { + + // The cursor is the start! This is using numeric pagination. + return cursor != null ? cursor : 0; + }, + endCursor(ctx, nodes, {cursor}) { + return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null; + }, + sort(ctx, query, {cursor, sort}) { + if (cursor) { + query = query.skip(cursor); + } + + return query.sort({[`action_counts.${reaction}`]: sort === 'DESC' ? -1 : 1, created_at: sort === 'DESC' ? -1 : 1}); + }, + }, + }), + }, resolvers: { Subscription: { [`${reaction}ActionCreated`]: ({action}) => { diff --git a/services/mongoose.js b/services/mongoose.js index 43966eb0d..fb23a5fe1 100644 --- a/services/mongoose.js +++ b/services/mongoose.js @@ -61,7 +61,8 @@ if (WEBPACK) { debug('connection established'); }) .catch((err) => { - throw err; + console.error(err); + process.exit(1); }); } diff --git a/yarn.lock b/yarn.lock index 2141ef7ce..7c33f202b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5648,6 +5648,10 @@ pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" +pluralize@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" + pop-iterate@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pop-iterate/-/pop-iterate-1.0.1.tgz#ceacfdab4abf353d7a0f2aaa2c1fc7b3f9413ba3" From ffc6a7201789d8d6d8272a6973f9bc8e69be767f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 22 Aug 2017 11:47:52 -0600 Subject: [PATCH 035/121] added docs for Comments.Sort, some changes to interface --- docs/_docs/04-04-plugins-server.md | 46 +++++++++++++++++++++ graph/loaders/comments.js | 12 +++--- plugin-api/beta/server/getReactionConfig.js | 30 +++++++------- 3 files changed, 68 insertions(+), 20 deletions(-) diff --git a/docs/_docs/04-04-plugins-server.md b/docs/_docs/04-04-plugins-server.md index 13df5564f..f807da141 100644 --- a/docs/_docs/04-04-plugins-server.md +++ b/docs/_docs/04-04-plugins-server.md @@ -85,6 +85,52 @@ configure the context plugin before it would be mounted at `context.plugins`. This plugin above would mount at: `context.plugins.Slack`, or, if you're using [object destructuring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), `{plugins: {Slack}}`. +##### `Sort` + +A special context hook, `Sort` will allow plugin authors to provide new +methods to sort data. An example is as follows: + +```js +{ + Sort: () => ({ + Comments: { // <-- (1) + likes: { // <-- (2) + startCursor(ctx, nodes, {cursor}) { // <-- (3) + return cursor != null ? cursor : 0; + }, + endCursor(ctx, nodes, {cursor}) { // <-- (4) + return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null; + }, + sort(ctx, query, {cursor, sort}) { // <-- (5) + if (cursor) { + query = query.skip(cursor); + } + + return query.sort({ + 'action_counts.like': sort === 'DESC' ? -1 : 1, + created_at: sort === 'DESC' ? -1 : 1, + }); + }, + }, + }, + }), +} +``` + +This has a bunch of special features: + +1. `Comments` is the name of the type being sorted, this is pluralized and + capitalized. +2. `likes` is the `sortBy` field in lowercase. +3. `startCursor` will retrieve the start cursor based on the current set of + nodes and the current cursor. +4. `endCursor` will retrieve the end cursor based on the current set of nodes + and the current cursor. +5. `sort` will mutate the `query` to apply the sort operations. + +All the `startCursor`, `endCursor`, and `sort` functions must be provided in +order for the sorting to apply properly. + #### Field: `loaders` ```js diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index d637a9ef0..c71777b6b 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -151,11 +151,11 @@ const getStartCursor = (ctx, nodes, {cursor, sortBy}) => { } const SORT_KEY = sortBy.toLowerCase(); - if (!ctx.plugins || !ctx.plugins.CommentSort || !ctx.plugins.CommentSort[SORT_KEY] || !ctx.plugins.CommentSort[SORT_KEY].startCursor) { + if (!ctx.plugins || !ctx.plugins.Sort.Comments || !ctx.plugins.Sort.Comments[SORT_KEY] || !ctx.plugins.Sort.Comments[SORT_KEY].startCursor) { throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`); } - return ctx.plugins.CommentSort[SORT_KEY].startCursor(ctx, nodes, {cursor}); + return ctx.plugins.Sort.Comments[SORT_KEY].startCursor(ctx, nodes, {cursor}); }; /** @@ -174,11 +174,11 @@ const getEndCursor = (ctx, nodes, {cursor, sortBy}) => { } const SORT_KEY = sortBy.toLowerCase(); - if (!ctx.plugins || !ctx.plugins.CommentSort || !ctx.plugins.CommentSort[SORT_KEY] || !ctx.plugins.CommentSort[SORT_KEY].endCursor) { + if (!ctx.plugins || !ctx.plugins.Sort.Comments || !ctx.plugins.Sort.Comments[SORT_KEY] || !ctx.plugins.Sort.Comments[SORT_KEY].endCursor) { throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`); } - return ctx.plugins.CommentSort[SORT_KEY].endCursor(ctx, nodes, {cursor}); + return ctx.plugins.Sort.Comments[SORT_KEY].endCursor(ctx, nodes, {cursor}); }; /** @@ -220,11 +220,11 @@ const applySort = (ctx, query, {cursor, sort, sortBy}) => { } const SORT_KEY = sortBy.toLowerCase(); - if (!ctx.plugins || !ctx.plugins.CommentSort || !ctx.plugins.CommentSort[SORT_KEY] || !ctx.plugins.CommentSort[SORT_KEY].sort) { + if (!ctx.plugins || !ctx.plugins.Sort.Comments || !ctx.plugins.Sort.Comments[SORT_KEY] || !ctx.plugins.Sort.Comments[SORT_KEY].sort) { throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`); } - return ctx.plugins.CommentSort[SORT_KEY].sort(ctx, query, {cursor, sort}); + return ctx.plugins.Sort.Comments[SORT_KEY].sort(ctx, query, {cursor, sort}); }; /** diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js index f0aa23324..46bf0b892 100644 --- a/plugin-api/beta/server/getReactionConfig.js +++ b/plugin-api/beta/server/getReactionConfig.js @@ -118,22 +118,24 @@ function getReactionConfig(reaction) { return { typeDefs, context: { - CommentSort: () => ({ - [reactionPlural]: { - startCursor(ctx, nodes, {cursor}) { + Sort: () => ({ + Comments: { + [reactionPlural]: { + startCursor(ctx, nodes, {cursor}) { - // The cursor is the start! This is using numeric pagination. - return cursor != null ? cursor : 0; - }, - endCursor(ctx, nodes, {cursor}) { - return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null; - }, - sort(ctx, query, {cursor, sort}) { - if (cursor) { - query = query.skip(cursor); - } + // The cursor is the start! This is using numeric pagination. + return cursor != null ? cursor : 0; + }, + endCursor(ctx, nodes, {cursor}) { + return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null; + }, + sort(ctx, query, {cursor, sort}) { + if (cursor) { + query = query.skip(cursor); + } - return query.sort({[`action_counts.${reaction}`]: sort === 'DESC' ? -1 : 1, created_at: sort === 'DESC' ? -1 : 1}); + return query.sort({[`action_counts.${reaction}`]: sort === 'DESC' ? -1 : 1, created_at: sort === 'DESC' ? -1 : 1}); + }, }, }, }), From ef0c0ed62829cd8be2382fda14e181228ac42f2c Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 22 Aug 2017 12:05:25 -0600 Subject: [PATCH 036/121] reduced image size by 65% --- .dockerignore | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index 44d0bd178..96d04e69b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,7 @@ node_modules scripts !scripts/generateIntrospectionResult.js -# documentation should not be visable in production. +# documentation should not be visible in production. docs # static assets are rebuild in the docker container. diff --git a/Dockerfile b/Dockerfile index 092cbf57f..95aa74d8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:8 +FROM node:8-alpine # Create app directory RUN mkdir -p /usr/src/app From fccbcce7a804f93058e7d35cac1c255ce8054aee Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 01:41:58 +0700 Subject: [PATCH 037/121] Remove global dependency on registry and plugins --- client/coral-admin/src/graphql/index.js | 5 +- client/coral-admin/src/index.js | 6 +- .../Moderation/containers/Moderation.js | 20 +- .../routes/Moderation/hoc/withQueueConfig.js | 26 + .../src/routes/Moderation/queueConfig.js | 2 - .../src/containers/StreamTabPanel.js | 45 +- .../coral-embed-stream/src/graphql/index.js | 4 +- client/coral-embed-stream/src/index.js | 6 +- .../components/IfSlotIsEmpty.js | 6 +- .../components/IfSlotIsNotEmpty.js | 6 +- client/coral-framework/components/Slot.js | 13 +- .../components/TalkProvider.js | 6 +- client/coral-framework/helpers/plugins.js | 166 ------ client/coral-framework/hocs/withFragments.js | 19 +- client/coral-framework/hocs/withMutation.js | 17 +- client/coral-framework/hocs/withQuery.js | 30 +- client/coral-framework/services/bootstrap.js | 22 +- client/coral-framework/services/client.js | 5 - .../services/graphqlRegistry.js | 494 +++++++++--------- client/coral-framework/services/plugins.js | 174 ++++++ plugin-api/beta/client/services/index.js | 6 - 21 files changed, 582 insertions(+), 496 deletions(-) create mode 100644 client/coral-admin/src/routes/Moderation/hoc/withQueueConfig.js delete mode 100644 client/coral-framework/helpers/plugins.js create mode 100644 client/coral-framework/services/plugins.js diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js index 915fe2989..dea68c5e8 100644 --- a/client/coral-admin/src/graphql/index.js +++ b/client/coral-admin/src/graphql/index.js @@ -1,6 +1,4 @@ -import {add} from 'coral-framework/services/graphqlRegistry'; - -const extension = { +export default { mutations: { SetUserStatus: () => ({ refetchQueries: ['CoralAdmin_Community'], @@ -11,4 +9,3 @@ const extension = { }, }; -add(extension); diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index e85cbed8c..8b90f5acf 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -6,10 +6,10 @@ import {createContext} from 'coral-framework/services/bootstrap'; import reducers from './reducers'; import App from './components/App'; import 'react-mdl/extra/material.js'; -import './graphql'; -import plugins from 'pluginsConfig'; +import graphqlExtension from './graphql'; +import pluginsConfig from 'pluginsConfig'; -const context = createContext(reducers, plugins); +const context = createContext({reducers, graphqlExtension, pluginsConfig}); smoothscroll.polyfill(); diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index d5ca5c840..8df83da2a 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -26,25 +26,26 @@ import { storySearchChange, clearState } from 'actions/moderation'; +import withQueueConfig from '../hoc/withQueueConfig'; import {Spinner} from 'coral-ui'; import Moderation from '../components/Moderation'; import Comment from './Comment'; -import queueConfig from '../queueConfig'; +import baseQueueConfig from '../queueConfig'; function prepareNotificationText(text) { return truncate(text, {length: 50}).replace('\n', ' '); } function getAssetId(props) { - if (props.params.tabOrId && !(props.params.tabOrId in queueConfig)) { + if (props.params.tabOrId && !(props.params.tabOrId in props.queueConfig)) { return props.params.tabOrId; } return props.params.id || null; } function getTab(props) { - if (props.params.tabOrId && props.params.tabOrId in queueConfig) { + if (props.params.tabOrId && props.params.tabOrId in props.queueConfig) { return props.params.tabOrId; } return props.params.tab || null; @@ -54,7 +55,7 @@ class ModerationContainer extends Component { subscriptions = []; handleCommentChange = (root, comment, notify) => { - return handleCommentChange(root, comment, this.props.data.variables.sort, notify, queueConfig, this.activeTab); + return handleCommentChange(root, comment, this.props.data.variables.sort, notify, this.props.queueConfig, this.activeTab); }; get activeTab() { @@ -161,8 +162,8 @@ class ModerationContainer extends Component { cursor: this.props.root[tab].endCursor, sort: this.props.data.variables.sort, asset_id: this.props.data.variables.asset_id, - statuses: queueConfig[tab].statuses, - action_type: queueConfig[tab].action_type, + statuses: this.props.queueConfig[tab].statuses, + action_type: this.props.queueConfig[tab].action_type, }; return this.props.data.fetchMore({ query: LOAD_MORE_QUERY, @@ -206,7 +207,7 @@ class ModerationContainer extends Component { } const premodEnabled = assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation); - const currentQueueConfig = Object.assign({}, queueConfig); + const currentQueueConfig = Object.assign({}, this.props.queueConfig); if (premodEnabled) { delete currentQueueConfig.new; } else { @@ -304,7 +305,7 @@ const commentConnectionFragment = gql` ${Comment.fragments.comment} `; -const withModQueueQuery = withQuery(gql` +const withModQueueQuery = withQuery(({queueConfig}) => gql` query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER, $allAssets: Boolean!) { ${Object.keys(queueConfig).map((queue) => ` ${queue}: comments(query: { @@ -352,7 +353,7 @@ const withModQueueQuery = withQuery(gql` }, }); -const withQueueCountPolling = withQuery(gql` +const withQueueCountPolling = withQuery(({queueConfig}) => gql` query CoralAdmin_ModerationCountPoll($asset_id: ID) { ${Object.keys(queueConfig).map((queue) => ` ${queue}Count: commentCount(query: { @@ -398,6 +399,7 @@ const mapDispatchToProps = (dispatch) => ({ }); export default compose( + withQueueConfig(baseQueueConfig), connect(mapStateToProps, mapDispatchToProps), withSetCommentStatus, withQueueCountPolling, diff --git a/client/coral-admin/src/routes/Moderation/hoc/withQueueConfig.js b/client/coral-admin/src/routes/Moderation/hoc/withQueueConfig.js new file mode 100644 index 000000000..a56051810 --- /dev/null +++ b/client/coral-admin/src/routes/Moderation/hoc/withQueueConfig.js @@ -0,0 +1,26 @@ +import React from 'react'; +import hoistStatics from 'recompose/hoistStatics'; +import PropTypes from 'prop-types'; + +/** + * WithQueueConfig takes a `queueConfig` parameter that is + * passed down as a prop enriched with queue config data from plugins. + */ +export default (queueConfig) => hoistStatics((WrappedComponent) => { + class WithQueueConfig extends React.Component { + static contextTypes = { + plugins: PropTypes.object, + }; + + pluginsConfig = this.context.plugins.getModQueueConfigs(); + + render() { + return ; + } + } + + return WithQueueConfig; +}); diff --git a/client/coral-admin/src/routes/Moderation/queueConfig.js b/client/coral-admin/src/routes/Moderation/queueConfig.js index b9e9e5320..9a23ccc6c 100644 --- a/client/coral-admin/src/routes/Moderation/queueConfig.js +++ b/client/coral-admin/src/routes/Moderation/queueConfig.js @@ -1,5 +1,4 @@ import t from 'coral-framework/services/i18n'; -import {getModQueueConfigs} from 'coral-framework/helpers/plugins'; export default { premod: { @@ -33,5 +32,4 @@ export default { icon: 'question_answer', name: t('modqueue.all'), }, - ...getModQueueConfigs(), }; diff --git a/client/coral-embed-stream/src/containers/StreamTabPanel.js b/client/coral-embed-stream/src/containers/StreamTabPanel.js index 7b91d58ee..152f34e39 100644 --- a/client/coral-embed-stream/src/containers/StreamTabPanel.js +++ b/client/coral-embed-stream/src/containers/StreamTabPanel.js @@ -2,13 +2,15 @@ import React from 'react'; import StreamTabPanel from '../components/StreamTabPanel'; import {connect} from 'react-redux'; import omit from 'lodash/omit'; -import {getSlotComponents, getSlotComponentProps} from 'coral-framework/helpers/plugins'; import {Tab, TabPane} from 'coral-ui'; import {getShallowChanges} from 'coral-framework/utils'; import isEqual from 'lodash/isEqual'; import PropTypes from 'prop-types'; class StreamTabPanelContainer extends React.Component { + static contextTypes = { + plugins: PropTypes.object, + }; componentDidMount() { this.fallbackAllTab(); @@ -43,28 +45,37 @@ class StreamTabPanelContainer extends React.Component { } getSlotComponents(slot, props = this.props) { - return getSlotComponents(slot, props.reduxState, props.slotProps, props.queryData); + const {plugins} = this.context; + return plugins.getSlotComponents(slot, props.reduxState, props.slotProps, props.queryData); } getPluginTabElements(props = this.props) { - return this.getSlotComponents(props.tabSlot).map((PluginComponent) => ( - - - - )); + const {plugins} = this.context; + return this.getSlotComponents(props.tabSlot).map((PluginComponent) => { + const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData); + return ( + + + + ); + }); } getPluginTabPaneElements(props = this.props) { - return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => ( - - - - )); + const {plugins} = this.context; + return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => { + const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData); + return ( + + + + ); + }); } render() { diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index b4c5ce163..9ec9a2132 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -1,10 +1,9 @@ import {gql} from 'react-apollo'; -import {add} from 'coral-framework/services/graphqlRegistry'; import update from 'immutability-helper'; import uuid from 'uuid/v4'; import {insertCommentIntoEmbedQuery, removeCommentFromEmbedQuery} from './utils'; -const extension = { +export default { fragments: { EditCommentResponse: gql` fragment CoralEmbedStream_EditCommentResponse on EditCommentResponse { @@ -223,4 +222,3 @@ const extension = { }, }; -add(extension); diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index ac38d2eab..dca4e31b4 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -2,15 +2,15 @@ import React from 'react'; import {render} from 'react-dom'; import {checkLogin, handleAuthToken, logout} from 'coral-embed-stream/src/actions/auth'; -import './graphql'; +import graphqlExtension from './graphql'; import {addExternalConfig} from 'coral-embed-stream/src/actions/config'; import {createContext} from 'coral-framework/services/bootstrap'; import AppRouter from './AppRouter'; import reducers from './reducers'; import TalkProvider from 'coral-framework/components/TalkProvider'; -import plugins from 'pluginsConfig'; +import pluginsConfig from 'pluginsConfig'; -const context = createContext(reducers, plugins); +const context = createContext({reducers, graphqlExtension, pluginsConfig}); // TODO: move init code into `bootstrap` service after auth has been refactored. const {store, pym} = context; diff --git a/client/coral-framework/components/IfSlotIsEmpty.js b/client/coral-framework/components/IfSlotIsEmpty.js index e9211fc57..557ee3b3d 100644 --- a/client/coral-framework/components/IfSlotIsEmpty.js +++ b/client/coral-framework/components/IfSlotIsEmpty.js @@ -1,11 +1,13 @@ import React from 'react'; import {connect} from 'react-redux'; -import {isSlotEmpty} from 'coral-framework/helpers/plugins'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; import {getShallowChanges} from 'coral-framework/utils'; class IfSlotIsEmpty extends React.Component { + static contextTypes = { + plugins: PropTypes.object, + }; shouldComponentUpdate(next) { @@ -22,7 +24,7 @@ class IfSlotIsEmpty extends React.Component { isSlotEmpty(props = this.props) { const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props; - return isSlotEmpty(slot, reduxState, rest); + return this.context.plugins.isSlotEmpty(slot, reduxState, rest); } render() { diff --git a/client/coral-framework/components/IfSlotIsNotEmpty.js b/client/coral-framework/components/IfSlotIsNotEmpty.js index e7a0e83ce..4e3abfae4 100644 --- a/client/coral-framework/components/IfSlotIsNotEmpty.js +++ b/client/coral-framework/components/IfSlotIsNotEmpty.js @@ -1,11 +1,13 @@ import React from 'react'; import {connect} from 'react-redux'; -import {isSlotEmpty} from 'coral-framework/helpers/plugins'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; import {getShallowChanges} from 'coral-framework/utils'; class IfSlotIsNotEmpty extends React.Component { + static contextTypes = { + plugins: PropTypes.object, + }; shouldComponentUpdate(next) { @@ -22,7 +24,7 @@ class IfSlotIsNotEmpty extends React.Component { isSlotEmpty(props = this.props) { const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props; - return isSlotEmpty(slot, reduxState, rest); + return this.context.plugins.isSlotEmpty(slot, reduxState, rest); } render() { diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index c0cca143c..dad5d5cf2 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -2,14 +2,18 @@ import React from 'react'; import cn from 'classnames'; import styles from './Slot.css'; import {connect} from 'react-redux'; -import {getSlotElements, getSlotComponentProps} from 'coral-framework/helpers/plugins'; import omit from 'lodash/omit'; +import PropTypes from 'prop-types'; import isEqual from 'lodash/isEqual'; import {getShallowChanges} from 'coral-framework/utils'; const emptyConfig = {}; class Slot extends React.Component { + static contextTypes = { + plugins: PropTypes.object, + }; + shouldComponentUpdate(next) { // Prevent Slot from rerendering when only reduxState has changed and @@ -30,15 +34,18 @@ class Slot extends React.Component { } getChildren(props = this.props) { - return getSlotElements(props.fill, props.reduxState, this.getSlotProps(props), props.queryData); + const {plugins} = this.context; + return plugins.getSlotElements(props.fill, props.reduxState, this.getSlotProps(props), props.queryData); } render() { + const {plugins} = this.context; const {inline = false, className, reduxState, defaultComponent: DefaultComponent, queryData} = this.props; let children = this.getChildren(); const pluginConfig = reduxState.config.pluginConfig || emptyConfig; if (children.length === 0 && DefaultComponent) { - children = ; + const props = plugins.getSlotComponentProps(DefaultComponent, reduxState, this.getSlotProps(this.props), queryData); + children = ; } return ( diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index 0f44af43f..1350244f6 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -8,6 +8,8 @@ class TalkProvider extends React.Component { eventEmitter: this.props.eventEmitter, pym: this.props.pym, plugins: this.props.plugins, + rest: this.props.rest, + graphqlRegistry: this.props.graphqlRegistry, }; } @@ -24,7 +26,9 @@ class TalkProvider extends React.Component { TalkProvider.childContextTypes = { pym: PropTypes.object, eventEmitter: PropTypes.object, - plugins: PropTypes.array, + plugins: PropTypes.object, + rest: PropTypes.func, + graphqlRegistry: PropTypes.object, }; export default TalkProvider; diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js deleted file mode 100644 index b73c89c64..000000000 --- a/client/coral-framework/helpers/plugins.js +++ /dev/null @@ -1,166 +0,0 @@ -import React from 'react'; -import uniq from 'lodash/uniq'; -import pick from 'lodash/pick'; -import merge from 'lodash/merge'; -import flattenDeep from 'lodash/flattenDeep'; -import isEmpty from 'lodash/isEmpty'; -import flatten from 'lodash/flatten'; -import mapValues from 'lodash/mapValues'; -import {getDisplayName} from 'coral-framework/helpers/hoc'; -import camelize from './camelize'; -import plugins from 'pluginsConfig'; -import uuid from 'uuid/v4'; - -// This is returned for pluginConfig when it is empty. -const emptyConfig = {}; - -export function getSlotComponents(slot, reduxState, props = {}, queryData = {}) { - const pluginConfig = reduxState.config.plugin_config || emptyConfig; - return flatten(plugins - - // Filter out components that have slots and have been disabled in `plugin_config` - .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components)) - - .filter((o) => o.module.slots[slot]) - .map((o) => o.module.slots[slot]) - ) - .filter((component) => { - if(!component.isExcluded) { - return true; - } - let resolvedProps = getSlotComponentProps(component, reduxState, props, queryData); - if (component.mapStateToProps) { - resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)}; - } - return !component.isExcluded(resolvedProps); - }); -} - -export function isSlotEmpty(slot, reduxState, props = {}, queryData = {}) { - return getSlotComponents(slot, reduxState, props, queryData).length === 0; -} - -// Memoize the warnings so we only show them once. -const memoizedWarnings = []; - -// withWarnings decorates the props of queryData with a proxy that -// prints a warning when accessing deeper props. -function withWarnings(component, queryData) { - if (process.env.NODE_ENV !== 'production' && window.Proxy) { - - // Show warnings when accessing queryData only when not in production. - return mapValues(queryData, (value, key) => { - - // Keep null values.. - if (!queryData[key]) { - return queryData[key]; - } - return new Proxy(queryData[key], { - get(target, name) { - - // Only care about the components defined in the plugins. - if (component.talkPluginName) { - const warning = `'${getDisplayName(component)}' of '${component.talkPluginName}' accessed '${key}.${name}' but did not define fragments using the withFragment HOC`; - if (memoizedWarnings.indexOf(warning) === -1) { - console.warn(warning); - memoizedWarnings.push(warning); - } - } - return queryData[key][name]; - } - }); - }); - } - - return queryData; -} - -/** - * getSlotComponentProps calculate the props we would pass to the slot component. - * query datas are only passed to the component if it is defined in `component.fragments`. - */ -export function getSlotComponentProps(component, reduxState, props, queryData) { - const pluginConfig = reduxState.config.plugin_config || emptyConfig; - return { - ...props, - config: pluginConfig, - ...( - component.fragments - ? pick(queryData, Object.keys(component.fragments)) - : withWarnings(component, queryData) - ) - }; -} - -/** - * Returns React Elements for given slot. - */ -export function getSlotElements(slot, reduxState, props = {}, queryData = {}) { - return getSlotComponents(slot, reduxState, props, queryData) - .map((component, i) => { - return React.createElement(component, {key: i, ...getSlotComponentProps(component, reduxState, props, queryData)}); - }); -} - -export function getSlotFragments(slot, part) { - const components = uniq(flattenDeep(plugins - .filter((o) => o.module.slots ? o.module.slots[slot] : false) - .map((o) => o.module.slots[slot]) - )); - - const documents = components - .map((c) => c.fragments) - .filter((fragments) => fragments && fragments[part]) - .reduce((res, fragments) => { - res.push(fragments[part]); - return res; - }, []); - - return documents; -} - -export function getGraphQLExtensions() { - return plugins - .map((o) => pick(o.module, ['mutations', 'queries', 'fragments'])) - .filter((o) => !isEmpty(o)); -} - -export function getModQueueConfigs() { - return merge(...plugins - .map((o) => o.module.modQueues) - .filter((o) => o)); -} - -export function getTranslations(plugins) { - return plugins - .map((o) => o.module.translations) - .filter((o) => o); -} - -export function getReducers(plugins) { - return merge( - ...plugins - .filter((o) => o.module.reducer) - .map((o) => ({[camelize(o.name)] : o.module.reducer})) - ); -} - -function addMetaDataToSlotComponents() { - - // Add talkPluginName to Slot Components. - plugins.forEach((plugin) => { - const slots = plugin.module.slots; - slots && Object.keys(slots).forEach((slot) => { - slots[slot].forEach((component) => { - - // Attach plugin name to the component - component.talkPluginName = plugin.name; - - // Attach uuid to the component - component.talkUuid = uuid(); - }); - }); - }); -} - -addMetaDataToSlotComponents(); diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js index 9bc06290e..be6d07fed 100644 --- a/client/coral-framework/hocs/withFragments.js +++ b/client/coral-framework/hocs/withFragments.js @@ -1,9 +1,9 @@ import React from 'react'; import graphql from 'graphql-anywhere'; -import {resolveFragments} from 'coral-framework/services/graphqlRegistry'; import mapValues from 'lodash/mapValues'; import hoistStatics from 'recompose/hoistStatics'; import {getShallowChanges} from 'coral-framework/utils'; +import PropTypes from 'prop-types'; import union from 'lodash/union'; // TODO: Should not depend on `props.data` @@ -63,7 +63,22 @@ function hasEqualLeaves(a, b, path = '') { export default (fragments) => hoistStatics((BaseComponent) => { class WithFragments extends React.Component { - fragments = mapValues(fragments, (val) => resolveFragments(val)); + static contextTypes = { + graphqlRegistry: PropTypes.object, + }; + + get graphqlRegistry() { + return this.context.graphqlRegistry; + } + + resolveDocument(documentOrCallback) { + const document = typeof documentOrCallback === 'function' + ? documentOrCallback(this.props, this.context) + : documentOrCallback; + return this.graphqlRegistry.resolveFragments(document); + } + + fragments = mapValues(fragments, (val) => this.resolveDocument(val)); fragmentKeys = Object.keys(fragments).sort(); // Cache variables between lifecycles to speed up render. diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js index 7c3dcfa1a..46ec19eb7 100644 --- a/client/coral-framework/hocs/withMutation.js +++ b/client/coral-framework/hocs/withMutation.js @@ -4,7 +4,6 @@ import merge from 'lodash/merge'; import uniq from 'lodash/uniq'; import flatten from 'lodash/flatten'; import isEmpty from 'lodash/isEmpty'; -import {getMutationOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry'; import {getDefinitionName, getResponseErrors} from '../utils'; import PropTypes from 'prop-types'; import t from 'coral-framework/services/i18n'; @@ -43,8 +42,20 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { static contextTypes = { eventEmitter: PropTypes.object, store: PropTypes.object, + graphqlRegistry: PropTypes.object, }; + get graphqlRegistry() { + return this.context.graphqlRegistry; + } + + resolveDocument(documentOrCallback) { + const document = typeof documentOrCallback === 'function' + ? documentOrCallback(this.props, this.context) + : documentOrCallback; + return this.graphqlRegistry.resolveFragments(document); + } + // Lazily resolve fragments from graphRegistry to support circular dependencies. memoized = null; @@ -56,7 +67,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { propsWrapper = (data) => { const name = getDefinitionName(document); - const callbacks = getMutationOptions(name); + const callbacks = this.graphqlRegistry.getMutationOptions(name); const mutate = (base) => { const variables = base.variables || config.options.variables; const configs = callbacks.map((cb) => cb({variables, state: this.context.store.getState()})); @@ -167,7 +178,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { getWrapped = () => { if (!this.memoized) { - this.memoized = graphql(resolveFragments(document), {...config, props: this.propsWrapper})(WrappedComponent); + this.memoized = graphql(this.resolveDocument(document), {...config, props: this.propsWrapper})(WrappedComponent); } return this.memoized; }; diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index f19c3209e..c0825f28b 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -1,6 +1,5 @@ import * as React from 'react'; import {graphql} from 'react-apollo'; -import {getQueryOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry'; import {getDefinitionName, separateDataAndRoot, getResponseErrors} from '../utils'; import PropTypes from 'prop-types'; import hoistStatics from 'recompose/hoistStatics'; @@ -37,17 +36,28 @@ function networkStatusToString(networkStatus) { * apply query options registered in the graphRegistry. */ export default (document, config = {}) => hoistStatics((WrappedComponent) => { - const name = getDefinitionName(document); - return class WithQuery extends React.Component { static contextTypes = { eventEmitter: PropTypes.object, + graphqlRegistry: PropTypes.object, }; // Lazily resolve fragments from graphRegistry to support circular dependencies. memoized = null; lastNetworkStatus = null; data = null; + name = ''; + + get graphqlRegistry() { + return this.context.graphqlRegistry; + } + + resolveDocument(documentOrCallback) { + const document = typeof documentOrCallback === 'function' + ? documentOrCallback(this.props, this.context) + : documentOrCallback; + return this.graphqlRegistry.resolveFragments(document); + } emitWhenNeeded(data) { const {variables, networkStatus} = data; @@ -93,11 +103,12 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { refetch: data.refetch, updateQuery: data.updateQuery, subscribeToMore: (stmArgs) => { + const resolvedDocument = this.resolveDocument(stmArgs.document); // Resolve document fragments before passing it to `apollo-client`. return data.subscribeToMore({ ...stmArgs, - document: resolveFragments(stmArgs.document), + document: resolvedDocument, onError: (err) => { if (stmArgs.onErr) { return stmArgs.onErr(err); @@ -107,7 +118,8 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { }); }, fetchMore: (lmArgs) => { - const fetchName = getDefinitionName(lmArgs.query); + const resolvedDocument = this.resolveDocument(lmArgs.query); + const fetchName = getDefinitionName(resolvedDocument); this.context.eventEmitter.emit( `query.${name}.fetchMore.${fetchName}.begin`, {variables: lmArgs.variables}); @@ -115,7 +127,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { // Resolve document fragments before passing it to `apollo-client`. return data.fetchMore({ ...lmArgs, - query: resolveFragments(lmArgs.query), + query: resolvedDocument, }) .then((res) => { this.context.eventEmitter.emit( @@ -156,7 +168,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { const base = (typeof this.wrappedConfig.options === 'function') ? this.wrappedConfig.options(data) : this.wrappedConfig.options; - const configs = getQueryOptions(name); + const configs = this.graphqlRegistry.getQueryOptions(name); const reducerCallbacks = [base.reducer || ((i) => i)] .concat(...configs.map((cfg) => cfg.reducer)) @@ -178,8 +190,10 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { getWrapped = () => { if (!this.memoized) { + const resolvedDocument = this.resolveDocument(document); + this.name = getDefinitionName(resolvedDocument); this.memoized = graphql( - resolveFragments(document), + resolvedDocument, {...this.wrappedConfig, options: this.wrappedOptions}, )(WrappedComponent); } diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index 6a18695f7..0aa2c4c35 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -6,10 +6,12 @@ import {createReduxEmitter} from './events'; import {createRestClient} from './rest'; import thunk from 'redux-thunk'; import {loadTranslations} from './i18n'; -import {getTranslations, getReducers} from '../helpers/plugins'; import bowser from 'bowser'; import * as Storage from '../helpers/storage'; import {BASE_PATH} from 'coral-framework/constants/url'; +import {createPluginsService} from './plugins'; +import {createGraphQLRegistry} from './graphqlRegistry'; +import globalFragments from 'coral-framework/graphql/fragments'; /** * getAuthToken returns the active auth token or null @@ -34,7 +36,7 @@ const getAuthToken = (store) => { return null; }; -export function createContext(reducers, plugins) { +export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}}) { const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); let store = null; @@ -56,16 +58,28 @@ export function createContext(reducers, plugins) { liveUri: `${protocol}://${location.host}${BASE_PATH}api/v1/live`, token, }); + const plugins = createPluginsService(pluginsConfig); + const graphqlRegistry = createGraphQLRegistry(plugins.getSlotFragments.bind(plugins)); const context = { client, pym, plugins, eventEmitter, rest, + graphqlRegistry, }; + // Load framework fragments. + Object.keys(globalFragments).forEach((key) => graphqlRegistry.addFragment(key, globalFragments[key])); + + // Register graphql extension + graphqlRegistry.add(graphqlExtension); + + // Register plugin graphql extensions. + plugins.getGraphQLExtensions().forEach((ext) => graphqlRegistry.add(ext)); + // Load plugin translations. - getTranslations(plugins).forEach((t) => loadTranslations(t)); + plugins.getTranslations().forEach((t) => loadTranslations(t)); // Pass any events through our parent. eventEmitter.onAny((eventName, value) => { @@ -74,7 +88,7 @@ export function createContext(reducers, plugins) { const finalReducers = { ...reducers, - ...getReducers(plugins), + ...plugins.getReducers(), apollo: client.reducer(), }; diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index a354d68bc..ebd248f0d 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -64,11 +64,6 @@ export function createClient(options = {}) { }); client.resetWebsocket = () => { - if (wsClient === null) { - - // Nothing to reset! - return; - } // Close socket connection which will also unregister subscriptions on the server-side. wsClient.close(); diff --git a/client/coral-framework/services/graphqlRegistry.js b/client/coral-framework/services/graphqlRegistry.js index 104ec41dd..5ca549183 100644 --- a/client/coral-framework/services/graphqlRegistry.js +++ b/client/coral-framework/services/graphqlRegistry.js @@ -1,6 +1,4 @@ import {getDefinitionName, mergeDocuments} from 'coral-framework/utils'; -import {getGraphQLExtensions, getSlotFragments} from 'coral-framework/helpers/plugins'; -import globalFragments from 'coral-framework/graphql/fragments'; import uniq from 'lodash/uniq'; import {gql} from 'react-apollo'; @@ -14,272 +12,262 @@ import {gql} from 'react-apollo'; */ gql.disableFragmentWarnings(); -const fragments = {}; -const mutationOptions = {}; -const queryOptions = {}; - const getTypeName = (ast) => ast.definitions[0].typeCondition.name.value; -/** - * Add fragment - * - * Example: - * addFragment('MyFragment', gql` - * fragment Plugin_MyFragment on Comment { - * body - * } - * `); - */ -export function addFragment(key, document) { - const type = getTypeName(document); - const name = getDefinitionName(document); - if (!(key in fragments)) { - fragments[key] = {type, names: [name], documents: [document]}; - } else { - if (type !== fragments[key].type) { - console.error(`Type mismatch ${type} !== ${fragments[key].type}`); +class GraphQLRegistry { + fragments = {}; + mutationOptions = {}; + queryOptions = {}; + + constructor(getSlotFragments) { + this.getSlotFragments = getSlotFragments; + } + + /** + * Add fragment + * + * Example: + * addFragment('MyFragment', gql` + * fragment Plugin_MyFragment on Comment { + * body + * } + * `); + */ + addFragment(key, document) { + const type = getTypeName(document); + const name = getDefinitionName(document); + if (!(key in this.fragments)) { + this.fragments[key] = {type, names: [name], documents: [document]}; + } else { + if (type !== this.fragments[key].type) { + console.error(`Type mismatch ${type} !== ${this.fragments[key].type}`); + } + this.fragments[key].names.push(name); + this.fragments[key].documents.push(document); } - fragments[key].names.push(name); - fragments[key].documents.push(document); - } -} - -/** - * Add mutation options. - * - * Example: - * // state is the current redux state, which is sometimes - * // necessary to fill the optimistic response. - * addMutationOptions('PostComment', ({variables, state}) => ({ - * optimisticResponse: { - * CreateComment: { - * extra: '', - * }, - * }, - * refetchQueries: [], - * updateQueries: { - * EmbedQuery: (previous, data) => { - * return previous; - * }, - * }, - * update: (proxy, result) => { - * }, - * }) - */ -export function addMutationOptions(key, config) { - if (!(key in mutationOptions)) { - mutationOptions[key] = [config]; - } else { - mutationOptions[key].push(config); - } -} - -/** - * Add query options. - * - * Example: - * addQueryOptions('EmbedQuery', { - * reducer: (previousResult, action, variables) => previousResult, - * }); - */ -export function addQueryOptions(key, config) { - if (!(key in queryOptions)) { - queryOptions[key] = [config]; - } else { - queryOptions[key].push(config); - } -} - -/** - * Add all fragments, mutation options, and query options defined in the object. - * - * Example: - * add({ - * fragments: { - * CreateCommentResponse: gql` - * fragment CoralRandomEmoji_CreateCommentResponse on CreateCommentResponse { - * [...] - * }`, - * }, - * mutations: { - * // state is the current redux state, which is sometimes - * // necessary to fill the optimistic response. - * PostComment: ({variables, state}) => ({ - * optimisticResponse: { - * [...] - * }, - * refetchQueries: [], - * updateQueries: { - * EmbedQuery: (previous, data) => { - * return previous; - * }, - * }, - * update: (proxy, result) => { - * }, - * }) - * }, - * queries: { - * EmbedQuery: { - * reducer: (previousResult, action, variables) => { - * return previousResult; - * }, - * }, - * }, - * }); - */ -export function add(extension) { - Object.keys(extension.fragments || []).forEach((key) => addFragment(key, extension.fragments[key])); - Object.keys(extension.mutations || []).forEach((key) => addMutationOptions(key, extension.mutations[key])); - Object.keys(extension.queries || []).forEach((key) => addQueryOptions(key, extension.queries[key])); -} - -/** - * Get a list of mutation options. - */ -export function getMutationOptions(key) { - init(); - return mutationOptions[key] || []; -} - -/** - * Get a list of query options. - */ -export function getQueryOptions(key) { - init(); - return queryOptions[key] || []; -} - -/** - * getSlotFragmentDocument handles `key`s in the form of TalkSlot_SlotName_Resource. - * It parses the slot name and the resource and usees the plugin API to assemble - * the fragment document. - */ -function getSlotFragmentDocument(key) { - const match = key.match(/TalkSlot_(.*)_(.*)/); - if (!match) { - return ''; } - const slot = match[1][0].toLowerCase() + match[1].substr(1); - const resource = match[2]; - const documents = getSlotFragments(slot, resource); - - if (documents.length === 0) { - return ''; - } - - const names = documents.map((d) => getDefinitionName(d)); - const typeName = getTypeName(documents[0]); - - // Assemble arguments for `gql` to call it directly without using template literals. - const main = ` - fragment ${key} on ${typeName} { - ...${names.join('\n...')}\n + /** + * Add mutation options. + * + * Example: + * // state is the current redux state, which is sometimes + * // necessary to fill the optimistic response. + * addMutationOptions('PostComment', ({variables, state}) => ({ + * optimisticResponse: { + * CreateComment: { + * extra: '', + * }, + * }, + * refetchQueries: [], + * updateQueries: { + * EmbedQuery: (previous, data) => { + * return previous; + * }, + * }, + * update: (proxy, result) => { + * }, + * }) + */ + addMutationOptions(key, config) { + if (!(key in this.mutationOptions)) { + this.mutationOptions[key] = [config]; + } else { + this.mutationOptions[key].push(config); } - `; - return mergeDocuments([main, ...documents]); -} - -/** - * getRegistryFragmentDocument assembles a fragment document using - * all registered fragment under given `key`. - */ -function getRegistryFragmentDocument(key) { - if (!(key in fragments)) { - return ''; } - let documents = fragments[key].documents; - let fields = `...${fragments[key].names.join('\n...')}\n`; - - // Assemble arguments for `gql` to call it directly without using template literals. - const main = ` - fragment ${key} on ${fragments[key].type} { - ${fields} + /** + * Add query options. + * + * Example: + * addQueryOptions('EmbedQuery', { + * reducer: (previousResult, action, variables) => previousResult, + * }); + */ + addQueryOptions(key, config) { + if (!(key in this.queryOptions)) { + this.queryOptions[key] = [config]; + } else { + this.queryOptions[key].push(config); } - `; - return mergeDocuments([main, ...documents]); -} + } -/** - * getFragmentDocument returns a fragment that assembles all registered - * fragments under given `key` or if `key` refers to Slot fragments it will - * return the slot fragments specified by this key. - */ -export function getFragmentDocument(key) { - init(); - return getRegistryFragmentDocument(key) || getSlotFragmentDocument(key); -} + /** + * Add all fragments, mutation options, and query options defined in the object. + * + * Example: + * add({ + * fragments: { + * CreateCommentResponse: gql` + * fragment CoralRandomEmoji_CreateCommentResponse on CreateCommentResponse { + * [...] + * }`, + * }, + * mutations: { + * // state is the current redux state, which is sometimes + * // necessary to fill the optimistic response. + * PostComment: ({variables, state}) => ({ + * optimisticResponse: { + * [...] + * }, + * refetchQueries: [], + * updateQueries: { + * EmbedQuery: (previous, data) => { + * return previous; + * }, + * }, + * update: (proxy, result) => { + * }, + * }) + * }, + * queries: { + * EmbedQuery: { + * reducer: (previousResult, action, variables) => { + * return previousResult; + * }, + * }, + * }, + * }); + */ + add(extension) { + Object.keys(extension.fragments || []).forEach((key) => this.addFragment(key, extension.fragments[key])); + Object.keys(extension.mutations || []).forEach((key) => this.addMutationOptions(key, extension.mutations[key])); + Object.keys(extension.queries || []).forEach((key) => this.addQueryOptions(key, extension.queries[key])); + } -// The fragments and configs are lazily loaded to allow circular dependencies to work. -// TODO: We might want to change this to an explicit add after we have lazy Queries and Mutations. -let initialized = false; + /** + * Get a list of mutation options. + */ + getMutationOptions(key) { + return this.mutationOptions[key] || []; + } -function init() { - if (initialized) { return; } - initialized = true; + /** + * Get a list of query options. + */ + getQueryOptions(key) { + return this.queryOptions[key] || []; + } - // Add fragments from framework. - [globalFragments].forEach((map) => - Object.keys(map).forEach((key) => addFragment(key, map[key])) - ); - - // Add configs from plugins. - getGraphQLExtensions().forEach((ext) => add(ext)); -} - -/** - * resolveFragments finds fragment spread names and attachs - * the related fragment document to the given root document. - */ -export function resolveFragments(document) { - if (document.loc.source) { - - // Remember keys that we have already resolved. - const resolvedKeys = []; - - // Spreads from slots that are empty and need to be removed. - // (works around the issue that we don't know the resource type - // if we don't have a fragment) - const spreadsToBeRemoved = []; - - // fragments to be attached. - const subFragments = []; - - // body contains the final result. - let body = document.loc.source.body; - - let done = false; - while (!done) { - done = true; - - const matchedSubFragments = body.match(/\.\.\.([_a-zA-Z][_a-zA-Z0-9]*)/g) || []; - uniq(matchedSubFragments.map((f) => f.replace('...', ''))) - .filter((key) => resolvedKeys.indexOf(key) === -1) - .forEach((key) => { - const doc = getFragmentDocument(key); - if (doc) { - subFragments.push(doc); - - // We found a new fragment, so we are not done yet. - done = false; - } else if(key.startsWith('TalkSlot_')) { - spreadsToBeRemoved.push(key); - } - resolvedKeys.push(key); - }); - - body = mergeDocuments([body, ...subFragments]).loc.source.body; + /** + * getSlotFragmentDocument handles `key`s in the form of TalkSlot_SlotName_Resource. + * It parses the slot name and the resource and usees the plugin API to assemble + * the fragment document. + */ + getSlotFragmentDocument(key) { + const match = key.match(/TalkSlot_(.*)_(.*)/); + if (!match) { + return ''; } - spreadsToBeRemoved.forEach((key) => { - const regex = new RegExp(`\\.\\.\\.${key}\n`, 'g'); - body = body.replace(regex, ''); - }); + const slot = match[1][0].toLowerCase() + match[1].substr(1); + const resource = match[2]; + const documents = this.getSlotFragments(slot, resource); - return gql`${body}`; - } else { - console.warn('Can only resolve fragments from documents definied using the gql tag.'); + if (documents.length === 0) { + return ''; + } + + const names = documents.map((d) => getDefinitionName(d)); + const typeName = getTypeName(documents[0]); + + // Assemble arguments for `gql` to call it directly without using template literals. + const main = ` + fragment ${key} on ${typeName} { + ...${names.join('\n...')}\n + } + `; + return mergeDocuments([main, ...documents]); + } + + /** + * getRegistryFragmentDocument assembles a fragment document using + * all registered fragment under given `key`. + */ + getRegistryFragmentDocument(key) { + if (!(key in this.fragments)) { + return ''; + } + + let documents = this.fragments[key].documents; + let fields = `...${this.fragments[key].names.join('\n...')}\n`; + + // Assemble arguments for `gql` to call it directly without using template literals. + const main = ` + fragment ${key} on ${this.fragments[key].type} { + ${fields} + } + `; + return mergeDocuments([main, ...documents]); + } + + /** + * getFragmentDocument returns a fragment that assembles all registered + * fragments under given `key` or if `key` refers to Slot fragments it will + * return the slot fragments specified by this key. + */ + getFragmentDocument(key) { + return this.getRegistryFragmentDocument(key) || this.getSlotFragmentDocument(key); + } + + /** + * resolveFragments finds fragment spread names and attachs + * the related fragment document to the given root document. + */ + resolveFragments(document) { + if (document.loc.source) { + + // Remember keys that we have already resolved. + const resolvedKeys = []; + + // Spreads from slots that are empty and need to be removed. + // (works around the issue that we don't know the resource type + // if we don't have a fragment) + const spreadsToBeRemoved = []; + + // fragments to be attached. + const subFragments = []; + + // body contains the final result. + let body = document.loc.source.body; + + let done = false; + while (!done) { + done = true; + + const matchedSubFragments = body.match(/\.\.\.([_a-zA-Z][_a-zA-Z0-9]*)/g) || []; + uniq(matchedSubFragments.map((f) => f.replace('...', ''))) + .filter((key) => resolvedKeys.indexOf(key) === -1) + .forEach((key) => { + const doc = this.getFragmentDocument(key); + if (doc) { + subFragments.push(doc); + + // We found a new fragment, so we are not done yet. + done = false; + } else if(key.startsWith('TalkSlot_')) { + spreadsToBeRemoved.push(key); + } + resolvedKeys.push(key); + }); + + body = mergeDocuments([body, ...subFragments]).loc.source.body; + } + + spreadsToBeRemoved.forEach((key) => { + const regex = new RegExp(`\\.\\.\\.${key}\n`, 'g'); + body = body.replace(regex, ''); + }); + + return gql`${body}`; + } else { + console.warn('Can only resolve fragments from documents definied using the gql tag.'); + } + return document; } - return document; +} + +export function createGraphQLRegistry(getSlotFragments) { + return new GraphQLRegistry(getSlotFragments); } diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js new file mode 100644 index 000000000..f51e33702 --- /dev/null +++ b/client/coral-framework/services/plugins.js @@ -0,0 +1,174 @@ +import React from 'react'; +import uniq from 'lodash/uniq'; +import pick from 'lodash/pick'; +import merge from 'lodash/merge'; +import flattenDeep from 'lodash/flattenDeep'; +import isEmpty from 'lodash/isEmpty'; +import flatten from 'lodash/flatten'; +import mapValues from 'lodash/mapValues'; +import {getDisplayName} from 'coral-framework/helpers/hoc'; +import camelize from '../helpers/camelize'; +import uuid from 'uuid/v4'; + +// This is returned for pluginConfig when it is empty. +const emptyConfig = {}; + +// Memoize the warnings so we only show them once. +const memoizedWarnings = []; + +// withWarnings decorates the props of queryData with a proxy that +// prints a warning when accessing deeper props. +function withWarnings(component, queryData) { + if (process.env.NODE_ENV !== 'production' && window.Proxy) { + + // Show warnings when accessing queryData only when not in production. + return mapValues(queryData, (value, key) => { + + // Keep null values.. + if (!queryData[key]) { + return queryData[key]; + } + return new Proxy(queryData[key], { + get(target, name) { + + // Only care about the components defined in the plugins. + if (component.talkPluginName) { + const warning = `'${getDisplayName(component)}' of '${component.talkPluginName}' accessed '${key}.${name}' but did not define fragments using the withFragment HOC`; + if (memoizedWarnings.indexOf(warning) === -1) { + console.warn(warning); + memoizedWarnings.push(warning); + } + } + return queryData[key][name]; + } + }); + }); + } + + return queryData; +} + +function addMetaDataToSlotComponents(plugins) { + + // Add talkPluginName to Slot Components. + plugins.forEach((plugin) => { + const slots = plugin.module.slots; + slots && Object.keys(slots).forEach((slot) => { + slots[slot].forEach((component) => { + + // Attach plugin name to the component + component.talkPluginName = plugin.name; + + // Attach uuid to the component + component.talkUuid = uuid(); + }); + }); + }); +} + +class PluginsService { + constructor(plugins) { + this.plugins = plugins; + addMetaDataToSlotComponents(plugins); + } + + getSlotComponents(slot, reduxState, props = {}, queryData = {}) { + const pluginConfig = reduxState.config.plugin_config || emptyConfig; + return flatten(this.plugins + + // Filter out components that have slots and have been disabled in `plugin_config` + .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components)) + + .filter((o) => o.module.slots[slot]) + .map((o) => o.module.slots[slot]) + ) + .filter((component) => { + if(!component.isExcluded) { + return true; + } + let resolvedProps = this.getSlotComponentProps(component, reduxState, props, queryData); + if (component.mapStateToProps) { + resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)}; + } + return !component.isExcluded(resolvedProps); + }); + } + + isSlotEmpty(slot, reduxState, props = {}, queryData = {}) { + return this.getSlotComponents(slot, reduxState, props, queryData).length === 0; + } + + /** + * getSlotComponentProps calculate the props we would pass to the slot component. + * query datas are only passed to the component if it is defined in `component.fragments`. + */ + getSlotComponentProps(component, reduxState, props, queryData) { + const pluginConfig = reduxState.config.plugin_config || emptyConfig; + return { + ...props, + config: pluginConfig, + ...( + component.fragments + ? pick(queryData, Object.keys(component.fragments)) + : withWarnings(component, queryData) + ) + }; + } + + /** + * Returns React Elements for given slot. + */ + getSlotElements(slot, reduxState, props = {}, queryData = {}) { + return this.getSlotComponents(slot, reduxState, props, queryData) + .map((component, i) => { + return React.createElement(component, {key: i, ...this.getSlotComponentProps(component, reduxState, props, queryData)}); + }); + } + + getSlotFragments(slot, part) { + const components = uniq(flattenDeep(this.plugins + .filter((o) => o.module.slots ? o.module.slots[slot] : false) + .map((o) => o.module.slots[slot]) + )); + + const documents = components + .map((c) => c.fragments) + .filter((fragments) => fragments && fragments[part]) + .reduce((res, fragments) => { + res.push(fragments[part]); + return res; + }, []); + + return documents; + } + + getGraphQLExtensions() { + return this.plugins + .map((o) => pick(o.module, ['mutations', 'queries', 'fragments'])) + .filter((o) => !isEmpty(o)); + } + + getModQueueConfigs() { + return merge(...this.plugins + .map((o) => o.module.modQueues) + .filter((o) => o)); + } + + getTranslations() { + return this.plugins + .map((o) => o.module.translations) + .filter((o) => o); + } + + getReducers() { + return merge( + ...this.plugins + .filter((o) => o.module.reducer) + .map((o) => ({[camelize(o.name)] : o.module.reducer})) + ); + } +} + +export function createPluginsService(plugins) { + return new PluginsService(plugins); +} diff --git a/plugin-api/beta/client/services/index.js b/plugin-api/beta/client/services/index.js index 54ad90fe7..4d2281dc8 100644 --- a/plugin-api/beta/client/services/index.js +++ b/plugin-api/beta/client/services/index.js @@ -1,9 +1,3 @@ export {t, timeago} from 'coral-framework/services/i18n'; export {can} from 'coral-framework/services/perms'; -import {isSlotEmpty as ise} from 'coral-framework/helpers/plugins'; -// @TODO: Deprecated. -export function isSlotEmpty(...args) { - console.warn('A plugin is using `isSlotEmpty` which has been deprecated, please port to the new API using the `IfSlotIsEmpty` and `IfSlotIsNotEmpty` components.'); - return ise(...args); -} From 9fa1f3d958bfa7c88c1313fc8eced34b958611ba Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 22 Aug 2017 16:15:41 -0300 Subject: [PATCH 038/121] Adding auto escape --- client/coral-admin/src/components/CommentBodyHighlighter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/coral-admin/src/components/CommentBodyHighlighter.js b/client/coral-admin/src/components/CommentBodyHighlighter.js index 39be90d81..9a430f7c7 100644 --- a/client/coral-admin/src/components/CommentBodyHighlighter.js +++ b/client/coral-admin/src/components/CommentBodyHighlighter.js @@ -17,6 +17,7 @@ export default ({suspectWords, bannedWords, body, ...rest}) => { return ( From 23f4724f99c1fe681865c88a36ff28b14c0278f9 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 22 Aug 2017 13:18:48 -0600 Subject: [PATCH 039/121] fixed template bug --- routes/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/index.js b/routes/index.js index dafaf09b9..6a8920dc3 100644 --- a/routes/index.js +++ b/routes/index.js @@ -89,7 +89,7 @@ if (process.env.NODE_ENV !== 'production') { // Interactive graphiql interface. router.use('/api/v1/graph/iql', (req, res) => { res.render('graphiql', { - endpointURL: `${req.locals.BASE_URL}api/v1/graph/ql` + endpointURL: `${req.app.locals.BASE_URL}api/v1/graph/ql` }); }); From a882b3650103785991f30d82512a0cc21f555253 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 02:20:43 +0700 Subject: [PATCH 040/121] Fix connection bug --- client/coral-framework/services/client.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index ebd248f0d..ce83d501f 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -11,13 +11,17 @@ export const apolloErrorReporter = () => (next) => (action) => { return next(action); }; +function resolveToken(token) { + return typeof token === 'function' ? token() : token; +} + export function createClient(options = {}) { const {token, uri, liveUri, ...apolloOptions} = options; const wsClient = new SubscriptionClient(liveUri, { reconnect: true, lazy: true, connectionParams: { - token, + get token() { return resolveToken(token); }, } }); @@ -34,7 +38,7 @@ export function createClient(options = {}) { req.options.headers = {}; // Create the header object if needed. } - let authToken = typeof token === 'function' ? token() : token; + let authToken = resolveToken(token); if (authToken) { req.options.headers['authorization'] = `Bearer ${authToken}`; } From a9347226722c9e48672479d9fe625d1c9843c14c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 02:23:18 +0700 Subject: [PATCH 041/121] Cleanup --- client/coral-admin/src/index.js | 8 +++----- client/coral-embed-stream/src/index.js | 4 +--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index 8b90f5acf..f433af4c2 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -9,14 +9,12 @@ import 'react-mdl/extra/material.js'; import graphqlExtension from './graphql'; import pluginsConfig from 'pluginsConfig'; -const context = createContext({reducers, graphqlExtension, pluginsConfig}); - smoothscroll.polyfill(); +const context = createContext({reducers, graphqlExtension, pluginsConfig}); + render( - + , document.querySelector('#root') diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index dca4e31b4..5d72ea3e7 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -52,9 +52,7 @@ if (!window.opener) { } render( - + , document.querySelector('#talk-embed-stream-container') From 1ee7c8167587f585807ca9eba75fbe99d64dd41d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 02:24:26 +0700 Subject: [PATCH 042/121] Remove admin store impl --- client/coral-admin/src/services/store.js | 31 ------------------------ 1 file changed, 31 deletions(-) delete mode 100644 client/coral-admin/src/services/store.js diff --git a/client/coral-admin/src/services/store.js b/client/coral-admin/src/services/store.js deleted file mode 100644 index ca29c15c8..000000000 --- a/client/coral-admin/src/services/store.js +++ /dev/null @@ -1,31 +0,0 @@ -import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; -import thunk from 'redux-thunk'; -import mainReducer from '../reducers'; -import {getClient} from 'coral-framework/services/client'; - -const middlewares = [ - applyMiddleware(getClient().middleware()), - applyMiddleware(thunk) -]; - -if (window.devToolsExtension) { - - // we can't have the last argument of compose() be undefined - middlewares.push(window.devToolsExtension()); -} - -const coralReducers = { - ...mainReducer, - apollo: getClient().reducer() -}; - -const store = createStore( - combineReducers(coralReducers), - {}, - compose(...middlewares) -); - -store.coralReducers = coralReducers; - -window.coralStore = store; -export default store; From b6951488c42fdba9f17e6f59010066bc9e9db71e Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 22 Aug 2017 16:37:24 -0300 Subject: [PATCH 043/121] Tokenizer is messing with escaped regexes --- services/wordlist.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/wordlist.js b/services/wordlist.js index 2ae8ed90c..477baace6 100644 --- a/services/wordlist.js +++ b/services/wordlist.js @@ -9,6 +9,10 @@ const Errors = require('../errors'); // REGEX to prevent emoji's from entering the wordlist. const EMOJI_REGEX = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?)*/; +const escapeRegExp = function(s) { + return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); +}; + /** * The root wordlist object. * @type {Object} @@ -74,7 +78,7 @@ class Wordlist { return [word]; } - return tokenizer.tokenize(word.toLowerCase()); + return tokenizer.tokenize(escapeRegExp(word.toLowerCase())); }) .filter((tokens) => { if (tokens.length === 0) { From f428bfa6c9cfc1aab7bfb46f4db6ac42d241170d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 03:16:19 +0700 Subject: [PATCH 044/121] WIP --- .../coral-framework/actions/notification.js | 24 ++++++++++++------- .../components/TalkProvider.js | 2 ++ .../coral-framework/constants/notification.js | 3 +-- client/coral-framework/services/bootstrap.js | 9 ++++++- .../coral-framework/services/notification.js | 13 ++++++++++ 5 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 client/coral-framework/services/notification.js diff --git a/client/coral-framework/actions/notification.js b/client/coral-framework/actions/notification.js index 57a7950f5..f940ab3b4 100644 --- a/client/coral-framework/actions/notification.js +++ b/client/coral-framework/actions/notification.js @@ -1,12 +1,18 @@ -import pym from '../services/pym'; import * as actions from '../constants/notification'; -export const addNotification = (notifType, text) => { - pym.sendMessage('coral-alert', `${notifType}|${text}`); - return {type: actions.ADD_NOTIFICATION, notifType, text}; -}; - -export const clearNotification = () => { - pym.sendMessage('coral-clear-notification'); - return {type: actions.CLEAR_NOTIFICATION}; +export const notify = (kind, msg) => (dispatch, _, {notification}) => { + switch (kind) { + case 'error': + notification.error(msg); + break; + case 'info': + notification.info(msg); + break; + case 'success': + notification.success(msg); + break; + default: + throw new Error(`Unknown notification kind ${kind}`); + } + dispatch({type: actions.NOTIFY, kind, msg}); }; diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index 1350244f6..b68fc7a1a 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -10,6 +10,7 @@ class TalkProvider extends React.Component { plugins: this.props.plugins, rest: this.props.rest, graphqlRegistry: this.props.graphqlRegistry, + notification: this.props.notification, }; } @@ -29,6 +30,7 @@ TalkProvider.childContextTypes = { plugins: PropTypes.object, rest: PropTypes.func, graphqlRegistry: PropTypes.object, + notification: PropTypes.object, }; export default TalkProvider; diff --git a/client/coral-framework/constants/notification.js b/client/coral-framework/constants/notification.js index a7334119a..af5828622 100644 --- a/client/coral-framework/constants/notification.js +++ b/client/coral-framework/constants/notification.js @@ -1,2 +1 @@ -export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'; -export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION'; +export const NOTIFY = 'NOTIFY'; diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index 0aa2c4c35..aa85e9d21 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -10,6 +10,7 @@ import bowser from 'bowser'; import * as Storage from '../helpers/storage'; import {BASE_PATH} from 'coral-framework/constants/url'; import {createPluginsService} from './plugins'; +import {createNotificationService} from './notification'; import {createGraphQLRegistry} from './graphqlRegistry'; import globalFragments from 'coral-framework/graphql/fragments'; @@ -36,7 +37,7 @@ const getAuthToken = (store) => { return null; }; -export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}}) { +export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification}) { const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); let store = null; @@ -60,6 +61,11 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi }); const plugins = createPluginsService(pluginsConfig); const graphqlRegistry = createGraphQLRegistry(plugins.getSlotFragments.bind(plugins)); + if (!notification) { + + // Use default notification service (pym based) + notification = createNotificationService(pym); + } const context = { client, pym, @@ -67,6 +73,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi eventEmitter, rest, graphqlRegistry, + notification, }; // Load framework fragments. diff --git a/client/coral-framework/services/notification.js b/client/coral-framework/services/notification.js new file mode 100644 index 000000000..fa0e61b3f --- /dev/null +++ b/client/coral-framework/services/notification.js @@ -0,0 +1,13 @@ +export function createNotificationService(pym) { + return { + success(msg) { + pym.sendMessage('coral-alert', `success|${msg}`); + }, + error(msg) { + pym.sendMessage('coral-alert', `error|${msg}`); + }, + info(msg) { + pym.sendMessage('coral-alert', `info|${msg}`); + }, + }; +} From 89037a2e051196b04c2f452f7b86aa50bf7574f6 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 03:22:56 +0700 Subject: [PATCH 045/121] Refactor notification --- .../coral-embed-stream/src/actions/asset.js | 6 ++-- client/coral-embed-stream/src/actions/auth.js | 4 +-- .../src/components/AllCommentsPane.js | 6 ++-- .../src/components/Comment.js | 20 ++++++------ .../src/components/EditableCommentContent.js | 10 +++--- .../src/components/Stream.js | 10 +++--- .../src/components/TopRightMenu.js | 6 ++-- .../src/containers/Embed.js | 10 +++--- .../src/containers/Stream.js | 4 +-- .../coral-framework/actions/notification.js | 32 +++++++++++-------- client/coral-framework/utils/index.js | 6 ++++ client/talk-plugin-commentbox/CommentBox.js | 16 +++++----- .../components/FlagButton.js | 2 +- client/talk-plugin-history/CommentHistory.js | 2 +- client/talk-plugin-replies/ReplyBox.js | 6 ++-- 15 files changed, 75 insertions(+), 65 deletions(-) diff --git a/client/coral-embed-stream/src/actions/asset.js b/client/coral-embed-stream/src/actions/asset.js index 6acebeafb..1fc9f09f7 100644 --- a/client/coral-embed-stream/src/actions/asset.js +++ b/client/coral-embed-stream/src/actions/asset.js @@ -1,5 +1,5 @@ import * as actions from '../constants/asset'; -import {addNotification} from 'coral-framework/actions/notification'; +import {notify} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; @@ -16,7 +16,7 @@ export const updateConfiguration = (newConfig) => (dispatch, getState, {rest}) = dispatch(updateAssetSettingsRequest()); rest(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig}) .then(() => { - dispatch(addNotification('success', t('framework.success_update_settings'))); + dispatch(notify('success', t('framework.success_update_settings'))); dispatch(updateAssetSettingsSuccess(newConfig)); }) .catch((error) => { @@ -30,7 +30,7 @@ export const updateOpenStream = (closedBody) => (dispatch, getState, {rest}) => dispatch(fetchAssetRequest()); rest(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody}) .then(() => { - dispatch(addNotification('success', t('framework.success_update_settings'))); + dispatch(notify('success', t('framework.success_update_settings'))); dispatch(fetchAssetSuccess(closedBody)); }) .catch((error) => { diff --git a/client/coral-embed-stream/src/actions/auth.js b/client/coral-embed-stream/src/actions/auth.js index 55f7eb546..bf1679b1c 100644 --- a/client/coral-embed-stream/src/actions/auth.js +++ b/client/coral-embed-stream/src/actions/auth.js @@ -2,7 +2,7 @@ import jwtDecode from 'jwt-decode'; import bowser from 'bowser'; import * as actions from '../constants/auth'; import * as Storage from 'coral-framework/helpers/storage'; -import {addNotification} from 'coral-framework/actions/notification'; +import {notify} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; @@ -387,7 +387,7 @@ export const editName = (username) => (dispatch, _, {rest}) => { return rest('/account/username', {method: 'PUT', body: {username}}) .then(() => { dispatch(editUsernameSuccess()); - dispatch(addNotification('success', t('framework.success_name_update'))); + dispatch(notify('success', t('framework.success_name_update'))); }) .catch((error) => { console.error(error); diff --git a/client/coral-embed-stream/src/components/AllCommentsPane.js b/client/coral-embed-stream/src/components/AllCommentsPane.js index c141f0618..cb7c3cbb5 100644 --- a/client/coral-embed-stream/src/components/AllCommentsPane.js +++ b/client/coral-embed-stream/src/components/AllCommentsPane.js @@ -87,7 +87,7 @@ class AllCommentsPane extends React.Component { }) .catch((error) => { this.setState({loadingState: 'error'}); - forEachError(error, ({msg}) => {this.props.addNotification('error', msg);}); + forEachError(error, ({msg}) => {this.props.notify('error', msg);}); }); } @@ -129,7 +129,7 @@ class AllCommentsPane extends React.Component { ignoreUser, setActiveReplyBox, activeReplyBox, - addNotification, + notify, disableReply, postComment, asset, @@ -166,7 +166,7 @@ class AllCommentsPane extends React.Component { disableReply={disableReply} setActiveReplyBox={setActiveReplyBox} activeReplyBox={activeReplyBox} - addNotification={addNotification} + notify={notify} depth={0} postComment={postComment} asset={asset} diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index bb6fee981..522e8f00b 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -152,7 +152,7 @@ export default class Comment extends React.Component { deleteAction: PropTypes.func.isRequired, parentId: PropTypes.string, highlighted: PropTypes.string, - addNotification: PropTypes.func.isRequired, + notify: PropTypes.func.isRequired, postComment: PropTypes.func.isRequired, depth: PropTypes.number.isRequired, liveUpdates: PropTypes.bool, @@ -205,7 +205,7 @@ export default class Comment extends React.Component { onClickEdit (e) { e.preventDefault(); if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) { - this.props.addNotification('error', t('error.NOT_AUTHORIZED')); + this.props.notify('error', t('error.NOT_AUTHORIZED')); return; } this.setState({isEditing: true}); @@ -235,7 +235,7 @@ export default class Comment extends React.Component { }) .catch((error) => { this.setState({loadingState: 'error'}); - forEachError(error, ({msg}) => {this.props.addNotification('error', msg);}); + forEachError(error, ({msg}) => {this.props.notify('error', msg);}); }); emit('ui.Comment.showMoreReplies', {id}); return; @@ -252,7 +252,7 @@ export default class Comment extends React.Component { if (can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) { this.props.setActiveReplyBox(this.props.comment.id); } else { - this.props.addNotification('error', t('error.NOT_AUTHORIZED')); + this.props.notify('error', t('error.NOT_AUTHORIZED')); } return; } @@ -331,7 +331,7 @@ export default class Comment extends React.Component { deleteAction, disableReply, maxCharCount, - addNotification, + notify, charCountEnable, showSignInDialog, liveUpdates, @@ -475,7 +475,7 @@ export default class Comment extends React.Component { + notify={notify} /> } { !isActive && @@ -487,7 +487,7 @@ export default class Comment extends React.Component { this.state.isEditing ? { if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) { - this.props.addNotification('error', t('error.NOT_AUTHORIZED')); + this.props.notify('error', t('error.NOT_AUTHORIZED')); return; } this.setState({loadingState: 'loading'}); - const {editComment, addNotification, stopEditing} = this.props; + const {editComment, notify, stopEditing} = this.props; if (typeof editComment !== 'function') {return;} let response; try { response = await editComment({body: this.state.body}); this.setState({loadingState: 'success'}); const status = response.data.editComment.comment.status; - notifyForNewCommentStatus(this.props.addNotification, status); + notifyForNewCommentStatus(this.props.notify, status); if (typeof stopEditing === 'function') { stopEditing(); } } catch (error) { this.setState({loadingState: 'error'}); - forEachError(error, ({msg}) => addNotification('error', msg)); + forEachError(error, ({msg}) => notify('error', msg)); } } diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index b37d232f7..a51b999d6 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -59,7 +59,7 @@ class Stream extends React.Component { commentClassNames, root: {asset, asset: {comment, comments, totalCommentCount}}, postComment, - addNotification, + notify, editComment, postFlag, postDontAgree, @@ -147,7 +147,7 @@ class Stream extends React.Component { />} {showCommentBox && this.setState({timesReset: this.state.timesReset + 1}); @@ -40,7 +40,7 @@ export class TopRightMenu extends React.Component { try { await ignoreUser({id}); } catch (error) { - addNotification('error', 'Failed to ignore user'); + notify('error', 'Failed to ignore user'); throw error; } }; diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index a8f6c1b18..71783e64d 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -14,7 +14,7 @@ import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils'; import {withQuery} from 'coral-framework/hocs'; import Embed from '../components/Embed'; import Stream from './Stream'; -import {addNotification} from 'coral-framework/actions/notification'; +import {notify} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; import PropTypes from 'prop-types'; import {setActiveTab} from '../actions/embed'; @@ -34,19 +34,19 @@ class EmbedContainer extends React.Component { const newSubscriptions = [{ document: USER_BANNED_SUBSCRIPTION, updateQuery: () => { - addNotification('info', t('your_account_has_been_banned')); + notify('info', t('your_account_has_been_banned')); }, }, { document: USER_SUSPENDED_SUBSCRIPTION, updateQuery: () => { - addNotification('info', t('your_account_has_been_suspended')); + notify('info', t('your_account_has_been_suspended')); }, }, { document: USERNAME_REJECTED_SUBSCRIPTION, updateQuery: () => { - addNotification('info', t('your_username_has_been_rejected')); + notify('info', t('your_username_has_been_rejected')); }, }]; @@ -193,7 +193,7 @@ const mapDispatchToProps = (dispatch) => checkLogin, setActiveTab, fetchAssetSuccess, - addNotification, + notify, focusSignInDialog, blurSignInDialog, hideSignInDialog, diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index c4a3ae736..7be3d09d5 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -26,7 +26,7 @@ import { } from '../graphql/utils'; const {showSignInDialog, editName} = authActions; -const {addNotification} = notificationActions; +const {notify} = notificationActions; class StreamContainer extends React.Component { subscriptions = []; @@ -303,7 +303,7 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => bindActionCreators({ showSignInDialog, - addNotification, + notify, setActiveReplyBox, editName, viewAllComments, diff --git a/client/coral-framework/actions/notification.js b/client/coral-framework/actions/notification.js index f940ab3b4..872936d15 100644 --- a/client/coral-framework/actions/notification.js +++ b/client/coral-framework/actions/notification.js @@ -1,18 +1,22 @@ import * as actions from '../constants/notification'; export const notify = (kind, msg) => (dispatch, _, {notification}) => { - switch (kind) { - case 'error': - notification.error(msg); - break; - case 'info': - notification.info(msg); - break; - case 'success': - notification.success(msg); - break; - default: - throw new Error(`Unknown notification kind ${kind}`); - } - dispatch({type: actions.NOTIFY, kind, msg}); + const messages = Array.isArray(msg) ? msg : [msg]; + + messages.forEach((message) => { + switch (kind) { + case 'error': + notification.error(message); + break; + case 'info': + notification.info(message); + break; + case 'success': + notification.success(message); + break; + default: + throw new Error(`Unknown notification kind ${kind}`); + } + dispatch({type: actions.NOTIFY, kind, message}); + }); }; diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 2b4b6378f..00e95243e 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -147,6 +147,12 @@ export function forEachError(error, callback) { }); } +export function getErrorMessages(error) { + const result = []; + forEachError(error, ({msg}) => result.push(msg)); + return result; +} + const ascending = (a, b) => { const dateA = new Date(a.created_at); const dateB = new Date(b.created_at); diff --git a/client/talk-plugin-commentbox/CommentBox.js b/client/talk-plugin-commentbox/CommentBox.js index 084e67fc4..d4cc44f22 100644 --- a/client/talk-plugin-commentbox/CommentBox.js +++ b/client/talk-plugin-commentbox/CommentBox.js @@ -12,11 +12,11 @@ export const name = 'talk-plugin-commentbox'; // Given a newly posted comment's status, show a notification to the user // if needed -export const notifyForNewCommentStatus = (addNotification, status) => { +export const notifyForNewCommentStatus = (notify, status) => { if (status === 'REJECTED') { - addNotification('error', t('comment_box.comment_post_banned_word')); + notify('error', t('comment_box.comment_post_banned_word')); } else if (status === 'PREMOD') { - addNotification('success', t('comment_box.comment_post_notif_premod')); + notify('success', t('comment_box.comment_post_notif_premod')); } }; @@ -45,12 +45,12 @@ class CommentBox extends React.Component { postComment, assetId, parentId, - addNotification, + notify, currentUser, } = this.props; if (!can(currentUser, 'INTERACT_WITH_COMMUNITY')) { - addNotification('error', t('error.NOT_AUTHORIZED')); + notify('error', t('error.NOT_AUTHORIZED')); return; } @@ -73,7 +73,7 @@ class CommentBox extends React.Component { // Execute postSubmit Hooks this.state.hooks.postSubmit.forEach((hook) => hook(data)); - notifyForNewCommentStatus(addNotification, postedComment.status); + notifyForNewCommentStatus(notify, postedComment.status); if (commentPostedHandler) { commentPostedHandler(); @@ -81,7 +81,7 @@ class CommentBox extends React.Component { }) .catch((err) => { this.setState({loadingState: 'error'}); - forEachError(err, ({msg}) => addNotification('error', msg)); + forEachError(err, ({msg}) => notify('error', msg)); }); } @@ -186,7 +186,7 @@ CommentBox.propTypes = { currentUser: PropTypes.object.isRequired, isReply: PropTypes.bool.isRequired, canPost: PropTypes.bool, - addNotification: PropTypes.func.isRequired, + notify: PropTypes.func.isRequired, }; const mapStateToProps = ({commentBox}) => ({commentBox}); diff --git a/client/talk-plugin-flags/components/FlagButton.js b/client/talk-plugin-flags/components/FlagButton.js index 1d8438ba0..bf3da6f01 100644 --- a/client/talk-plugin-flags/components/FlagButton.js +++ b/client/talk-plugin-flags/components/FlagButton.js @@ -42,7 +42,7 @@ export default class FlagButton extends Component { this.setState({showMenu: true}); } } else { - this.props.addNotification('error', t('error.NOT_AUTHORIZED')); + this.props.notify('error', t('error.NOT_AUTHORIZED')); } } diff --git a/client/talk-plugin-history/CommentHistory.js b/client/talk-plugin-history/CommentHistory.js index 43ae93f1d..91883cdc8 100644 --- a/client/talk-plugin-history/CommentHistory.js +++ b/client/talk-plugin-history/CommentHistory.js @@ -17,7 +17,7 @@ class CommentHistory extends React.Component { }) .catch((error) => { this.setState({loadingState: 'error'}); - forEachError(error, ({msg}) => {this.props.addNotification('error', msg);}); + forEachError(error, ({msg}) => {this.props.notify('error', msg);}); }); } diff --git a/client/talk-plugin-replies/ReplyBox.js b/client/talk-plugin-replies/ReplyBox.js index 58cb0110d..50bd9a538 100644 --- a/client/talk-plugin-replies/ReplyBox.js +++ b/client/talk-plugin-replies/ReplyBox.js @@ -19,7 +19,7 @@ class ReplyBox extends Component { postComment, assetId, currentUser, - addNotification, + notify, parentId, commentPostedHandler, maxCharCount, @@ -32,7 +32,7 @@ class ReplyBox extends Component { commentPostedHandler={commentPostedHandler} parentId={parentId} onCancel={this.cancelReply} - addNotification={addNotification} + notify={notify} currentUser={currentUser} assetId={assetId} postComment={postComment} @@ -47,7 +47,7 @@ ReplyBox.propTypes = { setActiveReplyBox: PropTypes.func.isRequired, commentPostedHandler: PropTypes.func, parentId: PropTypes.string, - addNotification: PropTypes.func.isRequired, + notify: PropTypes.func.isRequired, postComment: PropTypes.func.isRequired, assetId: PropTypes.string.isRequired }; From a23bad49c693cd621582b49f4b4a25dd14ae1e75 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 03:48:20 +0700 Subject: [PATCH 046/121] Remove global dependency on notification on admin --- .../src/containers/SuspendUserDialog.js | 11 ++++-- client/coral-admin/src/index.js | 5 ++- .../Moderation/containers/Moderation.js | 31 ++++++++++------ .../src/routes/Moderation/graphql.js | 14 +++---- .../coral-admin/src/services/notification.js | 37 ++++++------------- .../client/containers/ModSubscription.js | 4 +- 6 files changed, 50 insertions(+), 52 deletions(-) diff --git a/client/coral-admin/src/containers/SuspendUserDialog.js b/client/coral-admin/src/containers/SuspendUserDialog.js index bd95328a3..f60014832 100644 --- a/client/coral-admin/src/containers/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/SuspendUserDialog.js @@ -5,22 +5,24 @@ import SuspendUserDialog from '../components/SuspendUserDialog'; import {hideSuspendUserDialog} from '../actions/suspendUserDialog'; import {withSetCommentStatus, withSuspendUser} from 'coral-framework/graphql/mutations'; import {compose, gql} from 'react-apollo'; -import * as notification from 'coral-admin/src/services/notification'; 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'; class SuspendUserDialogContainer extends Component { suspendUser = async ({message, until}) => { - const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser} = this.props; + const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser, notify} = this.props; hideSuspendUserDialog(); try { const result = await suspendUser({id: userId, message, until}); if (result.data.suspendUser.errors) { throw result.data.suspendUser.errors; } - notification.success( + notify( + 'success', t('suspenduser.notify_suspend_until', username, timeago(until)), ); if (commentId && commentStatus && commentStatus !== 'REJECTED') { @@ -33,7 +35,7 @@ class SuspendUserDialogContainer extends Component { } } catch(err) { - notification.showMutationErrors(err); + notify('error', getErrorMessages(err)); } }; @@ -69,6 +71,7 @@ const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId, const mapDispatchToProps = (dispatch) => ({ ...bindActionCreators({ hideSuspendUserDialog, + notify, }, dispatch), }); diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index f433af4c2..2b0a982b4 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -8,10 +8,13 @@ import App from './components/App'; import 'react-mdl/extra/material.js'; import graphqlExtension from './graphql'; import pluginsConfig from 'pluginsConfig'; +import {toast} from 'react-toastify'; +import {createNotificationService} from './services/notification'; smoothscroll.polyfill(); -const context = createContext({reducers, graphqlExtension, pluginsConfig}); +const notification = createNotificationService(toast); +const context = createContext({reducers, graphqlExtension, pluginsConfig, notification}); render( diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index 8df83da2a..0b9fc2f38 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -27,6 +27,7 @@ import { clearState } from 'actions/moderation'; import withQueueConfig from '../hoc/withQueueConfig'; +import {notify} from 'coral-framework/actions/notification'; import {Spinner} from 'coral-ui'; import Moderation from '../components/Moderation'; @@ -54,8 +55,15 @@ function getTab(props) { class ModerationContainer extends Component { subscriptions = []; - handleCommentChange = (root, comment, notify) => { - return handleCommentChange(root, comment, this.props.data.variables.sort, notify, this.props.queueConfig, this.activeTab); + handleCommentChange = (root, comment, notifyText) => { + return handleCommentChange( + root, + comment, + this.props.data.variables.sort, + () => notifyText && this.props.notify('info', notifyText), + this.props.queueConfig, + this.activeTab + ); }; get activeTab() { @@ -79,10 +87,10 @@ class ModerationContainer extends Component { variables, updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => { const user = comment.status_history[comment.status_history.length - 1].assigned_by; - const notify = this.props.auth.user.id === user.id + const notifyText = this.props.auth.user.id === user.id ? '' : t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body)); - return this.handleCommentChange(prev, comment, notify); + return this.handleCommentChange(prev, comment, notifyText); }, }); @@ -91,10 +99,10 @@ class ModerationContainer extends Component { variables, updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => { const user = comment.status_history[comment.status_history.length - 1].assigned_by; - const notify = this.props.auth.user.id === user.id + const notifyText = this.props.auth.user.id === user.id ? '' : t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body)); - return this.handleCommentChange(prev, comment, notify); + return this.handleCommentChange(prev, comment, notifyText); }, }); @@ -102,8 +110,8 @@ class ModerationContainer extends Component { document: COMMENT_EDITED_SUBSCRIPTION, variables, updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => { - const notify = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body)); - return this.handleCommentChange(prev, comment, notify); + const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notifyText); }, }); @@ -112,8 +120,8 @@ class ModerationContainer extends Component { variables, updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => { const user = comment.actions[comment.actions.length - 1].user; - const notify = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body)); - return this.handleCommentChange(prev, comment, notify); + const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notifyText); }, }); @@ -394,7 +402,8 @@ const mapDispatchToProps = (dispatch) => ({ viewUserDetail, setSortOrder, storySearchChange, - clearState + clearState, + notify, }, dispatch), }); diff --git a/client/coral-admin/src/routes/Moderation/graphql.js b/client/coral-admin/src/routes/Moderation/graphql.js index 0d4991f72..2039fb0e5 100644 --- a/client/coral-admin/src/routes/Moderation/graphql.js +++ b/client/coral-admin/src/routes/Moderation/graphql.js @@ -1,5 +1,4 @@ import update from 'immutability-helper'; -import * as notification from 'coral-admin/src/services/notification'; const limit = 10; @@ -92,10 +91,7 @@ function getCommentQueues(comment, queueConfig) { * @param {Object} root current state of the store * @param {Object} comment comment that was changed * @param {string} sort current sort order of the queues - * @param {Object} [notify] show know notifications if set - * @param {string} notify.activeQueue current active queue - * @param {string} notify.text notification text to show - * @param {bool} notify.anyQueue if true show the notification when the comment is shown + * @param {string} notify callback to show notification * in the current active queue besides the 'all' queue. * @param {Object} queueConfig queue configuration * @return {Object} next state of the store @@ -110,7 +106,7 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac if (notificationShown) { return; } - notification.info(notify); + notify(); notificationShown = true; }; @@ -119,13 +115,13 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac if (!queueHasComment(next, queue, comment.id)) { next = addCommentToQueue(next, queue, comment, sort); if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sort)) { - showNotificationOnce(comment); + showNotificationOnce(); } } } else if(queueHasComment(next, queue, comment.id)){ next = removeCommentFromQueue(next, queue, comment.id); if (notify && activeQueue === queue) { - showNotificationOnce(comment); + showNotificationOnce(); } } @@ -134,7 +130,7 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac && queueHasComment(next, queue, comment.id) && activeQueue === queue ) { - showNotificationOnce(comment); + showNotificationOnce(); } }); return next; diff --git a/client/coral-admin/src/services/notification.js b/client/coral-admin/src/services/notification.js index 5e8673060..7ad85692b 100644 --- a/client/coral-admin/src/services/notification.js +++ b/client/coral-admin/src/services/notification.js @@ -1,26 +1,13 @@ -import t from 'coral-framework/services/i18n'; -import {toast} from 'react-toastify'; - -export function success(msg) { - return toast(msg, {type: 'success'}); -} - -export function error(msg) { - return toast(msg, {type: 'error'}); -} - -export function info(msg) { - return toast(msg, {type: 'info'}); -} - -export function showMutationErrors(error) { - console.error(error); - if (error.errors) { - error.errors.forEach((err) => { - toast( - err.translation_key ? t(`error.${err.translation_key}`) : err, - {type: 'error'} - ); - }); - } +export function createNotificationService(toast) { + return { + success(msg) { + toast(msg, {type: 'success'}); + }, + error(msg) { + toast(msg, {type: 'error'}); + }, + info(msg) { + toast(msg, {type: 'info'}); + }, + }; } diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js index 82f323530..0e9fc8c89 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js +++ b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js @@ -21,14 +21,14 @@ class ModSubscription extends React.Component { assetId: this.props.data.variables.asset_id, }, updateQuery: (prev, {subscriptionData: {data: {commentFeatured: {user, comment}}}}) => { - const notify = this.props.user.id === user.id + const notifyText = this.props.user.id === user.id ? '' : t( 'talk-plugin-featured-comments.notify_featured', user.username, prepareNotificationText(comment.body), ); - return this.props.handleCommentChange(prev, comment, notify); + return this.props.handleCommentChange(prev, comment, notifyText); }, }, { From a9e3115bd9901e88a45ddb6e1bd2d1d610fc598e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 03:53:37 +0700 Subject: [PATCH 047/121] Expose `getErrorMessages` in plugin-api --- plugin-api/beta/client/utils/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js index e0ff635e0..eecfe0305 100644 --- a/plugin-api/beta/client/utils/index.js +++ b/plugin-api/beta/client/utils/index.js @@ -3,5 +3,6 @@ export { insertCommentsSorted, getSlotFragmentSpreads, forEachError, + getErrorMessages, getDefinitionName, } from 'coral-framework/utils'; From 5eafb672ef094a0a4746a0a7c8c511986266c16d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 16:50:07 +0700 Subject: [PATCH 048/121] Add missing fragment field to Comment --- client/coral-embed-stream/src/containers/Comment.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 005a24ae1..8e592d28e 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -96,6 +96,7 @@ const withCommentFragments = withFragments({ asset: gql` fragment CoralEmbedStream_Comment_asset on Asset { __typename + id ${getSlotFragmentSpreads(slots, 'asset')} } `, From 4dd20260a82a7a9fecddd66ef79721bd09f124c1 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 17:32:29 +0700 Subject: [PATCH 049/121] Unprotect User.created_at --- graph/resolvers/user.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index 5a851233a..e566a6af0 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -22,13 +22,6 @@ const User = { } }, - created_at({roles, created_at}, _, {user}) { - if (user && user.can(SEARCH_OTHER_USERS)) { - return created_at; - } - - return null; - }, comments({id}, {query}, {loaders: {Comments}, user}) { // If the user is not an admin, only return comment list for the owner of From 0811c9302d375762f0691ae682cdf1b4146296e2 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 17:30:46 +0700 Subject: [PATCH 050/121] Ignore access from react dev tools --- client/coral-framework/services/plugins.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js index f51e33702..5474e919f 100644 --- a/client/coral-framework/services/plugins.js +++ b/client/coral-framework/services/plugins.js @@ -31,8 +31,13 @@ function withWarnings(component, queryData) { return new Proxy(queryData[key], { get(target, name) { + // Detect access from React DevTools and ignore those. + const error = new Error(); + const accessFromDevTools = ['backend.js', 'dehydrate'] + .every((keyword) => error.stack && error.stack.includes(keyword)); + // Only care about the components defined in the plugins. - if (component.talkPluginName) { + if (component.talkPluginName && !accessFromDevTools) { const warning = `'${getDisplayName(component)}' of '${component.talkPluginName}' accessed '${key}.${name}' but did not define fragments using the withFragment HOC`; if (memoizedWarnings.indexOf(warning) === -1) { console.warn(warning); From bf89409f60228fef8c59cc5f7f5f3d197a90d535 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 19:53:06 +0700 Subject: [PATCH 051/121] Remove global dependency on Storage --- client/coral-admin/src/actions/auth.js | 28 +++--- client/coral-admin/src/actions/moderation.js | 8 +- client/coral-admin/src/index.js | 10 ++ client/coral-admin/src/reducers/moderation.js | 7 +- .../Dashboard/components/CountdownTimer.js | 25 +++-- client/coral-embed-stream/src/actions/auth.js | 26 ++++-- .../components/TalkProvider.js | 2 + client/coral-framework/helpers/storage.js | 92 ------------------- client/coral-framework/services/bootstrap.js | 12 ++- client/coral-framework/services/storage.js | 39 ++++++++ 10 files changed, 117 insertions(+), 132 deletions(-) delete mode 100644 client/coral-framework/helpers/storage.js create mode 100644 client/coral-framework/services/storage.js diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 3433fc95a..422258f50 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -1,6 +1,5 @@ import bowser from 'bowser'; import * as actions from '../constants/auth'; -import * as Storage from 'coral-framework/helpers/storage'; import t from 'coral-framework/services/i18n'; import jwtDecode from 'jwt-decode'; @@ -8,7 +7,7 @@ import jwtDecode from 'jwt-decode'; // SIGN IN //============================================================================== -export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client}) => { +export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client, storage}) => { dispatch({type: actions.LOGIN_REQUEST}); const params = { @@ -29,8 +28,8 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, .then(({user, token}) => { if (!user) { - if (!bowser.safari && !bowser.ios) { - Storage.removeItem('token'); + if (!bowser.safari && !bowser.ios && storage) { + storage.removeItem('token'); } return dispatch(checkLoginFailure('not logged in')); } @@ -114,13 +113,13 @@ const checkLoginFailure = (error) => ({ error }); -export const checkLogin = () => (dispatch, _, {rest, client}) => { +export const checkLogin = () => (dispatch, _, {rest, client, storage}) => { dispatch(checkLoginRequest()); return rest('/auth') .then(({user}) => { if (!user) { - if (!bowser.safari && !bowser.ios) { - Storage.removeItem('token'); + if (!bowser.safari && !bowser.ios && storage) { + storage.removeItem('token'); } return dispatch(checkLoginFailure('not logged in')); } @@ -139,9 +138,11 @@ export const checkLogin = () => (dispatch, _, {rest, client}) => { // LOGOUT //============================================================================== -export const logout = () => (dispatch, _, {rest, client}) => { +export const logout = () => (dispatch, _, {rest, client, storage}) => { return rest('/auth', {method: 'DELETE'}).then(() => { - Storage.removeItem('token'); + if (storage) { + storage.removeItem('token'); + } // Reset the websocket. client.resetWebsocket(); @@ -154,10 +155,11 @@ export const logout = () => (dispatch, _, {rest, client}) => { // AUTH TOKEN //============================================================================== -export const handleAuthToken = (token) => (dispatch) => { - Storage.setItem('exp', jwtDecode(token).exp); - Storage.setItem('token', token); - +export const handleAuthToken = (token) => (dispatch, _, {storage}) => { + if (storage) { + storage.setItem('exp', jwtDecode(token).exp); + storage.setItem('token', token); + } dispatch({type: 'HANDLE_AUTH_TOKEN'}); }; diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index 5136db4c3..a3ff2ca3e 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -4,15 +4,17 @@ export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open}); export const singleView = () => ({type: actions.SINGLE_VIEW}); // hide shortcuts note -export const hideShortcutsNote = () => { +export const hideShortcutsNote = () => (dispatch, _, {storage}) => { try { - window.localStorage.setItem('coral:shortcutsNote', 'hide'); + if (storage) { + storage.setItem('coral:shortcutsNote', 'hide'); + } } catch (e) { // above will fail in Safari private mode } - return {type: actions.HIDE_SHORTCUTS_NOTE}; + dispatch({type: actions.HIDE_SHORTCUTS_NOTE}); }; export const setSortOrder = (order) => ({ diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index 2b0a982b4..5df2ce391 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -10,12 +10,22 @@ import graphqlExtension from './graphql'; import pluginsConfig from 'pluginsConfig'; import {toast} from 'react-toastify'; import {createNotificationService} from './services/notification'; +import {hideShortcutsNote} from './actions/moderation'; + +function hidrateStore({store, storage}) { + if (storage && storage.getItem('coral:shortcutsNote') === 'hide') { + store.dispatch(hideShortcutsNote()); + } +} smoothscroll.polyfill(); const notification = createNotificationService(toast); const context = createContext({reducers, graphqlExtension, pluginsConfig, notification}); +// hidrate Store with external data. +hidrateStore(context); + render( diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index e0a608488..3a4a3f5e9 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -5,14 +5,17 @@ const initialState = { modalOpen: false, storySearchVisible: false, storySearchString: '', - shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show', + shortcutsNoteVisible: 'show', sortOrder: 'REVERSE_CHRONOLOGICAL', }; export default function moderation (state = initialState, action) { switch (action.type) { case actions.MODERATION_CLEAR_STATE: - return initialState; + return { + ...initialState, + shortcutsNoteVisible: state.shortcutsNoteVisible, + }; case actions.TOGGLE_MODAL: return { ...state, diff --git a/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js b/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js index 9e1995b7f..6864acb6b 100644 --- a/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js +++ b/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js @@ -5,17 +5,24 @@ import {Icon} from 'coral-ui'; import t from 'coral-framework/services/i18n'; const refreshIntervalSeconds = 60 * 5; +// TODO: refactor out storage code into redux. + class CountdownTimer extends React.Component { + static contextTypes = { + storage: PropTypes.object, + }; + static propTypes = { handleTimeout: PropTypes.func.isRequired } - constructor (props) { - super(props); + constructor (props, context) { + super(props, context); + const {storage} = context; try { - if (window.localStorage.getItem('coral:dashboardNote') === null) { - window.localStorage.setItem('coral:dashboardNote', 'show'); + if (storage && storage.getItem('coral:dashboardNote') === null) { + storage.setItem('coral:dashboardNote', 'show'); } } catch (e) { @@ -24,7 +31,7 @@ class CountdownTimer extends React.Component { this.state = { secondsUntilRefresh: refreshIntervalSeconds, - dashboardNote: window.localStorage.getItem('coral:dashboardNote') || 'show' + dashboardNote: (storage && storage.getItem('coral:dashboardNote')) || 'show' }; } @@ -54,8 +61,11 @@ class CountdownTimer extends React.Component { } dismissNote = () => { + const {storage} = this.context; try { - window.localStorage.setItem('coral:dashboardNote', 'hide'); + if (storage) { + storage.setItem('coral:dashboardNote', 'hide'); + } } catch (e) { // when setItem fails in Safari Private mode @@ -64,7 +74,8 @@ class CountdownTimer extends React.Component { } render () { - const hideReloadNote = window.localStorage.getItem('coral:dashboardNote') === 'hide' || + const {storage} = this.context; + const hideReloadNote = (storage && storage.getItem('coral:dashboardNote') === 'hide') || this.state.dashboardNote === 'hide'; // for Safari Incognito return (

    ({ // AUTH TOKEN //============================================================================== -export const handleAuthToken = (token) => (dispatch) => { - Storage.setItem('exp', jwtDecode(token).exp); - Storage.setItem('token', token); +export const handleAuthToken = (token) => (dispatch, _, {storage}) => { + if (storage) { + storage.setItem('exp', jwtDecode(token).exp); + storage.setItem('token', token); + } dispatch({type: 'HANDLE_AUTH_TOKEN'}); }; @@ -272,9 +273,12 @@ export const fetchForgotPassword = (email) => (dispatch, getState, {rest}) => { // LOGOUT //============================================================================== -export const logout = () => async (dispatch, _, {rest, client, pym}) => { +export const logout = () => async (dispatch, _, {rest, client, pym, storage}) => { await rest('/auth', {method: 'DELETE'}); - Storage.removeItem('token'); + + if (storage) { + storage.removeItem('token'); + } // Reset the websocket. client.resetWebsocket(); @@ -296,12 +300,14 @@ const checkLoginSuccess = (user, isAdmin) => ({ isAdmin }); -export const checkLogin = () => (dispatch, _, {rest, client, pym}) => { +export const checkLogin = () => (dispatch, _, {rest, client, pym, storage}) => { dispatch(checkLoginRequest()); rest('/auth') .then((result) => { if (!result.user) { - Storage.removeItem('token'); + if (storage) { + storage.removeItem('token'); + } throw new Error('Not logged in'); } @@ -318,10 +324,10 @@ export const checkLogin = () => (dispatch, _, {rest, client, pym}) => { }) .catch((error) => { console.error(error); - if (error.status && error.status === 401) { + if (error.status && error.status === 401 && storage) { // Unauthorized. - Storage.removeItem('token'); + storage.removeItem('token'); } const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); dispatch(checkLoginFailure(errorMessage)); diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index b68fc7a1a..b553cff45 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -11,6 +11,7 @@ class TalkProvider extends React.Component { rest: this.props.rest, graphqlRegistry: this.props.graphqlRegistry, notification: this.props.notification, + storage: this.props.storage, }; } @@ -31,6 +32,7 @@ TalkProvider.childContextTypes = { rest: PropTypes.func, graphqlRegistry: PropTypes.object, notification: PropTypes.object, + storage: PropTypes.object, }; export default TalkProvider; diff --git a/client/coral-framework/helpers/storage.js b/client/coral-framework/helpers/storage.js deleted file mode 100644 index ec83c3fb3..000000000 --- a/client/coral-framework/helpers/storage.js +++ /dev/null @@ -1,92 +0,0 @@ -let available, error; - -function storageAvailable(type) { - let storage = window[type], x = '__storage_test__'; - try { - storage.setItem(x, x); - storage.removeItem(x); - return true; - } catch (e) { - error = e; - return ( - e instanceof DOMException && - - // everything except Firefox - (e.code === 22 || - - // Firefox - - e.code === 1014 || - - // test name field too, because code might not be present - - // everything except Firefox - e.name === 'QuotaExceededError' || - - // Firefox - e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && - - // acknowledge QuotaExceededError only if there's something already stored - storage.length !== 0 - ); - } -} - -function lazyCheckStorage() { - if (typeof available === 'undefined') { - available = storageAvailable('localStorage'); - } -} - -export function getItem(item = '') { - lazyCheckStorage(); - - if (available) { - return localStorage.getItem(item); - } else { - console.error( - `Cannot get from localStorage. localStorage is not available. ${error}` - ); - } -} - -export function setItem(item = '', value) { - lazyCheckStorage(); - - if (available) { - return localStorage.setItem(item, value); - } else { - console.error( - `Cannot set localStorage. localStorage is not available. ${error}` - ); - } -} - -export function removeItem(item = '') { - lazyCheckStorage(); - - if (available) { - return localStorage.removeItem(item); - } else { - console.error( - `Cannot remove item from localStorage. localStorage is not available. ${error}` - ); - } -} - -export function clear() { - lazyCheckStorage(); - - if (available) { - return localStorage.clear(); - } else { - console.error( - `Cannot clear localStorage. localStorage is not available. ${error}` - ); - } -} - -// window.addEventListener('storage', function(e) { -// const msg = `${e.key} " was changed in page ${e.url} from ${e.oldValue} to ${e.newValue}`; -// console.log(msg); -// }); diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index aa85e9d21..a1b8aec12 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -7,12 +7,12 @@ import {createRestClient} from './rest'; import thunk from 'redux-thunk'; import {loadTranslations} from './i18n'; import bowser from 'bowser'; -import * as Storage from '../helpers/storage'; import {BASE_PATH} from 'coral-framework/constants/url'; import {createPluginsService} from './plugins'; import {createNotificationService} from './notification'; import {createGraphQLRegistry} from './graphqlRegistry'; import globalFragments from 'coral-framework/graphql/fragments'; +import {createStorage} from 'coral-framework/services/storage'; /** * getAuthToken returns the active auth token or null @@ -20,7 +20,7 @@ import globalFragments from 'coral-framework/graphql/fragments'; * browsers that don't allow us to use cross domain iframe local storage. * @return {string|null} */ -const getAuthToken = (store) => { +const getAuthToken = (store, storage) => { let state = store.getState(); if (state.config.auth_token) { @@ -28,10 +28,10 @@ const getAuthToken = (store) => { // if an auth_token exists in config, use it. return state.config.auth_token; - } else if (!bowser.safari && !bowser.ios) { + } else if (!bowser.safari && !bowser.ios && storage) { // Use local storage auth tokens where there's a stable api. - return Storage.getItem('token'); + return storage.getItem('token'); } return null; @@ -40,6 +40,7 @@ const getAuthToken = (store) => { export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification}) { const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); + const storage = createStorage(); let store = null; const token = () => { @@ -48,7 +49,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi // NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. - return getAuthToken(store); + return getAuthToken(store, storage); }; const rest = createRestClient({ uri: `${BASE_PATH}api/v1`, @@ -74,6 +75,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi rest, graphqlRegistry, notification, + storage, }; // Load framework fragments. diff --git a/client/coral-framework/services/storage.js b/client/coral-framework/services/storage.js new file mode 100644 index 000000000..1454d2ac5 --- /dev/null +++ b/client/coral-framework/services/storage.js @@ -0,0 +1,39 @@ + +function getStorage(type) { + let storage = window[type], x = '__storage_test__'; + try { + storage.setItem(x, x); + storage.removeItem(x); + } catch (e) { + const ignore = ( + e instanceof DOMException && + + // everything except Firefox + (e.code === 22 || + + // Firefox + + e.code === 1014 || + + // test name field too, because code might not be present + + // everything except Firefox + e.name === 'QuotaExceededError' || + + // Firefox + e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && + + // acknowledge QuotaExceededError only if there's something already stored + storage.length !== 0 + ); + if (!ignore) { + console.warning(e); // eslint-disable-line + return null; + } + } + return storage; +} + +export function createStorage() { + return getStorage('localStorage'); +} From c2b0a60d8af3c55443386a1e7b27d364347889eb Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 20:02:36 +0700 Subject: [PATCH 052/121] Remove global dependency on router history --- client/coral-admin/src/AppRouter.js | 12 ++++++++++-- client/coral-embed-stream/src/AppRouter.js | 12 ++++++++++-- client/coral-framework/components/TalkProvider.js | 2 ++ client/coral-framework/helpers/router.js | 7 ------- client/coral-framework/services/bootstrap.js | 3 +++ client/coral-framework/services/history.js | 12 ++++++++++++ 6 files changed, 37 insertions(+), 11 deletions(-) delete mode 100644 client/coral-framework/helpers/router.js create mode 100644 client/coral-framework/services/history.js diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 9772ebf17..dcd660c25 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -1,6 +1,6 @@ import React from 'react'; import {Router, Route, IndexRedirect, IndexRoute} from 'react-router'; -import {history} from 'coral-framework/helpers/router'; +import PropTypes from 'prop-types'; import Configure from 'routes/Configure'; import Dashboard from 'routes/Dashboard'; @@ -47,6 +47,14 @@ const routes = (

    ); -const AppRouter = () => ; +class AppRouter extends React.Component { + static contextTypes = { + history: PropTypes.object, + }; + + render() { + return ; + } +} export default AppRouter; diff --git a/client/coral-embed-stream/src/AppRouter.js b/client/coral-embed-stream/src/AppRouter.js index c2e7b2c5d..38093abd8 100644 --- a/client/coral-embed-stream/src/AppRouter.js +++ b/client/coral-embed-stream/src/AppRouter.js @@ -1,6 +1,6 @@ import React from 'react'; import {Router, Route} from 'react-router'; -import {history} from 'coral-framework/helpers/router'; +import PropTypes from 'prop-types'; import Embed from './containers/Embed'; import {LoginContainer} from 'coral-sign-in/containers/LoginContainer'; @@ -12,6 +12,14 @@ const routes = (
  • ); -const AppRouter = () => ; +class AppRouter extends React.Component { + static contextTypes = { + history: PropTypes.object, + }; + + render() { + return ; + } +} export default AppRouter; diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index b553cff45..418bb3760 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -12,6 +12,7 @@ class TalkProvider extends React.Component { graphqlRegistry: this.props.graphqlRegistry, notification: this.props.notification, storage: this.props.storage, + history: this.props.history, }; } @@ -33,6 +34,7 @@ TalkProvider.childContextTypes = { graphqlRegistry: PropTypes.object, notification: PropTypes.object, storage: PropTypes.object, + history: PropTypes.object, }; export default TalkProvider; diff --git a/client/coral-framework/helpers/router.js b/client/coral-framework/helpers/router.js deleted file mode 100644 index e6277e9d8..000000000 --- a/client/coral-framework/helpers/router.js +++ /dev/null @@ -1,7 +0,0 @@ -import {useBasename} from 'history'; -import {browserHistory} from 'react-router'; -import {BASE_PATH} from 'coral-framework/constants/url'; - -export const history = useBasename(() => browserHistory)({ - basename: BASE_PATH -}); diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index a1b8aec12..70fb9fa50 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -13,6 +13,7 @@ import {createNotificationService} from './notification'; import {createGraphQLRegistry} from './graphqlRegistry'; import globalFragments from 'coral-framework/graphql/fragments'; import {createStorage} from 'coral-framework/services/storage'; +import {createHistory} from 'coral-framework/services/history'; /** * getAuthToken returns the active auth token or null @@ -41,6 +42,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); const storage = createStorage(); + const history = createHistory(BASE_PATH); let store = null; const token = () => { @@ -76,6 +78,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi graphqlRegistry, notification, storage, + history, }; // Load framework fragments. diff --git a/client/coral-framework/services/history.js b/client/coral-framework/services/history.js new file mode 100644 index 000000000..f64bb8fb1 --- /dev/null +++ b/client/coral-framework/services/history.js @@ -0,0 +1,12 @@ +import {browserHistory} from 'react-router'; +import {useBasename} from 'history'; + +export function createHistory(basename) { + if (!basename) { + return browserHistory; + } + + return useBasename(() => browserHistory)({ + basename + }); +} From 0342b6c826af21510f5c7c373a51131392e17d4d Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 23 Aug 2017 10:06:39 -0300 Subject: [PATCH 053/121] rolling back --- services/wordlist.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/services/wordlist.js b/services/wordlist.js index 477baace6..2ae8ed90c 100644 --- a/services/wordlist.js +++ b/services/wordlist.js @@ -9,10 +9,6 @@ const Errors = require('../errors'); // REGEX to prevent emoji's from entering the wordlist. const EMOJI_REGEX = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?)*/; -const escapeRegExp = function(s) { - return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); -}; - /** * The root wordlist object. * @type {Object} @@ -78,7 +74,7 @@ class Wordlist { return [word]; } - return tokenizer.tokenize(escapeRegExp(word.toLowerCase())); + return tokenizer.tokenize(word.toLowerCase()); }) .filter((tokens) => { if (tokens.length === 0) { From ceba418e285bb46eb15bca79b67af1e404d50b65 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 20:14:55 +0700 Subject: [PATCH 054/121] created_at is a Date --- graph/typeDefs.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index d83d9f75f..0e430cd84 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -76,7 +76,7 @@ type User { username: String! # creation date of user - created_at: String! + created_at: Date! # Action summaries against the user. action_summaries: [ActionSummary!]! From ce360a47f8099ab6284a099da7974bceee225205 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 22:40:48 +0700 Subject: [PATCH 055/121] Add slot to fragments in featured comments --- .../talk-plugin-featured-comments/client/containers/Comment.js | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/talk-plugin-featured-comments/client/containers/Comment.js b/plugins/talk-plugin-featured-comments/client/containers/Comment.js index bba764a1d..a647bf1b4 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/Comment.js +++ b/plugins/talk-plugin-featured-comments/client/containers/Comment.js @@ -5,6 +5,7 @@ import {getSlotFragmentSpreads} from 'plugin-api/beta/client/utils'; const slots = [ 'commentReactions', + 'commentAuthorName', ]; export default withFragments({ From a68513227abd9ec1b5c20dd72ba32d19ce9f1b18 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 23 Aug 2017 09:54:00 -0600 Subject: [PATCH 056/121] adjusted tokenizer to use regex based tokenizer instead --- services/wordlist.js | 8 ++++---- test/server/services/wordlist.js | 29 ++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/services/wordlist.js b/services/wordlist.js index 2ae8ed90c..8744de4ba 100644 --- a/services/wordlist.js +++ b/services/wordlist.js @@ -1,8 +1,8 @@ const debug = require('debug')('talk:services:wordlist'); const _ = require('lodash'); -const natural = require('natural'); -const tokenizer = new natural.WordTokenizer(); -const nameTokenizer = new natural.RegexpTokenizer({pattern: /\_/}); +const {RegexpTokenizer} = require('natural'); +const tokenizer = new RegexpTokenizer({pattern: /[\.\s\'\"\?\!]/}); +const nameTokenizer = new RegexpTokenizer({pattern: /\_/}); const SettingsService = require('./settings'); const Errors = require('../errors'); @@ -73,7 +73,7 @@ class Wordlist { if (word.length === 1) { return [word]; } - + return tokenizer.tokenize(word.toLowerCase()); }) .filter((tokens) => { diff --git a/test/server/services/wordlist.js b/test/server/services/wordlist.js index 417844da4..6d0116e26 100644 --- a/test/server/services/wordlist.js +++ b/test/server/services/wordlist.js @@ -10,10 +10,12 @@ describe('services.Wordlist', () => { 'cookies', 'how to do bad things', 'how to do really bad things', - 's h i t' + 's h i t', + '$hit', + 'p**ch', ], suspect: [ - 'do bad things' + 'do bad things', ] }; @@ -26,9 +28,18 @@ describe('services.Wordlist', () => { before(() => wordlist.upsert(wordlists)); - it('has entries', () => { - expect(wordlist.lists.banned).to.not.be.empty; - expect(wordlist.lists.suspect).to.not.be.empty; + it('parses the wordlists correctly', () => { + expect(wordlist.lists.banned).to.deep.equal([ + [ 'cookies' ], + [ 'how', 'to', 'do', 'bad', 'things' ], + [ 'how', 'to', 'do', 'really', 'bad', 'things' ], + [ 's', 'h', 'i', 't' ], + [ '$hit' ], + [ 'p**ch' ], + ]); + expect(wordlist.lists.suspect).to.deep.equal([ + [ 'do', 'bad', 'things' ], + ]); }); }); @@ -57,7 +68,9 @@ describe('services.Wordlist', () => { 'cookies', 'COOKIES.', 'how to do bad things', - 'How To do bad things!' + 'How To do bad things!', + 'This stuff is $hit!', + 'That\'s a p**ch!', ].forEach((word) => { expect(wordlist.match(bannedList, word)).to.be.true; }); @@ -68,7 +81,9 @@ describe('services.Wordlist', () => { 'how to', 'cookie', 'how to be a great person?', - 'how to not do really bad things?' + 'how to not do really bad things?', + 'i have $100 dollars.', + 'I have bad $ hit lling', ].forEach((word) => { expect(wordlist.match(bannedList, word)).to.be.false; }); From 6d8185c33e7863ac65096cf884e0f0e0fa617f66 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 23 Aug 2017 11:52:02 -0600 Subject: [PATCH 057/121] modified middleware to change AND -> OR, added mod to com pages --- middleware/authorization.js | 27 +++++++--- routes/api/users/index.js | 12 ++--- test/server/middleware/authorization.js | 47 ++++++++++++++++ test/server/routes/api/account/index.js | 72 ++++++++++--------------- 4 files changed, 101 insertions(+), 57 deletions(-) create mode 100644 test/server/middleware/authorization.js diff --git a/middleware/authorization.js b/middleware/authorization.js index d93913109..742b6983a 100644 --- a/middleware/authorization.js +++ b/middleware/authorization.js @@ -10,18 +10,29 @@ const debug = require('debug')('talk:middleware:authorization'); const ErrNotAuthorized = require('../errors').ErrNotAuthorized; /** - * has returns true if the user has all the roles specified, otherwise it will - * return false. + * has returns true if the user has at least one of the roles specified, + * otherwise it will return false. * @param {Object} user the user to check for roles - * @param {Array} roles all the roles that a user must have - * @return {Boolean} true if the user has all the roles required, false + * @param {Array} roles roles to check if the user has + * @return {Boolean} true if the user has some the roles required, false * otherwise */ -authorization.has = (user, ...roles) => roles.every((role) => { +authorization.has = (user, ...roles) => { - // TODO: remove toUpperCase once we've migrated over the roles. - return user.roles.indexOf(role.toUpperCase()) >= 0; -}); + // If no user is specified, then they can't have the roles you want! + if (!user || !user.roles) { + return false; + } + + // If no roles are specified, then any user has the roles you want! + if (!roles || roles.length === 0) { + return true; + } + + // If there's a user, and roles, then check to see that the user has at least + // one of those roles. + return roles.some((role) => user.roles.includes(role)); +}; /** * needed is a connect middleware layer that ensures that all requests coming diff --git a/routes/api/users/index.js b/routes/api/users/index.js index 60c568253..41b2771ca 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -10,7 +10,7 @@ const { } = require('../../../config'); // get a list of users. -router.get('/', authorization.needed('ADMIN'), async (req, res, next) => { +router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => { const { value = '', field = 'created_at', @@ -44,7 +44,7 @@ router.get('/', authorization.needed('ADMIN'), async (req, res, next) => { }); -router.post('/:user_id/role', authorization.needed('ADMIN'), async (req, res, next) => { +router.post('/:user_id/role', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => { try { await UsersService.addRoleToUser(req.params.user_id, req.body.role); res.status(204).end(); @@ -54,7 +54,7 @@ router.post('/:user_id/role', authorization.needed('ADMIN'), async (req, res, ne }); // update the status of a user -router.post('/:user_id/status', authorization.needed('ADMIN'), async (req, res, next) => { +router.post('/:user_id/status', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => { let {status} = req.body; try { @@ -74,7 +74,7 @@ router.post('/:user_id/status', authorization.needed('ADMIN'), async (req, res, } }); -router.post('/:user_id/username-enable', authorization.needed('ADMIN'), async (req, res, next) => { +router.post('/:user_id/username-enable', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => { try { await UsersService.toggleNameEdit(req.params.user_id, true); res.status(204).end(); @@ -83,7 +83,7 @@ router.post('/:user_id/username-enable', authorization.needed('ADMIN'), async (r } }); -router.post('/:user_id/email', authorization.needed('ADMIN'), async (req, res, next) => { +router.post('/:user_id/email', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => { try { let user = await UsersService.findById(req.params.user_id); @@ -189,7 +189,7 @@ router.post('/resend-verify', async (req, res, next) => { }); // trigger an email confirmation re-send from the admin panel -router.post('/:user_id/email/confirm', authorization.needed('ADMIN'), async (req, res, next) => { +router.post('/:user_id/email/confirm', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => { const { user_id } = req.params; diff --git a/test/server/middleware/authorization.js b/test/server/middleware/authorization.js new file mode 100644 index 000000000..7966cffa9 --- /dev/null +++ b/test/server/middleware/authorization.js @@ -0,0 +1,47 @@ +const chai = require('chai'); +const expect = chai.expect; + +const authz = require('../../../middleware/authorization'); + +describe('middleware.authorization', () => { + describe('#has', () => { + it('allows if no roles are specified', () => { + expect(authz.has({roles: []})).to.be.true; + }); + it('allows if the correct roles are met', () => { + expect(authz.has({roles: ['ADMIN']}, 'ADMIN', 'MODERATOR')).to.be.true; + }); + it('disallows if the role required is missing', () => { + expect(authz.has({roles: []}, 'ADMIN', 'MODERATOR')).to.be.false; + }); + }); + + describe('#needed', () => { + let needed = (...roles) => { + let middleware = authz.needed(...roles); + + return middleware[middleware.length - 1]; + }; + + it('allows if no roles are specified', () => { + needed()({user: {roles: []}}, {}, (err) => { + expect(err).to.be.undefined; + }); + }); + it('allows if the correct roles are met', () => { + needed()({user: {roles: ['ADMIN']}}, {}, (err) => { + expect(err).to.be.undefined; + }); + }); + it('disallows if the role required is missing', () => { + needed('ADMIN', 'MODERATOR')({user: {roles: []}}, {}, (err) => { + expect(err).to.not.be.undefined; + }); + }); + it('disallows if there is no user on the request', () => { + needed('ADMIN', 'MODERATOR')({}, {}, (err) => { + expect(err).to.not.be.undefined; + }); + }); + }); +}); diff --git a/test/server/routes/api/account/index.js b/test/server/routes/api/account/index.js index 8d4e0057a..3195db0e5 100644 --- a/test/server/routes/api/account/index.js +++ b/test/server/routes/api/account/index.js @@ -1,71 +1,57 @@ const passport = require('../../../passport'); const app = require('../../../../../app'); + const chai = require('chai'); +chai.use(require('chai-as-promised')); +chai.use(require('chai-http')); const expect = chai.expect; +const UsersService = require('../../../../../services/users'); const SettingsService = require('../../../../../services/settings'); const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}}; -// Setup chai. -chai.should(); -chai.use(require('chai-http')); - -const UsersService = require('../../../../../services/users'); - describe('/api/v1/account/username', () => { let mockUser; - - beforeEach(() => SettingsService.init(settings).then(() => { - return UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana'); - }) - .then((user) => { - mockUser = user; - })); + beforeEach(async () => { + await SettingsService.init(settings); + mockUser = await UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana'); + }); describe('#put', () => { - it('it should enable a user to edit their username if canEditName is enabled', () => { - return chai.request(app) + it('it should enable a user to edit their username if canEditName is enabled', async () => { + await chai.request(app) .post(`/api/v1/users/${mockUser.id}/username-enable`) - .set(passport.inject({id: '456', roles: ['ADMIN']})) - .then(() => chai.request(app) + .set(passport.inject({id: '456', roles: ['ADMIN']})); + + const res = await chai.request(app) .put('/api/v1/account/username') .set(passport.inject({id: mockUser.id, roles: []})) - .send({username: 'MojoJojo'})) - .then((res) => { - expect(res).to.have.status(204); - }); + .send({username: 'MojoJojo'}); + + expect(res).to.have.status(204); }); - it('it should return an error if the wrong user tries to edit a username', (done) => { - chai.request(app) + it('it should return an error if the wrong user tries to edit a username', async () => { + await chai.request(app) .post(`/api/v1/users/${mockUser.id}/username-enable`) - .set(passport.inject({id: '456', roles: ['ADMIN']})) - .then(() => chai.request(app) + .set(passport.inject({id: '456', roles: ['ADMIN']})); + + let res = chai.request(app) .put('/api/v1/account/username') .set(passport.inject({id: 'wrongid', roles: []})) - .send({username: 'MojoJojo'})) - .then(() => { - done(new Error('Expected Error')); - }) - .catch((err) => { - expect(err).to.be.ok; - done(); - }); + .send({username: 'MojoJojo'}); + + return expect(res).to.eventually.be.rejected; }); - it('it should return an error when the user tries to edit their username if canEditName is disabled', (done) => { - chai.request(app) + it('it should return an error when the user tries to edit their username if canEditName is disabled', () => { + let res = chai.request(app) .put('/api/v1/account/username') .set(passport.inject({id: mockUser.id, roles: []})) - .send({username: 'MojoJojo'}) - .then(() => { - done(new Error('Expected Error')); - }) - .catch((err) => { - expect(err).to.be.ok; - done(); - }); + .send({username: 'MojoJojo'}); + + return expect(res).to.eventually.be.rejected; }); }); }); From 38469a7e6939135b8a1782418ba35367d818c269 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 23 Aug 2017 13:30:23 -0600 Subject: [PATCH 058/121] Added message to Extendable Error's --- errors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/errors.js b/errors.js index 79731dad6..ab1667e07 100644 --- a/errors.js +++ b/errors.js @@ -5,7 +5,7 @@ class ExtendableError { constructor(message = null) { this.message = message; - this.stack = (new Error()).stack; + this.stack = (new Error(message)).stack; } } From acf792089deb44b870a8ab12faa56432a9743c80 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 23 Aug 2017 15:42:30 -0600 Subject: [PATCH 059/121] added more db optims --- graph/resolvers/asset.js | 41 ++++++++++------------- graph/resolvers/comment_status_history.js | 6 ++-- graph/resolvers/flag_action.js | 6 ++-- graph/resolvers/root_query.js | 4 +-- services/comments.js | 6 ++-- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js index 51234fd5a..ebae4e5a7 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -1,32 +1,27 @@ const {decorateWithTags} = require('./util'); -const { - SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS, -} = require('../../perms/constants'); const Asset = { - async comment({id}, {id: commentId}, {loaders: {Comments}, user}) { - const statuses = user && user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) - ? ['NONE', 'ACCEPTED', 'PREMOD', 'REJECTED'] - : ['NONE', 'ACCEPTED']; + async comment({id}, {id: commentId}, {loaders: {Comments}}) { - const comments = await Comments.getByQuery({ - asset_id: id, - ids: commentId, - statuses, - }); + // Load the comment from the database. + const comment = Comments.get.load(id); + if (!comment) { + return null; + } - return comments.nodes[0]; + // If the comment asset mismatches, then don't return it! + if (comment.asset_id !== id) { + return null; + } + + return comment; }, - comments({id}, {query: {sort, sortBy, limit, excludeIgnored, tags}, deep}, {loaders: {Comments}}) { - return Comments.getByQuery({ - asset_id: id, - sort, - sortBy, - limit, - parent_id: deep ? undefined : null, - tags, - excludeIgnored, - }); + comments({id}, {query, deep}, {loaders: {Comments}}) { + if (!deep) { + query.parent_id = null; + } + + return Comments.getByQuery(query); }, commentCount({id, commentCount}, {tags}, {loaders: {Comments}}) { if (commentCount != null) { diff --git a/graph/resolvers/comment_status_history.js b/graph/resolvers/comment_status_history.js index 2e1676d90..fb431b628 100644 --- a/graph/resolvers/comment_status_history.js +++ b/graph/resolvers/comment_status_history.js @@ -2,9 +2,11 @@ const {SEARCH_OTHER_USERS} = require('../../perms/constants'); const CommentStatusHistory = { assigned_by({assigned_by}, _, {user, loaders: {Users}}) { - if (user && user.can(SEARCH_OTHER_USERS) && assigned_by != null) { - return Users.getByID.load(assigned_by); + if (!user || !user.can(SEARCH_OTHER_USERS) || assigned_by != null) { + return null; } + + return Users.getByID.load(assigned_by); } }; diff --git a/graph/resolvers/flag_action.js b/graph/resolvers/flag_action.js index e4b30c408..dee583747 100644 --- a/graph/resolvers/flag_action.js +++ b/graph/resolvers/flag_action.js @@ -8,9 +8,11 @@ const FlagAction = { return group_id; }, user({user_id}, _, {loaders: {Users}}) { - if (user_id) { - return Users.getByID.load(user_id); + if (!user_id) { + return null; } + + return Users.getByID.load(user_id); }, }; diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index c88184dc9..07dab8eed 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -39,8 +39,8 @@ const RootQuery = { return null; } - const {asset_url} = query; - if (asset_url && asset_url.length > 0) { + const {asset_url, asset_id} = query; + if ((!asset_id || asset_id.length === 0) && asset_url && asset_url.length > 0) { let asset = await Assets.findByUrl(asset_url); if (asset) { query.asset_id = asset.id; diff --git a/services/comments.js b/services/comments.js index ac7d6ae45..db0714233 100644 --- a/services/comments.js +++ b/services/comments.js @@ -95,11 +95,11 @@ module.exports = class CommentsService { }, }); - if (originalComment === null) { + if (originalComment == null) { // Try to get the comment. const comment = await CommentsService.findById(id); - if (comment === null) { + if (comment == null) { debug('rejecting comment edit because comment was not found'); throw errors.ErrNotFound; } @@ -253,7 +253,7 @@ module.exports = class CommentsService { $set: {status} }); - if (originalComment === null) { + if (originalComment == null) { throw errors.ErrNotFound; } From 645aaa09ae6c7bc050eceaa62fd76f0858abdd3c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 24 Aug 2017 18:40:38 +0700 Subject: [PATCH 060/121] Add some docs --- client/coral-admin/src/services/notification.js | 5 +++++ client/coral-framework/services/bootstrap.js | 12 +++++++++++- client/coral-framework/services/client.js | 11 +++++++++-- client/coral-framework/services/events.js | 6 ++++++ client/coral-framework/services/graphqlRegistry.js | 5 +++++ client/coral-framework/services/history.js | 5 +++++ client/coral-framework/services/notification.js | 5 +++++ client/coral-framework/services/plugins.js | 9 +++++++-- client/coral-framework/services/rest.js | 7 +++++++ client/coral-framework/services/storage.js | 4 ++++ client/coral-framework/services/store.js | 6 ++++++ 11 files changed, 70 insertions(+), 5 deletions(-) diff --git a/client/coral-admin/src/services/notification.js b/client/coral-admin/src/services/notification.js index 7ad85692b..aca5bdc80 100644 --- a/client/coral-admin/src/services/notification.js +++ b/client/coral-admin/src/services/notification.js @@ -1,3 +1,8 @@ +/** + * createNotificationService returns a notification services based on toast. + * @param {Object} toast + * @return {Object} notification service + */ export function createNotificationService(toast) { return { success(msg) { diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index 70fb9fa50..ac94d8b29 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -38,7 +38,17 @@ const getAuthToken = (store, storage) => { return null; }; -export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification}) { +/** + * createContext setups and returns Talk dependencies that should be + * passed to `TalkProvider`. + * @param {Object} [config] configuration + * @param {Object} [config.reducers] extra reducers to add to redux + * @param {Array} [config.pluginsConfig] plugin configuration as returned by importing pluginsConfig + * @param {Object} [config.graphqlExtensions] additional extension to the graphql framework + * @param {Object} [config.notification] replace default notification service + * @return {Object} context + */ +export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification} = {}) { const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); const storage = createStorage(); diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index ce83d501f..5907488e3 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -15,8 +15,16 @@ function resolveToken(token) { return typeof token === 'function' ? token() : token; } +/** + * createClient setups and returns an Apollo GraphQL Client + * @param {Object} [options] configuration + * @param {string|function} [options.token] auth token + * @param {string} [options.uri] uri of the graphql server + * @param {string} [options.liveUri] uri of the graphql subscription server + * @return {Object} apollo client + */ export function createClient(options = {}) { - const {token, uri, liveUri, ...apolloOptions} = options; + const {token, uri, liveUri} = options; const wsClient = new SubscriptionClient(liveUri, { reconnect: true, lazy: true, @@ -53,7 +61,6 @@ export function createClient(options = {}) { ); const client = new ApolloClient({ - ...apolloOptions, connectToDevTools: true, addTypename: true, fragmentMatcher: new IntrospectionFragmentMatcher({introspectionQueryResultData}), diff --git a/client/coral-framework/services/events.js b/client/coral-framework/services/events.js index f6a62763d..e1b13afd9 100644 --- a/client/coral-framework/services/events.js +++ b/client/coral-framework/services/events.js @@ -1,3 +1,9 @@ +/** + * createReduxEmitter returns a redux middleware proxying redux actions to + * the event emitter + * @param {Object} eventEmitter + * @return {function} redux middleware + */ export function createReduxEmitter(eventEmitter) { return () => (next) => (action) => { diff --git a/client/coral-framework/services/graphqlRegistry.js b/client/coral-framework/services/graphqlRegistry.js index 5ca549183..938bf7f78 100644 --- a/client/coral-framework/services/graphqlRegistry.js +++ b/client/coral-framework/services/graphqlRegistry.js @@ -268,6 +268,11 @@ class GraphQLRegistry { } } +/** + * createGraphQLRegistry + * @param {Function} getSlotFragments A callback with signature `(slot, part) => [documents]` to retrieve slot fragments. + * @return {Object} graphql registry + */ export function createGraphQLRegistry(getSlotFragments) { return new GraphQLRegistry(getSlotFragments); } diff --git a/client/coral-framework/services/history.js b/client/coral-framework/services/history.js index f64bb8fb1..c80d4b920 100644 --- a/client/coral-framework/services/history.js +++ b/client/coral-framework/services/history.js @@ -1,6 +1,11 @@ import {browserHistory} from 'react-router'; import {useBasename} from 'history'; +/** + * createHistory returns the history service for react router + * @param {string} basename base path of the url + * @return {Object} histor service + */ export function createHistory(basename) { if (!basename) { return browserHistory; diff --git a/client/coral-framework/services/notification.js b/client/coral-framework/services/notification.js index fa0e61b3f..5730200ad 100644 --- a/client/coral-framework/services/notification.js +++ b/client/coral-framework/services/notification.js @@ -1,3 +1,8 @@ +/** + * createNotificationService returns a notification services based on pym. + * @param {Object} pym + * @return {Object} notification service + */ export function createNotificationService(pym) { return { success(msg) { diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js index 5474e919f..1aae0c662 100644 --- a/client/coral-framework/services/plugins.js +++ b/client/coral-framework/services/plugins.js @@ -174,6 +174,11 @@ class PluginsService { } } -export function createPluginsService(plugins) { - return new PluginsService(plugins); +/** + * createPluginsService returns a plugins service. + * @param {Array} plugins config as returned from importing `pluginsConfig` + * @return {Object} plugins service + */ +export function createPluginsService(pluginsConfig) { + return new PluginsService(pluginsConfig); } diff --git a/client/coral-framework/services/rest.js b/client/coral-framework/services/rest.js index f879659ca..e5f9e5e09 100644 --- a/client/coral-framework/services/rest.js +++ b/client/coral-framework/services/rest.js @@ -43,6 +43,13 @@ const handleResp = (res) => { } }; +/** + * createRestClient setups and returns a Rest Client + * @param {Object} options configuration + * @param {string} options.uri uri of the rest server + * @param {string|function} [options.token] auth token + * @return {Object} rest client + */ export function createRestClient(options) { const {token, uri} = options; const client = (path, options) => { diff --git a/client/coral-framework/services/storage.js b/client/coral-framework/services/storage.js index 1454d2ac5..9f06595fa 100644 --- a/client/coral-framework/services/storage.js +++ b/client/coral-framework/services/storage.js @@ -34,6 +34,10 @@ function getStorage(type) { return storage; } +/** + * createStorage returns a localStorage wrapper if available + * @return {Object} localStorage wrapper + */ export function createStorage() { return getStorage('localStorage'); } diff --git a/client/coral-framework/services/store.js b/client/coral-framework/services/store.js index e5fc52503..3ccfe9e2f 100644 --- a/client/coral-framework/services/store.js +++ b/client/coral-framework/services/store.js @@ -1,5 +1,11 @@ import {createStore as reduxCreateStore, combineReducers, applyMiddleware, compose} from 'redux'; +/** + * createStore creates a Redux Store + * @param {Object} reducers addtional reducers + * @param {Array} [middlewares] additional middlewares + * @return {Object} redux store + */ export function createStore(reducers, middlewares = []) { const enhancers = [ applyMiddleware( From ca31dedca84aae3d215c26c6ce5ffcec90142d97 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 24 Aug 2017 20:07:45 +0700 Subject: [PATCH 061/121] Make comments query optional --- graph/typeDefs.graphql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 72a1ab6fd..bf51f47a9 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -334,7 +334,7 @@ type Comment { user: User # the replies that were made to the comment. - replies(query: RepliesQuery!): CommentConnection! + replies(query: RepliesQuery = {}): CommentConnection! # The count of replies on a comment. replyCount: Int @@ -591,7 +591,7 @@ type Asset { # The comments that are attached to the asset. When `deep` is true, the # comments returned will be at all depths. - comments(query: CommentsQuery!, deep: Boolean = false): CommentConnection! + comments(query: CommentsQuery = {}, deep: Boolean = false): CommentConnection! # A Comment from the Asset by comment's ID comment(id: ID!): Comment From d1a3a67b35c405ab4a0ab6c6948da7c308e78127 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 24 Aug 2017 20:12:08 +0700 Subject: [PATCH 062/121] Fix featured comments --- .../client/containers/TabPane.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js index 31f2769a0..b25e93040 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js @@ -16,9 +16,9 @@ class TabPaneContainer extends React.Component { query: LOAD_MORE_QUERY, variables: { limit: 5, - cursor: this.props.root.asset.featuredComments.endCursor, - asset_id: this.props.root.asset.id, - sort: 'REVERSE_CHRONOLOGICAL', + cursor: this.props.asset.featuredComments.endCursor, + asset_id: this.props.asset.id, + sort: 'DESC', excludeIgnored: this.props.data.variables.excludeIgnored, }, updateQuery: (previous, {fetchMoreResult:{comments}}) => { @@ -47,7 +47,7 @@ class TabPaneContainer extends React.Component { } const LOAD_MORE_QUERY = gql` - query TalkFeaturedComments_LoadMoreComments($limit: Int = 5, $cursor: String, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { + query TalkFeaturedComments_LoadMoreComments($limit: Int = 5, $cursor: Cursor, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { comments(query: {limit: $limit, cursor: $cursor, tags: ["FEATURED"], asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) { nodes { ...${getDefinitionName(Comment.fragments.comment)} From ddb555e07f9b38d767a2a6896f93d1685b1d9464 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 24 Aug 2017 21:30:16 +0700 Subject: [PATCH 063/121] Add new sorting variables --- .../src/containers/Embed.js | 18 +++++++-- .../src/containers/Stream.js | 27 ++++++++++++-- .../coral-embed-stream/src/graphql/index.js | 2 +- .../coral-embed-stream/src/reducers/stream.js | 2 + .../client/containers/TabPane.js | 37 ++++++++++++++++--- test/server/graph/mutations/editComment.js | 2 +- 6 files changed, 73 insertions(+), 15 deletions(-) diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 731cab264..5a0ea534d 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -151,7 +151,15 @@ const slots = [ ]; const EMBED_QUERY = gql` - query CoralEmbedStream_Embed($assetId: ID, $assetUrl: String, $commentId: ID!, $hasComment: Boolean!, $excludeIgnored: Boolean) { + query CoralEmbedStream_Embed( + $assetId: ID, + $assetUrl: String, + $commentId: ID!, + $hasComment: Boolean!, + $excludeIgnored: Boolean, + $sortBy: SORT_COMMENTS_BY!, + $sort: SORT_ORDER!, + ) { me { id status @@ -163,13 +171,15 @@ const EMBED_QUERY = gql` `; export const withEmbedQuery = withQuery(EMBED_QUERY, { - options: ({auth, commentId, assetId, assetUrl}) => ({ + options: ({auth, commentId, assetId, assetUrl, sortBy, sort}) => ({ variables: { assetId, assetUrl, commentId, hasComment: commentId !== '', excludeIgnored: Boolean(auth && auth.user && auth.user.id), + sortBy, + sort, }, }), }); @@ -180,7 +190,9 @@ const mapStateToProps = (state) => ({ assetId: state.stream.assetId, assetUrl: state.stream.assetUrl, activeTab: state.embed.activeTab, - config: state.config + config: state.config, + sort: state.stream.sort, + sortBy: state.stream.sortBy, }); const mapDispatchToProps = (dispatch) => diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 8b02e793f..4720a6889 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -119,7 +119,8 @@ class StreamContainer extends React.Component { cursor: this.props.root.asset.comments.endCursor, parent_id: null, asset_id: this.props.root.asset.id, - sort: 'DESC', + sort: this.props.data.variables.sort, + sortBy: this.props.data.variables.sortBy, excludeIgnored: this.props.data.variables.excludeIgnored, }, updateQuery: (prev, {fetchMoreResult:{comments}}) => { @@ -203,8 +204,26 @@ const COMMENTS_EDITED_SUBSCRIPTION = gql` `; const LOAD_MORE_QUERY = gql` - query CoralEmbedStream_LoadMoreComments($limit: Int = 5, $cursor: Cursor, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { - comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) { + query CoralEmbedStream_LoadMoreComments( + $limit: Int = 5 + $cursor: Cursor + $parent_id: ID + $asset_id: ID + $sort: SORT_ORDER + $sortBy: SORT_COMMENTS_BY = CREATED_AT + $excludeIgnored: Boolean + ) { + comments( + query: { + limit: $limit + cursor: $cursor + parent_id: $parent_id + asset_id: $asset_id + sort: $sort + sortBy: $sortBy + excludeIgnored: $excludeIgnored + } + ) { nodes { ...CoralEmbedStream_Stream_comment } @@ -255,7 +274,7 @@ const fragments = { } commentCount @skip(if: $hasComment) totalCommentCount @skip(if: $hasComment) - comments(query: {limit: 10, excludeIgnored: $excludeIgnored}) @skip(if: $hasComment) { + comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sort: $sort, sortBy: $sortBy}) @skip(if: $hasComment) { nodes { ...CoralEmbedStream_Stream_comment } diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 9a3718b0b..b4c5ce163 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -56,7 +56,7 @@ const extension = { fragment CoralEmbedStream_CreateCommentResponse on CreateCommentResponse { comment { ...CoralEmbedStream_CreateCommentResponse_Comment - replies(query: {}) { + replies { nodes { ...CoralEmbedStream_CreateCommentResponse_Comment } diff --git a/client/coral-embed-stream/src/reducers/stream.js b/client/coral-embed-stream/src/reducers/stream.js index 86d5301b7..02d6933ba 100644 --- a/client/coral-embed-stream/src/reducers/stream.js +++ b/client/coral-embed-stream/src/reducers/stream.js @@ -23,6 +23,8 @@ const initialState = { commentClassNames: [], activeTab: process.env.TALK_DEFAULT_STREAM_TAB, previousTab: '', + sortBy: 'CREATED_AT', + sort: 'DESC', }; export default function stream(state = initialState, action) { diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js index b25e93040..9a2280233 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js @@ -6,7 +6,7 @@ import {withFragments, connect} from 'plugin-api/beta/client/hocs'; import Comment from '../containers/Comment'; import {addNotification} from 'plugin-api/beta/client/actions/notification'; import {viewComment} from 'coral-embed-stream/src/actions/stream'; -import {insertCommentsSorted, getDefinitionName} from 'plugin-api/beta/client/utils'; +import {getDefinitionName} from 'plugin-api/beta/client/utils'; import update from 'immutability-helper'; class TabPaneContainer extends React.Component { @@ -18,7 +18,8 @@ class TabPaneContainer extends React.Component { limit: 5, cursor: this.props.asset.featuredComments.endCursor, asset_id: this.props.asset.id, - sort: 'DESC', + sort: this.props.data.variables.sort, + sortBy: this.props.data.variables.sortBy, excludeIgnored: this.props.data.variables.excludeIgnored, }, updateQuery: (previous, {fetchMoreResult:{comments}}) => { @@ -26,7 +27,7 @@ class TabPaneContainer extends React.Component { asset: { featuredComments: { nodes: { - $apply: (nodes) => insertCommentsSorted(nodes, comments.nodes, 'DESC'), + $push: comments.nodes, }, hasNextPage: {$set: comments.hasNextPage}, endCursor: {$set: comments.endCursor}, @@ -47,8 +48,25 @@ class TabPaneContainer extends React.Component { } const LOAD_MORE_QUERY = gql` - query TalkFeaturedComments_LoadMoreComments($limit: Int = 5, $cursor: Cursor, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { - comments(query: {limit: $limit, cursor: $cursor, tags: ["FEATURED"], asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) { + query TalkFeaturedComments_LoadMoreComments( + $limit: Int = 5 + $cursor: Cursor + $asset_id: ID + $sort: SORT_ORDER + $sortBy: SORT_COMMENTS_BY + $excludeIgnored: Boolean + ) { + comments( + query: { + limit: $limit + cursor: $cursor + tags: ["FEATURED"] + asset_id: $asset_id, + sort: $sort + sortBy: $sortBy + excludeIgnored: $excludeIgnored + } + ) { nodes { ...${getDefinitionName(Comment.fragments.comment)} } @@ -79,7 +97,14 @@ const enhance = compose( asset: gql` fragment TalkFeaturedComments_TabPane_asset on Asset { id - featuredComments: comments(query: {tags: ["FEATURED"]}, deep: true) @skip(if: $hasComment) { + featuredComments: comments( + query: { + tags: ["FEATURED"] + sort: $sort + sortBy: $sortBy + } + deep: true + ) @skip(if: $hasComment) { nodes { ...${getDefinitionName(Comment.fragments.comment)} } diff --git a/test/server/graph/mutations/editComment.js b/test/server/graph/mutations/editComment.js index c6b13638b..8507fdc36 100644 --- a/test/server/graph/mutations/editComment.js +++ b/test/server/graph/mutations/editComment.js @@ -227,7 +227,7 @@ describe('graph.mutations.editComment', () => { asset_id: asset.id, author_id: user.id, }, - beforeEdit, + beforeEdit )); // now edit From 9a1374070feabd699040b38cb9144eb31132123a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 24 Aug 2017 09:47:27 -0600 Subject: [PATCH 064/121] small fixes --- .vscode/settings.json | 8 -------- graph/resolvers/user.js | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 838540390..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "eslint.autoFixOnSave": true, - "editor.tabSize": 2, - "editor.rulers": [ - 80 - ], - "files.trimTrailingWhitespace": true -} diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index e566a6af0..7c97e3833 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -42,7 +42,7 @@ const User = { return null; }, tokens({id, tokens}, args, {user}) { - if (!user || ((user.id !== id) && !user.can(LIST_OWN_TOKENS))) { + if (!user || ((user.id !== id) && !user.can(LIST_OWN_TOKENS))) { return null; } From 308449a67cc9a088e885cf9aa2b460f697a3be7a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 24 Aug 2017 10:24:56 -0600 Subject: [PATCH 065/121] added more messaging for cli tool --- bin/verifications/database/comments.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/verifications/database/comments.js b/bin/verifications/database/comments.js index e80e8ced6..f7dd4d397 100644 --- a/bin/verifications/database/comments.js +++ b/bin/verifications/database/comments.js @@ -21,7 +21,7 @@ module.exports = async ({fix, limit, batch}) => { let comments = []; let commentIDs = []; - debug(`Processing ${totalCount} comments...`); + console.log(`Processing ${totalCount} comments in batches of ${limit}...`); // Keep processing documents until there are is none left. while (offset < totalCount) { @@ -166,13 +166,13 @@ module.exports = async ({fix, limit, batch}) => { const OPERATIONS_LENGTH = operations.length; if (limit < Infinity && offset + comments.length < totalCount) { - debug(`Processed ${offset + comments.length}/${totalCount} comments because we reached the update limit of ${limit}.`); - debug(`Fixing ${OPERATIONS_LENGTH} documents.`); + console.log(`Processed ${offset + comments.length}/${totalCount} comments because we reached the update limit of ${limit}.`); } else { - debug(`Processed all ${totalCount} comments.`); - debug(`${OPERATIONS_LENGTH} documents need fixing.`); + 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) { @@ -185,7 +185,7 @@ module.exports = async ({fix, limit, batch}) => { debug(`Fixed batch of ${result.modifiedCount} documents.`); } - debug(`Applied all ${OPERATIONS_LENGTH} fixes.`); + console.log(`Applied all ${OPERATIONS_LENGTH} fixes.`); } else { console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors'); } From 13796fb26863505144b4cf1659ff6cdacf3ddeab Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 24 Aug 2017 11:18:17 -0600 Subject: [PATCH 066/121] added more test cases --- test/server/services/wordlist.js | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/server/services/wordlist.js b/test/server/services/wordlist.js index 6d0116e26..19545ff3e 100644 --- a/test/server/services/wordlist.js +++ b/test/server/services/wordlist.js @@ -13,6 +13,7 @@ describe('services.Wordlist', () => { 's h i t', '$hit', 'p**ch', + 'p*ch', ], suspect: [ 'do bad things', @@ -36,6 +37,7 @@ describe('services.Wordlist', () => { [ 's', 'h', 'i', 't' ], [ '$hit' ], [ 'p**ch' ], + [ 'p*ch' ], ]); expect(wordlist.lists.suspect).to.deep.equal([ [ 'do', 'bad', 'things' ], @@ -84,6 +86,7 @@ describe('services.Wordlist', () => { 'how to not do really bad things?', 'i have $100 dollars.', 'I have bad $ hit lling', + 'That\'s a p***ch!', ].forEach((word) => { expect(wordlist.match(bannedList, word)).to.be.false; }); @@ -91,6 +94,39 @@ describe('services.Wordlist', () => { }); + describe('#scan', () => { + + it('does match on a bad word', () => { + [ + 'how to do really bad things', + 'what is cookies', + 'cookies', + 'COOKIES.', + 'how to do bad things', + 'How To do bad things!', + 'This stuff is $hit!', + 'That\'s a p**ch!', + ].forEach((word) => { + expect(wordlist.scan('body', word)).to.not.be.undefined; + }); + }); + + it('does not match on a good word', () => { + [ + 'how to', + 'cookie', + 'how to be a great person?', + 'how to not do really bad things?', + 'i have $100 dollars.', + 'I have bad $ hit lling', + 'That\'s a p***ch!', + ].forEach((word) => { + expect(wordlist.scan('body', word)).to.be.undefined; + }); + }); + + }); + describe('#checkName', () => { [ 'flowers', From 7991188f8d2f59160e9fecc43678eb62f518b312 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 24 Aug 2017 14:45:47 -0300 Subject: [PATCH 067/121] deleting old code from default.css and building QuestionBox component --- .../components/ConfigureCommentStream.js | 1 - client/coral-embed-stream/style/default.css | 67 ++----------------- .../talk-plugin-questionbox/QuestionBox.css | 60 +++++++++++++++++ client/talk-plugin-questionbox/QuestionBox.js | 28 +++++--- models/setting.js | 4 ++ 5 files changed, 87 insertions(+), 73 deletions(-) create mode 100644 client/talk-plugin-questionbox/QuestionBox.css diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js index 5ee9b706f..f84923caf 100644 --- a/client/coral-configure/components/ConfigureCommentStream.js +++ b/client/coral-configure/components/ConfigureCommentStream.js @@ -65,7 +65,6 @@ export default ({handleChange, handleApply, changed, ...props}) => ( />
    -
    diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index d4078bf27..010a65ab1 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -79,6 +79,12 @@ body { } /* Info Box Styles */ + +.hidden { + visibility: hidden; + display: none; +} + .talk-plugin-infobox-info { top: 0; border: 0; @@ -93,7 +99,6 @@ body { border-radius: 2px; } - .talk-plugin-infobox-info em{ font-style: italic; } @@ -113,66 +118,6 @@ body { } /* Question Box Styles */ -.talk-plugin-questionbox-info { - top: 0; - border: 0; - background: #F0F0F0; - color: black; - width: 100%; - text-align: left; - padding-left: 0px; - margin-bottom: 0px; - font-weight: bold; - font-size: 14px; - overflow: hidden; - min-height: 50px; - display: flex; -} - -.talk-plugin-questionbox-icon.bubble{ - position: absolute; - top: 8px; - left: 10px; - color: #9E9E9E; - font-size: 24px; - z-index: 0; -} - -.talk-plugin-questionbox-icon.person{ - z-index: 2; - top: 12px; - left: 12px; - position: absolute; - font-size: 33px; - color: #262626; -} - -.talk-plugin-questionbox-box { - position: relative; - border: 0; - color: white; - padding: 20px; - margin-left: 0px !important; - margin-right: 10px; - display: inline-block; - width: 10px; - min-height: 100%; - padding: 5px 20px; - vertical-align: middle; -} - -.talk-plugin-questionbox-content { - padding: 5px; - display: flex; - align-items: center; - justify-content: center; - font-weight: 400; -} - -.hidden { - visibility: hidden; - display: none; -} .talk-stream-comments-container { position: relative; diff --git a/client/talk-plugin-questionbox/QuestionBox.css b/client/talk-plugin-questionbox/QuestionBox.css new file mode 100644 index 000000000..2bee1020f --- /dev/null +++ b/client/talk-plugin-questionbox/QuestionBox.css @@ -0,0 +1,60 @@ +.qbInfo { + top: 0; + border: 0; + background: #F0F0F0; + color: black; + width: 100%; + text-align: left; + padding-left: 0px; + margin-bottom: 0px; + font-weight: bold; + font-size: 14px; + overflow: hidden; + min-height: 50px; + display: flex; +} + +.iconBubble{ + position: absolute; + top: 8px; + left: 10px; + color: #9E9E9E; + font-size: 24px; + z-index: 0; +} + +.iconPerson{ + z-index: 2; + top: 12px; + left: 12px; + position: absolute; + font-size: 33px; + color: #262626; +} + +.qbBox { + position: relative; + border: 0; + color: white; + padding: 20px; + margin-left: 0px !important; + margin-right: 10px; + display: inline-block; + width: 10px; + min-height: 100%; + padding: 5px 20px; + vertical-align: middle; +} + +.qbContent { + padding: 5px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 400; +} + +.hidden { + visibility: hidden; + display: none; +} \ No newline at end of file diff --git a/client/talk-plugin-questionbox/QuestionBox.js b/client/talk-plugin-questionbox/QuestionBox.js index d22801bae..3f804dacb 100644 --- a/client/talk-plugin-questionbox/QuestionBox.js +++ b/client/talk-plugin-questionbox/QuestionBox.js @@ -1,17 +1,23 @@ import React from 'react'; -const packagename = 'talk-plugin-questionbox'; +import cn from 'classnames'; +import styles from './QuestionBox.css'; +import {Icon} from 'coral-ui'; + import Slot from 'coral-framework/components/Slot'; -const QuestionBox = ({enable, content}) => -
    -
    - chat_bubble - person +const QuestionBox = ({content, enable}) => ( +
    +
    + + +
    + +
    + {content} +
    + +
    -
    - {content} -
    - -
    ; +); export default QuestionBox; diff --git a/models/setting.js b/models/setting.js index ca8dfa456..c9dd45984 100644 --- a/models/setting.js +++ b/models/setting.js @@ -33,6 +33,10 @@ const SettingSchema = new Schema({ type: Boolean, default: false }, + questionBoxIcon: { + type: String, + default: '' + }, questionBoxContent: { type: String, default: '' From 60677e3d2a29902b86d04f902b6ae594f21ce576 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 24 Aug 2017 13:07:44 -0600 Subject: [PATCH 068/121] cleaned up user query logic --- graph/resolvers/user.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index e566a6af0..c2818bd20 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -24,13 +24,16 @@ const User = { }, comments({id}, {query}, {loaders: {Comments}, user}) { - // If the user is not an admin, only return comment list for the owner of - // the comments. - if (user && (user.can(SEARCH_OTHERS_COMMENTS) || user.id === id)) { - return Comments.getByQuery(Object.assign({}, query, {author_id: id})); + // If there is no user, or there is a user, but they are requesting someone + // else's comments, and they aren't allowed, don't return then anything! + if (!user || (user.id !== id && !user.can(SEARCH_OTHERS_COMMENTS))) { + return null; } - return null; + // Set the author id on the query. + query.author_id = id; + + return Comments.getByQuery(query); }, profiles({profiles}, _, {user}) { From 5cb71eaee8b3411ad3cf6ea6314300d29cfe0161 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 24 Aug 2017 16:32:07 -0300 Subject: [PATCH 069/121] Icon List for Question Box --- .../components/ConfigureCommentStream.css | 6 +- .../components/ConfigureCommentStream.js | 19 +++-- .../components/DefaultIcon.css | 27 +++++++ .../coral-configure/components/DefaultIcon.js | 13 ++++ .../components/QuestionBoxBuilder.css | 39 ++++++++++ .../components/QuestionBoxBuilder.js | 73 +++++++++++++++++++ .../containers/ConfigureStreamContainer.js | 6 ++ .../src/components/Stream.js | 1 + .../src/containers/Stream.js | 1 + .../talk-plugin-questionbox/QuestionBox.css | 11 ++- client/talk-plugin-questionbox/QuestionBox.js | 19 +++-- graph/typeDefs.graphql | 1 + models/setting.js | 2 +- 13 files changed, 195 insertions(+), 23 deletions(-) create mode 100644 client/coral-configure/components/DefaultIcon.css create mode 100644 client/coral-configure/components/DefaultIcon.js create mode 100644 client/coral-configure/components/QuestionBoxBuilder.css create mode 100644 client/coral-configure/components/QuestionBoxBuilder.js diff --git a/client/coral-configure/components/ConfigureCommentStream.css b/client/coral-configure/components/ConfigureCommentStream.css index a8cbc36cb..5575e073b 100644 --- a/client/coral-configure/components/ConfigureCommentStream.css +++ b/client/coral-configure/components/ConfigureCommentStream.css @@ -12,10 +12,6 @@ padding: 0; } -.wrapper ul ul { - padding-left: 20px -} - .checkbox { vertical-align: top; margin: 12px 12px 12px 0; @@ -36,4 +32,4 @@ .hidden { display: none; -} +} \ No newline at end of file diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js index f84923caf..726b54e19 100644 --- a/client/coral-configure/components/ConfigureCommentStream.js +++ b/client/coral-configure/components/ConfigureCommentStream.js @@ -1,5 +1,6 @@ import React from 'react'; -import {Button, Checkbox, TextField} from 'coral-ui'; +import {Button, Checkbox} from 'coral-ui'; +import QuestionBoxBuilder from './QuestionBoxBuilder'; import styles from './ConfigureCommentStream.css'; @@ -55,15 +56,13 @@ export default ({handleChange, handleApply, changed, ...props}) => ( title: t('configure.enable_questionbox'), description: t('configure.enable_questionbox_description') }} /> -
    - -
    + { + props.questionBoxEnable && + }
    diff --git a/client/coral-configure/components/DefaultIcon.css b/client/coral-configure/components/DefaultIcon.css new file mode 100644 index 000000000..6125d2f6a --- /dev/null +++ b/client/coral-configure/components/DefaultIcon.css @@ -0,0 +1,27 @@ +.iconBubble{ + position: absolute; + top: 8px; + left: 10px; + color: #9E9E9E; + font-size: 24px; + z-index: 0; +} + +.iconPerson{ + z-index: 2; + top: 12px; + left: 12px; + position: absolute; + font-size: 33px; + color: #262626; +} + +.qbIconContainer { + position: relative; + border: 0; + color: white; + display: inline-block; + padding: 5px 20px; + vertical-align: middle; + width: 10px; +} \ No newline at end of file diff --git a/client/coral-configure/components/DefaultIcon.js b/client/coral-configure/components/DefaultIcon.js new file mode 100644 index 000000000..5f6f4876c --- /dev/null +++ b/client/coral-configure/components/DefaultIcon.js @@ -0,0 +1,13 @@ +import React from 'react'; +import cn from 'classnames'; +import styles from './DefaultIcon.css'; +import {Icon} from 'coral-ui'; + +const DefaultIcon = ({className}) => ( +
    + + +
    +); + +export default DefaultIcon; diff --git a/client/coral-configure/components/QuestionBoxBuilder.css b/client/coral-configure/components/QuestionBoxBuilder.css new file mode 100644 index 000000000..d8ddcd090 --- /dev/null +++ b/client/coral-configure/components/QuestionBoxBuilder.css @@ -0,0 +1,39 @@ +.qbBuilder { + margin-left: 50px; +} + +.qbItemIconList { + padding: 0; + margin: 10px 0; +} + +.qbItemIcon { + background: #F0F0F0; + width: 45px; + height: 45px; + font-size: 24px; + text-align: center; + line-height: 45px; + color: #252525; + border-radius: 3px; + display: inline-block; + overflow: hidden; + margin-right: 10px; + position: relative; +} + +.qbItemIcon:hover { + cursor: pointer; +} + +.qbItemIconActive { + border: solid 2px #00796B; +} + +.defaultIcon { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/client/coral-configure/components/QuestionBoxBuilder.js b/client/coral-configure/components/QuestionBoxBuilder.js new file mode 100644 index 000000000..f37b5da53 --- /dev/null +++ b/client/coral-configure/components/QuestionBoxBuilder.js @@ -0,0 +1,73 @@ +import React from 'react'; +import QuestionBox from 'talk-plugin-questionbox/QuestionBox'; +import {Icon, TextField} from 'coral-ui'; +import DefaultIcon from './DefaultIcon'; +import cn from 'classnames'; +import styles from './QuestionBoxBuilder.css'; + +class QuestionBoxBuilder extends React.Component { + render() { + const {handleChange, questionBoxIcon, questionBoxContent} = this.props; + + return ( +
    +

    Include an Icon

    + +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    + + + + +
    + ); + } +} + +export default QuestionBoxBuilder; \ No newline at end of file diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index 1db8f497a..59e81e8c5 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -27,6 +27,7 @@ class ConfigureStreamContainer extends Component { handleApply (e) { e.preventDefault(); const {elements} = e.target; + const {questionBoxIcon} = this.state.dirtySettings; const premod = elements.premod.checked; const questionBoxEnable = elements.qboxenable.checked; const questionBoxContent = elements.qboxcontent.value; @@ -38,6 +39,7 @@ class ConfigureStreamContainer extends Component { moderation: premod ? 'PRE' : 'POST', questionBoxEnable, questionBoxContent, + questionBoxIcon, premodLinksEnable }; @@ -60,6 +62,9 @@ class ConfigureStreamContainer extends Component { if (e.target && e.target.id === 'qboxcontent') { changes.questionBoxContent = e.target.value; } + if (e.currentTarget && e.currentTarget.id === 'qboxicon') { + changes.questionBoxIcon = e.currentTarget.dataset.icon; + } if (e.target && e.target.id === 'plinksenable') { changes.premodLinksEnable = e.target.value; } @@ -105,6 +110,7 @@ class ConfigureStreamContainer extends Component { changed={this.state.changed} premodLinksEnable={dirtySettings.premodLinksEnable} premod={premod} + questionBoxIcon={dirtySettings.questionBoxIcon} questionBoxEnable={dirtySettings.questionBoxEnable} questionBoxContent={dirtySettings.questionBoxContent} /> diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index b37d232f7..1f2ac3eca 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -129,6 +129,7 @@ class Stream extends React.Component { {!banned && temporarilySuspended && diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index ef5838453..9a0b73ecc 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -247,6 +247,7 @@ const fragments = { premodLinksEnable questionBoxEnable questionBoxContent + questionBoxIcon closeTimeout closedMessage charCountEnable diff --git a/client/talk-plugin-questionbox/QuestionBox.css b/client/talk-plugin-questionbox/QuestionBox.css index 2bee1020f..19902c984 100644 --- a/client/talk-plugin-questionbox/QuestionBox.css +++ b/client/talk-plugin-questionbox/QuestionBox.css @@ -14,6 +14,15 @@ display: flex; } +.icon { + z-index: 2; + top: 12px; + left: 12px; + position: absolute; + font-size: 33px; + color: #262626; +} + .iconBubble{ position: absolute; top: 8px; @@ -32,7 +41,7 @@ color: #262626; } -.qbBox { +.qbIconContainer { position: relative; border: 0; color: white; diff --git a/client/talk-plugin-questionbox/QuestionBox.js b/client/talk-plugin-questionbox/QuestionBox.js index 3f804dacb..83e6db96f 100644 --- a/client/talk-plugin-questionbox/QuestionBox.js +++ b/client/talk-plugin-questionbox/QuestionBox.js @@ -5,13 +5,20 @@ import {Icon} from 'coral-ui'; import Slot from 'coral-framework/components/Slot'; -const QuestionBox = ({content, enable}) => ( +const QuestionBox = ({content, enable, icon = ''}) => (
    -
    - - -
    - + { + icon === 'default' ? ( +
    + + +
    + ) : ( +
    + +
    + ) + }
    {content}
    diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 0e430cd84..9d1376372 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -545,6 +545,7 @@ type Settings { premodLinksEnable: Boolean questionBoxEnable: Boolean questionBoxContent: String + questionBoxIcon: String closeTimeout: Int closedMessage: String charCountEnable: Boolean diff --git a/models/setting.js b/models/setting.js index c9dd45984..222541df6 100644 --- a/models/setting.js +++ b/models/setting.js @@ -35,7 +35,7 @@ const SettingSchema = new Schema({ }, questionBoxIcon: { type: String, - default: '' + default: 'default' }, questionBoxContent: { type: String, From 73058c624fe72a7475d9df8ea40d6331fe88019e Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 24 Aug 2017 16:32:51 -0300 Subject: [PATCH 070/121] Linting --- client/coral-configure/components/QuestionBoxBuilder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-configure/components/QuestionBoxBuilder.js b/client/coral-configure/components/QuestionBoxBuilder.js index f37b5da53..17d33beb6 100644 --- a/client/coral-configure/components/QuestionBoxBuilder.js +++ b/client/coral-configure/components/QuestionBoxBuilder.js @@ -70,4 +70,4 @@ class QuestionBoxBuilder extends React.Component { } } -export default QuestionBoxBuilder; \ No newline at end of file +export default QuestionBoxBuilder; From 575a3030f93af55fadceca27b52adfb8d2504cca Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 24 Aug 2017 16:40:16 -0300 Subject: [PATCH 071/121] Smooth transition --- client/coral-configure/components/QuestionBoxBuilder.css | 2 ++ client/talk-plugin-questionbox/QuestionBox.css | 1 + 2 files changed, 3 insertions(+) diff --git a/client/coral-configure/components/QuestionBoxBuilder.css b/client/coral-configure/components/QuestionBoxBuilder.css index d8ddcd090..c8f6c6a03 100644 --- a/client/coral-configure/components/QuestionBoxBuilder.css +++ b/client/coral-configure/components/QuestionBoxBuilder.css @@ -20,6 +20,8 @@ overflow: hidden; margin-right: 10px; position: relative; + border: solid 2px #F0F0F0; + transition: border 0.3s cubic-bezier(.4,0,.2,1); } .qbItemIcon:hover { diff --git a/client/talk-plugin-questionbox/QuestionBox.css b/client/talk-plugin-questionbox/QuestionBox.css index 19902c984..cb9902ade 100644 --- a/client/talk-plugin-questionbox/QuestionBox.css +++ b/client/talk-plugin-questionbox/QuestionBox.css @@ -12,6 +12,7 @@ overflow: hidden; min-height: 50px; display: flex; + border-radius: 3px; } .icon { From 3d0d5df139b02d232273f72b986d394b8fac544a Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 24 Aug 2017 18:16:32 -0300 Subject: [PATCH 072/121] Markdown Added --- .../Configure/components/StreamSettings.js | 2 +- client/coral-configure/components/Markdown.js | 23 +++++++++++++++++++ .../components/QuestionBoxBuilder.js | 13 +++++------ .../containers/ConfigureStreamContainer.js | 13 +++++++---- .../components/MarkdownEditor.css | 0 .../components/MarkdownEditor.js | 0 6 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 client/coral-configure/components/Markdown.js rename client/{coral-admin/src => coral-framework}/components/MarkdownEditor.css (100%) rename client/{coral-admin/src => coral-framework}/components/MarkdownEditor.js (100%) diff --git a/client/coral-admin/src/routes/Configure/components/StreamSettings.js b/client/coral-admin/src/routes/Configure/components/StreamSettings.js index 1bd7534af..9c216a853 100644 --- a/client/coral-admin/src/routes/Configure/components/StreamSettings.js +++ b/client/coral-admin/src/routes/Configure/components/StreamSettings.js @@ -4,7 +4,7 @@ import t from 'coral-framework/services/i18n'; import styles from './Configure.css'; import {Checkbox, Textfield} from 'react-mdl'; import {Card, Icon, TextArea} from 'coral-ui'; -import MarkdownEditor from 'coral-admin/src/components/MarkdownEditor'; +import MarkdownEditor from 'coral-framework/components/MarkdownEditor'; const TIMESTAMPS = { weeks: 60 * 60 * 24 * 7, diff --git a/client/coral-configure/components/Markdown.js b/client/coral-configure/components/Markdown.js new file mode 100644 index 000000000..482467987 --- /dev/null +++ b/client/coral-configure/components/Markdown.js @@ -0,0 +1,23 @@ +import React, {PureComponent, PropTypes} from 'react'; +import marked from 'marked'; + +const renderer = new marked.Renderer(); + +// Set link target to `_parent` to work properly in an embed. +renderer.link = (href, title, text) => + `${text}`; + +marked.setOptions({renderer}); + +export default class Markdown extends PureComponent { + render() { + const {content, ...rest} = this.props; + const __html = marked(content); + return
    ; + } +} + +Markdown.propTypes = { + content: PropTypes.string, +}; + diff --git a/client/coral-configure/components/QuestionBoxBuilder.js b/client/coral-configure/components/QuestionBoxBuilder.js index 17d33beb6..ca19c0e7b 100644 --- a/client/coral-configure/components/QuestionBoxBuilder.js +++ b/client/coral-configure/components/QuestionBoxBuilder.js @@ -1,9 +1,10 @@ import React from 'react'; import QuestionBox from 'talk-plugin-questionbox/QuestionBox'; -import {Icon, TextField} from 'coral-ui'; +import {Icon} from 'coral-ui'; import DefaultIcon from './DefaultIcon'; import cn from 'classnames'; import styles from './QuestionBoxBuilder.css'; +import MarkdownEditor from 'coral-framework/components/MarkdownEditor'; class QuestionBoxBuilder extends React.Component { render() { @@ -57,14 +58,12 @@ class QuestionBoxBuilder extends React.Component { icon={questionBoxIcon} content={questionBoxContent} /> - - handleChange({}, {questionBoxContent: value})} /> +
    ); } diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index 59e81e8c5..a5d6cf8aa 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -53,15 +53,16 @@ class ConfigureStreamContainer extends Component { } } - handleChange (e) { - const changes = {}; + handleChange (e, newChanges) { + let changes = {}; + + if (changes) { + changes = {...newChanges}; + } if (e.target && e.target.id === 'qboxenable') { changes.questionBoxEnable = e.target.checked; } - if (e.target && e.target.id === 'qboxcontent') { - changes.questionBoxContent = e.target.value; - } if (e.currentTarget && e.currentTarget.id === 'qboxicon') { changes.questionBoxIcon = e.currentTarget.dataset.icon; } @@ -69,6 +70,8 @@ class ConfigureStreamContainer extends Component { changes.premodLinksEnable = e.target.value; } + console.log(changes) + this.setState({ changed: true, dirtySettings: { diff --git a/client/coral-admin/src/components/MarkdownEditor.css b/client/coral-framework/components/MarkdownEditor.css similarity index 100% rename from client/coral-admin/src/components/MarkdownEditor.css rename to client/coral-framework/components/MarkdownEditor.css diff --git a/client/coral-admin/src/components/MarkdownEditor.js b/client/coral-framework/components/MarkdownEditor.js similarity index 100% rename from client/coral-admin/src/components/MarkdownEditor.js rename to client/coral-framework/components/MarkdownEditor.js From 5302d6d9357a832128fcfa41347543b072629732 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 24 Aug 2017 18:21:52 -0300 Subject: [PATCH 073/121] Markdown Editor Added and Working --- client/coral-configure/components/QuestionBoxBuilder.css | 4 ++++ client/coral-configure/components/QuestionBoxBuilder.js | 3 ++- .../coral-configure/containers/ConfigureStreamContainer.js | 6 +----- client/coral-framework/components/MarkdownEditor.css | 3 ++- client/talk-plugin-questionbox/QuestionBox.js | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/client/coral-configure/components/QuestionBoxBuilder.css b/client/coral-configure/components/QuestionBoxBuilder.css index c8f6c6a03..758f350b9 100644 --- a/client/coral-configure/components/QuestionBoxBuilder.css +++ b/client/coral-configure/components/QuestionBoxBuilder.css @@ -38,4 +38,8 @@ left: 0; width: 100%; height: 100%; +} + +.qb { + margin: 10px 0; } \ No newline at end of file diff --git a/client/coral-configure/components/QuestionBoxBuilder.js b/client/coral-configure/components/QuestionBoxBuilder.js index ca19c0e7b..5444102d2 100644 --- a/client/coral-configure/components/QuestionBoxBuilder.js +++ b/client/coral-configure/components/QuestionBoxBuilder.js @@ -54,6 +54,7 @@ class QuestionBoxBuilder extends React.Component { handleChange({}, {questionBoxContent: value})} /> - +
    ); } diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index a5d6cf8aa..3946ff6ed 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -27,11 +27,9 @@ class ConfigureStreamContainer extends Component { handleApply (e) { e.preventDefault(); const {elements} = e.target; - const {questionBoxIcon} = this.state.dirtySettings; + const {questionBoxIcon, questionBoxContent} = this.state.dirtySettings; const premod = elements.premod.checked; const questionBoxEnable = elements.qboxenable.checked; - const questionBoxContent = elements.qboxcontent.value; - const premodLinksEnable = elements.plinksenable.checked; const {changed} = this.state; @@ -70,8 +68,6 @@ class ConfigureStreamContainer extends Component { changes.premodLinksEnable = e.target.value; } - console.log(changes) - this.setState({ changed: true, dirtySettings: { diff --git a/client/coral-framework/components/MarkdownEditor.css b/client/coral-framework/components/MarkdownEditor.css index 93bf7a9dd..201ba10bc 100644 --- a/client/coral-framework/components/MarkdownEditor.css +++ b/client/coral-framework/components/MarkdownEditor.css @@ -467,7 +467,6 @@ $fullscreenZIndex: 10; text-align: center; text-decoration: none!important; color: #2c3e50!important; - width: 30px; height: 30px; margin: 0; border: 1px solid transparent; @@ -475,6 +474,8 @@ $fullscreenZIndex: 10; cursor: pointer; outline: 0; margin-right: 2px; + font-size: 1.5em; + width: 25px; &.active { background: #fcfcfc; border-color: #95a5a6; diff --git a/client/talk-plugin-questionbox/QuestionBox.js b/client/talk-plugin-questionbox/QuestionBox.js index 83e6db96f..8cc8d7efa 100644 --- a/client/talk-plugin-questionbox/QuestionBox.js +++ b/client/talk-plugin-questionbox/QuestionBox.js @@ -5,8 +5,8 @@ import {Icon} from 'coral-ui'; import Slot from 'coral-framework/components/Slot'; -const QuestionBox = ({content, enable, icon = ''}) => ( -
    +const QuestionBox = ({content, enable, icon = '', className = ''}) => ( +
    { icon === 'default' ? (
    From de7ab8f7ac6a9125efd6d6fad47b2dd4bff6c21b Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 24 Aug 2017 19:05:57 -0300 Subject: [PATCH 074/121] With fancy lazy load for the Markdown Editor :D --- client/.babelrc | 3 +- .../components/QuestionBoxBuilder.js | 29 +++++++++++++++++-- package.json | 1 + 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/client/.babelrc b/client/.babelrc index 60be246eb..2af1a3dad 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -9,6 +9,7 @@ "transform-object-assign", "transform-object-rest-spread", "transform-async-to-generator", - "transform-react-jsx" + "transform-react-jsx", + "syntax-dynamic-import" ] } \ No newline at end of file diff --git a/client/coral-configure/components/QuestionBoxBuilder.js b/client/coral-configure/components/QuestionBoxBuilder.js index 5444102d2..d3285b173 100644 --- a/client/coral-configure/components/QuestionBoxBuilder.js +++ b/client/coral-configure/components/QuestionBoxBuilder.js @@ -4,11 +4,36 @@ import {Icon} from 'coral-ui'; import DefaultIcon from './DefaultIcon'; import cn from 'classnames'; import styles from './QuestionBoxBuilder.css'; -import MarkdownEditor from 'coral-framework/components/MarkdownEditor'; class QuestionBoxBuilder extends React.Component { + constructor() { + super(); + + this.state = { + loading: true + }; + } + + componentWillMount() { + this.loadEditor(); + } + + async loadEditor() { + const MarkdownEditor = await import('coral-framework/components/MarkdownEditor'); + + return this.setState({ + loading : false, + MarkdownEditor + }); + } + render() { const {handleChange, questionBoxIcon, questionBoxContent} = this.props; + const {loading, MarkdownEditor} = this.state; + + if (loading) { + return
    Loading
    ; + } return (
    @@ -64,7 +89,7 @@ class QuestionBoxBuilder extends React.Component { value={questionBoxContent} onChange={(value) => handleChange({}, {questionBoxContent: value})} /> - +
    ); } diff --git a/package.json b/package.json index 71477361e..09980841d 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "babel-jest": "^19.0.0", "babel-loader": "^6.4.1", "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-plugin-transform-async-to-generator": "^6.16.0", "babel-plugin-transform-class-properties": "^6.23.0", "babel-plugin-transform-decorators-legacy": "^1.3.4", From 46f17f538136daf34c8b3522a77230e3d0b96e5d Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 24 Aug 2017 19:10:30 -0300 Subject: [PATCH 075/121] Fully working :) --- client/coral-configure/components/QuestionBoxBuilder.js | 4 ++-- .../components}/Markdown.js | 0 client/talk-plugin-infobox/InfoBox.js | 2 +- client/talk-plugin-questionbox/QuestionBox.js | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) rename client/{talk-plugin-infobox => coral-framework/components}/Markdown.js (100%) diff --git a/client/coral-configure/components/QuestionBoxBuilder.js b/client/coral-configure/components/QuestionBoxBuilder.js index d3285b173..8ccc6ba31 100644 --- a/client/coral-configure/components/QuestionBoxBuilder.js +++ b/client/coral-configure/components/QuestionBoxBuilder.js @@ -1,6 +1,6 @@ import React from 'react'; import QuestionBox from 'talk-plugin-questionbox/QuestionBox'; -import {Icon} from 'coral-ui'; +import {Icon, Spinner} from 'coral-ui'; import DefaultIcon from './DefaultIcon'; import cn from 'classnames'; import styles from './QuestionBoxBuilder.css'; @@ -32,7 +32,7 @@ class QuestionBoxBuilder extends React.Component { const {loading, MarkdownEditor} = this.state; if (loading) { - return
    Loading
    ; + return ; } return ( diff --git a/client/talk-plugin-infobox/Markdown.js b/client/coral-framework/components/Markdown.js similarity index 100% rename from client/talk-plugin-infobox/Markdown.js rename to client/coral-framework/components/Markdown.js diff --git a/client/talk-plugin-infobox/InfoBox.js b/client/talk-plugin-infobox/InfoBox.js index 3b9d7da75..658f4ffcc 100644 --- a/client/talk-plugin-infobox/InfoBox.js +++ b/client/talk-plugin-infobox/InfoBox.js @@ -1,5 +1,5 @@ import React from 'react'; -import Markdown from './Markdown'; +import Markdown from 'coral-framework/components/Markdown'; const packagename = 'talk-plugin-infobox'; diff --git a/client/talk-plugin-questionbox/QuestionBox.js b/client/talk-plugin-questionbox/QuestionBox.js index 8cc8d7efa..f15549b7c 100644 --- a/client/talk-plugin-questionbox/QuestionBox.js +++ b/client/talk-plugin-questionbox/QuestionBox.js @@ -2,6 +2,7 @@ import React from 'react'; import cn from 'classnames'; import styles from './QuestionBox.css'; import {Icon} from 'coral-ui'; +import Markdown from 'coral-framework/components/Markdown'; import Slot from 'coral-framework/components/Slot'; @@ -20,7 +21,7 @@ const QuestionBox = ({content, enable, icon = '', className = ''}) => ( ) }
    - {content} +
    From f0931b4dc6e03d173ce60a3fb21a9fbb501c2b9b Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 25 Aug 2017 19:21:14 +0700 Subject: [PATCH 076/121] Fix asset.comment resolver --- graph/resolvers/asset.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js index ebae4e5a7..a48af7674 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -4,7 +4,7 @@ const Asset = { async comment({id}, {id: commentId}, {loaders: {Comments}}) { // Load the comment from the database. - const comment = Comments.get.load(id); + const comment = await Comments.get.load(commentId); if (!comment) { return null; } From 6ae68a4f90b22c38c499e7411ed339a545e6684f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 25 Aug 2017 19:21:57 +0700 Subject: [PATCH 077/121] First working version --- .eslintignore | 1 + .gitignore | 1 + .../coral-embed-stream/src/actions/stream.js | 2 + .../src/components/Stream.js | 265 +++++++++++------- .../src/constants/stream.js | 1 + .../src/containers/Embed.js | 8 +- .../src/containers/Stream.js | 35 ++- .../coral-embed-stream/src/reducers/stream.js | 8 +- client/coral-framework/components/Slot.js | 58 +++- client/coral-framework/utils/index.js | 1 + plugin-api/beta/client/actions/stream.js | 1 + .../beta/client/components/SortOption.css | 8 + .../beta/client/components/SortOption.js | 15 + plugin-api/beta/client/components/index.js | 2 + plugin-api/beta/client/hocs/index.js | 1 + plugin-api/beta/client/hocs/withSortOption.js | 40 +++ plugin-api/beta/client/selectors/stream.js | 2 + plugin-api/beta/client/utils/index.js | 1 + .../client/containers/TabPane.js | 8 +- .../client/components/styles.css | 1 - plugins/talk-plugin-offtopic/client/index.js | 4 +- .../talk-plugin-sort-oldest/client/.babelrc | 14 + .../client/.eslintrc.json | 23 ++ .../client/components/SortOption.css | 9 + .../client/components/SortOption.js | 15 + .../client/containers/SortOption.js | 4 + .../client/containers/SortOptionNewest.js | 4 + .../talk-plugin-sort-oldest/client/index.js | 15 + .../client/translations.yml | 4 + plugins/talk-plugin-sort-oldest/index.js | 2 + .../client/components/Category.css | 15 + .../client/components/Category.js | 16 ++ .../client/components/ViewingOptions.css | 4 +- .../client/components/ViewingOptions.js | 81 +++--- .../client/translations.yml | 2 + 35 files changed, 502 insertions(+), 169 deletions(-) create mode 100644 plugin-api/beta/client/actions/stream.js create mode 100644 plugin-api/beta/client/components/SortOption.css create mode 100644 plugin-api/beta/client/components/SortOption.js create mode 100644 plugin-api/beta/client/hocs/withSortOption.js create mode 100644 plugin-api/beta/client/selectors/stream.js create mode 100644 plugins/talk-plugin-sort-oldest/client/.babelrc create mode 100644 plugins/talk-plugin-sort-oldest/client/.eslintrc.json create mode 100644 plugins/talk-plugin-sort-oldest/client/components/SortOption.css create mode 100644 plugins/talk-plugin-sort-oldest/client/components/SortOption.js create mode 100644 plugins/talk-plugin-sort-oldest/client/containers/SortOption.js create mode 100644 plugins/talk-plugin-sort-oldest/client/containers/SortOptionNewest.js create mode 100644 plugins/talk-plugin-sort-oldest/client/index.js create mode 100644 plugins/talk-plugin-sort-oldest/client/translations.yml create mode 100644 plugins/talk-plugin-sort-oldest/index.js create mode 100644 plugins/talk-plugin-viewing-options/client/components/Category.css create mode 100644 plugins/talk-plugin-viewing-options/client/components/Category.js diff --git a/.eslintignore b/.eslintignore index 2702e0a4d..3811f290c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,5 +13,6 @@ plugins/* !plugins/talk-plugin-comment-content !plugins/talk-plugin-permalink !plugins/talk-plugin-featured-comments +!plugins/talk-plugin-sort-oldest node_modules diff --git a/.gitignore b/.gitignore index 882c24883..3f5cbc085 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,6 @@ plugins/* !plugins/talk-plugin-comment-content !plugins/talk-plugin-permalink !plugins/talk-plugin-featured-comments +!plugins/talk-plugin-sort-oldest **/node_modules/* diff --git a/client/coral-embed-stream/src/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js index c20760259..d7301f8a1 100644 --- a/client/coral-embed-stream/src/actions/stream.js +++ b/client/coral-embed-stream/src/actions/stream.js @@ -5,6 +5,8 @@ import queryString from 'query-string'; export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id}); +export const setSort = ({order, by}) => ({type: actions.SET_SORT, order, by}); + export const viewAllComments = () => { const search = queryString.stringify({ diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index b37d232f7..544515da4 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -13,7 +13,7 @@ import t, {timeago} from 'coral-framework/services/i18n'; import CommentBox from 'talk-plugin-commentbox/CommentBox'; import QuestionBox from 'talk-plugin-questionbox/QuestionBox'; import {isCommentActive} from 'coral-framework/utils'; -import {Button, Tab, TabCount, TabPane} from 'coral-ui'; +import {Spinner, Button, Tab, TabCount, TabPane} from 'coral-ui'; import cn from 'classnames'; import {getTopLevelParent, attachCommentToParent} from '../graphql/utils'; @@ -23,6 +23,8 @@ import StreamTabPanel from '../containers/StreamTabPanel'; import styles from './Stream.css'; +const SpinnerWhenLoading = ({loading, children}) => loading ? :
    {children}
    ; + class Stream extends React.Component { constructor(props) { @@ -49,15 +51,14 @@ class Stream extends React.Component { ); }; - render() { + renderHighlightedComment() { const { data, root, activeReplyBox, setActiveReplyBox, - appendItemArray, commentClassNames, - root: {asset, asset: {comment, comments, totalCommentCount}}, + root: {asset, asset: {comment}}, postComment, addNotification, editComment, @@ -65,23 +66,15 @@ class Stream extends React.Component { postDontAgree, deleteAction, showSignInDialog, - updateItem, ignoreUser, - activeStreamTab, - setActiveStreamTab, loadNewReplies, - loadMoreComments, - viewAllComments, - auth: {loggedIn, user}, - editName, + auth: {user}, emit, } = this.props; - const {keepCommentBox} = this.state; - const open = !asset.isClosed; // even though the permalinked comment is the highlighted one, we're displaying its parent + replies - let highlightedComment = comment && getTopLevelParent(comment); - if (highlightedComment) { + let topLevelComment = getTopLevelParent(comment); + if (topLevelComment) { // Inactive comments can be viewed by moderators and admins (e.g. using permalinks). const isInactive = !isCommentActive(comment.status); @@ -89,10 +82,152 @@ class Stream extends React.Component { // the highlighted comment is not active and as such not in the replies, so we // attach it to the right parent. - highlightedComment = attachCommentToParent(highlightedComment, comment); + topLevelComment = attachCommentToParent(topLevelComment, comment); } } + return ( +
    + +
    + ); + } + + renderTabPanel() { + const { + data, + root, + activeReplyBox, + setActiveReplyBox, + commentClassNames, + root: {asset, asset: {comments, totalCommentCount}}, + postComment, + addNotification, + editComment, + postFlag, + postDontAgree, + deleteAction, + showSignInDialog, + ignoreUser, + activeStreamTab, + setActiveStreamTab, + loadNewReplies, + loadMoreComments, + auth: {user}, + emit, + sortOrder, + sortBy, + } = this.props; + + const slotProps = {data}; + const slotQueryData = {root, asset}; + + // `key` of `StreamTabPanel` depends on sorting so that we always reset + // the state when changing sorting. + return ( +
    +
    + +
    + + All Comments {totalCommentCount} + + } + appendTabPanes={ + + + + } + sub + /> +
    + ); + } + + render() { + const { + data, + root, + appendItemArray, + root: {asset, asset: {comment: highlightedComment, comments}}, + postComment, + addNotification, + updateItem, + viewAllComments, + auth: {loggedIn, user}, + editName, + loading, + } = this.props; + const {keepCommentBox} = this.state; + const open = !asset.isClosed; + const banned = user && user.status === 'BANNED'; const temporarilySuspended = user && @@ -103,7 +238,7 @@ class Stream extends React.Component { const slotProps = {data}; const slotQueryData = {root, asset}; - if (!comment && !comments) { + if (!highlightedComment && !comments) { console.error('Talk: No comments came back from the graph given that query. Please, check the query params.'); return ; } @@ -111,7 +246,7 @@ class Stream extends React.Component { return (
    - {comment && + {highlightedComment &&
    ); } diff --git a/client/coral-embed-stream/src/constants/stream.js b/client/coral-embed-stream/src/constants/stream.js index 7ac9427cf..b4deb24e1 100644 --- a/client/coral-embed-stream/src/constants/stream.js +++ b/client/coral-embed-stream/src/constants/stream.js @@ -6,3 +6,4 @@ export const ADD_COMMENT_CLASSNAME = 'ADD_COMMENT_CLASSNAME'; export const REMOVE_COMMENT_CLASSNAME = 'REMOVE_COMMENT_CLASSNAME'; export const THREADING_LEVEL = process.env.TALK_THREADING_LEVEL; export const SET_ACTIVE_TAB = 'CORAL_STREAM_SET_ACTIVE_TAB'; +export const SET_SORT = 'CORAL_STREAM_SET_SORT'; diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 5a0ea534d..289d370a1 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -158,7 +158,7 @@ const EMBED_QUERY = gql` $hasComment: Boolean!, $excludeIgnored: Boolean, $sortBy: SORT_COMMENTS_BY!, - $sort: SORT_ORDER!, + $sortOrder: SORT_ORDER!, ) { me { id @@ -171,7 +171,7 @@ const EMBED_QUERY = gql` `; export const withEmbedQuery = withQuery(EMBED_QUERY, { - options: ({auth, commentId, assetId, assetUrl, sortBy, sort}) => ({ + options: ({auth, commentId, assetId, assetUrl, sortBy, sortOrder}) => ({ variables: { assetId, assetUrl, @@ -179,7 +179,7 @@ export const withEmbedQuery = withQuery(EMBED_QUERY, { hasComment: commentId !== '', excludeIgnored: Boolean(auth && auth.user && auth.user.id), sortBy, - sort, + sortOrder, }, }), }); @@ -191,7 +191,7 @@ const mapStateToProps = (state) => ({ assetUrl: state.stream.assetUrl, activeTab: state.embed.activeTab, config: state.config, - sort: state.stream.sort, + sortOrder: state.stream.sortOrder, sortBy: state.stream.sortBy, }); diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 4720a6889..b98d7e9cf 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -102,7 +102,7 @@ class StreamContainer extends React.Component { cursor: comment.replies.endCursor, parent_id, asset_id: this.props.root.asset.id, - sort: 'ASC', + sortOrder: 'ASC', excludeIgnored: this.props.data.variables.excludeIgnored, }, updateQuery: (prev, {fetchMoreResult:{comments}}) => { @@ -119,7 +119,7 @@ class StreamContainer extends React.Component { cursor: this.props.root.asset.comments.endCursor, parent_id: null, asset_id: this.props.root.asset.id, - sort: this.props.data.variables.sort, + sortOrder: this.props.data.variables.sortOrder, sortBy: this.props.data.variables.sortBy, excludeIgnored: this.props.data.variables.excludeIgnored, }, @@ -141,24 +141,43 @@ class StreamContainer extends React.Component { clearInterval(this.countPoll); } + componentWillReceiveProps(nextProps) { + if (nextProps.sortBy === 'CREATED_AT' && nextProps.sortOrder === 'DESC') { + + // When switching to 'Newest first' we refetch and subscribe so that we always have the newest comments. + if (this.props.sortOrder !== nextProps.sortOrder || this.props.sortBy !== nextProps.sortBy) { + nextProps.data.refetch(); + this.subscribeToUpdates(); + } + } else { + + // TODO: only unsubscribe from posting comments. + // We only subscribe to new comments during 'Newest first'. + this.unsubscribe(); + } + } + userIsDegraged({auth: {user}} = this.props) { return !can(user, 'INTERACT_WITH_COMMUNITY'); } render() { - if (this.props.refetching - || !this.props.root.asset + if (!this.props.root.asset || !this.props.root.asset.comment && !this.props.root.asset.comments ) { return ; } + + const streamLoading = this.props.refetching || this.props.data.loading; + return ; } } @@ -209,7 +228,7 @@ const LOAD_MORE_QUERY = gql` $cursor: Cursor $parent_id: ID $asset_id: ID - $sort: SORT_ORDER + $sortOrder: SORT_ORDER $sortBy: SORT_COMMENTS_BY = CREATED_AT $excludeIgnored: Boolean ) { @@ -219,7 +238,7 @@ const LOAD_MORE_QUERY = gql` cursor: $cursor parent_id: $parent_id asset_id: $asset_id - sort: $sort + sort: $sortOrder sortBy: $sortBy excludeIgnored: $excludeIgnored } @@ -274,7 +293,7 @@ const fragments = { } commentCount @skip(if: $hasComment) totalCommentCount @skip(if: $hasComment) - comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sort: $sort, sortBy: $sortBy}) @skip(if: $hasComment) { + comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sort: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) { nodes { ...CoralEmbedStream_Stream_comment } @@ -317,6 +336,8 @@ const mapStateToProps = (state) => ({ previousStreamTab: state.stream.previousTab, commentClassNames: state.stream.commentClassNames, pluginConfig: state.config.plugin_config, + sortOrder: state.stream.sortOrder, + sortBy: state.stream.sortBy, }); const mapDispatchToProps = (dispatch) => diff --git a/client/coral-embed-stream/src/reducers/stream.js b/client/coral-embed-stream/src/reducers/stream.js index 02d6933ba..dd1817769 100644 --- a/client/coral-embed-stream/src/reducers/stream.js +++ b/client/coral-embed-stream/src/reducers/stream.js @@ -24,7 +24,7 @@ const initialState = { activeTab: process.env.TALK_DEFAULT_STREAM_TAB, previousTab: '', sortBy: 'CREATED_AT', - sort: 'DESC', + sortOrder: 'DESC', }; export default function stream(state = initialState, action) { @@ -68,6 +68,12 @@ export default function stream(state = initialState, action) { ...state.commentClassNames.slice(action.idx + 1) ] }; + case actions.SET_SORT : + return { + ...state, + sortOrder: action.order ? action.order : state.order, + sortBy: action.by ? action.by : state.by, + }; default: return state; } diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index c0cca143c..c4d680afb 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -6,6 +6,7 @@ import {getSlotElements, getSlotComponentProps} from 'coral-framework/helpers/pl import omit from 'lodash/omit'; import isEqual from 'lodash/isEqual'; import {getShallowChanges} from 'coral-framework/utils'; +import PropTypes from 'prop-types'; const emptyConfig = {}; @@ -25,7 +26,18 @@ class Slot extends React.Component { return changes.length !== 0; } - getSlotProps({fill: _a, inline: _b, className: _c, reduxState: _d, defaultComponent_: _e, queryData: _f, ...rest} = this.props) { + getSlotProps(props = this.props) { + const { + fill: _a, + inline: _b, + className: _c, + reduxState: _d, + defaultComponent_: _e, + queryData: _f, + childFactory: _g, + component: _h, + ...rest + } = props; return rest; } @@ -34,26 +46,60 @@ class Slot extends React.Component { } render() { - const {inline = false, className, reduxState, defaultComponent: DefaultComponent, queryData} = this.props; + const { + inline = false, + className, + reduxState, + component: Component, + childFactory, + defaultComponent: DefaultComponent, + queryData, + } = this.props; let children = this.getChildren(); const pluginConfig = reduxState.config.pluginConfig || emptyConfig; if (children.length === 0 && DefaultComponent) { children = ; } + if (childFactory) { + children = children.map(childFactory); + } + return ( -
    + {children} -
    + ); } } +Slot.defaultProps = { + component: 'div', +}; + Slot.propTypes = { - fill: React.PropTypes.string.isRequired, + fill: PropTypes.string.isRequired, + + /** + * You may specify the component to use as the root wrapper. + * Defaults to 'div'. + */ + component: PropTypes.any, // props coming from graphql must be passed through this property. - queryData: React.PropTypes.object, + queryData: PropTypes.object, + + /** + * You may need to apply reactive updates to a child as it is exiting. + * This is generally done by using `cloneElement` however in the case of an exiting + * child the element has already been removed and not accessible to the consumer. + * + * If you do need to update a child as it leaves you can provide a `childFactory` + * to wrap every child, even the ones that are leaving. + * + * @type Function(child: ReactElement) -> ReactElement + */ + childFactory: PropTypes.func, }; const mapStateToProps = (state) => ({ diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 634b6661f..26320df97 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -2,6 +2,7 @@ import {gql} from 'react-apollo'; import t from 'coral-framework/services/i18n'; import union from 'lodash/union'; import {capitalize} from 'coral-framework/helpers/strings'; +export * from 'coral-framework/helpers/strings'; export const getTotalActionCount = (type, comment) => { return comment.action_summaries diff --git a/plugin-api/beta/client/actions/stream.js b/plugin-api/beta/client/actions/stream.js new file mode 100644 index 000000000..8171357ba --- /dev/null +++ b/plugin-api/beta/client/actions/stream.js @@ -0,0 +1 @@ +export {setSort} from 'coral-embed-stream/src/actions/stream'; diff --git a/plugin-api/beta/client/components/SortOption.css b/plugin-api/beta/client/components/SortOption.css new file mode 100644 index 000000000..f2df56082 --- /dev/null +++ b/plugin-api/beta/client/components/SortOption.css @@ -0,0 +1,8 @@ +.input { + cursor: pointer; +} + +.label { + cursor: pointer; + padding-left: 4px; +} diff --git a/plugin-api/beta/client/components/SortOption.js b/plugin-api/beta/client/components/SortOption.js new file mode 100644 index 000000000..42e1c7ba5 --- /dev/null +++ b/plugin-api/beta/client/components/SortOption.js @@ -0,0 +1,15 @@ +import React from 'react'; +import styles from './SortOption.css'; + +export default class SortOption extends React.Component { + render() { + return ( +
    + + +
    + ); + } +} diff --git a/plugin-api/beta/client/components/index.js b/plugin-api/beta/client/components/index.js index 8b0448f4e..4367d3420 100644 --- a/plugin-api/beta/client/components/index.js +++ b/plugin-api/beta/client/components/index.js @@ -1,3 +1,5 @@ export {Slot} from 'coral-framework/components'; export {default as ClickOutside} from 'coral-framework/components/ClickOutside'; +export {default as IfSlotIsEmpty} from 'coral-framework/components/IfSlotIsEmpty'; +export {default as IfSlotIsNotEmpty} from 'coral-framework/components/IfSlotIsNotEmpty'; export {default as CommentAuthorName} from 'coral-framework/components/CommentAuthorName'; diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js index 60b118522..1bc47868e 100644 --- a/plugin-api/beta/client/hocs/index.js +++ b/plugin-api/beta/client/hocs/index.js @@ -1,5 +1,6 @@ export {default as withReaction} from './withReaction'; export {default as withTags} from './withTags'; +export {default as withSortOption} from './withSortOption'; export {default as withFragments} from 'coral-framework/hocs/withFragments'; export {default as excludeIf} from 'coral-framework/hocs/excludeIf'; export {default as connect} from 'coral-framework/hocs/connect'; diff --git a/plugin-api/beta/client/hocs/withSortOption.js b/plugin-api/beta/client/hocs/withSortOption.js new file mode 100644 index 000000000..16162710d --- /dev/null +++ b/plugin-api/beta/client/hocs/withSortOption.js @@ -0,0 +1,40 @@ +import React from 'react'; +import {connect} from 'plugin-api/beta/client/hocs'; +import {bindActionCreators} from 'redux'; +import {sortOrderSelector, sortBySelector} from 'plugin-api/beta/client/selectors/stream'; +import {setSort} from 'plugin-api/beta/client/actions/stream'; +import hoistStatics from 'recompose/hoistStatics'; + +const mapStateToProps = (state) => ({ + sortOrder: sortOrderSelector(state), + sortBy: sortBySelector(state), +}); + +const mapDispatchToProps = (dispatch) => + bindActionCreators( + { + setSort + }, + dispatch + ); + +export default ({by = 'created_at', order = 'DESC', label}) => hoistStatics((WrappedComponent) => { + class WithSortOption extends React.Component { + setSort = () => { + this.props.setSort({by, order}); + } + + render() { + const active = this.props.sortOrder === order && this.props.sortBy === by; + const resolvedLabel = typeof label === 'function' ? label() : label; + return ( + + ); + } + } + return connect(mapStateToProps, mapDispatchToProps)(WithSortOption); +}); diff --git a/plugin-api/beta/client/selectors/stream.js b/plugin-api/beta/client/selectors/stream.js new file mode 100644 index 000000000..6d8934bd3 --- /dev/null +++ b/plugin-api/beta/client/selectors/stream.js @@ -0,0 +1,2 @@ +export const sortOrderSelector = (state) => state.stream.sortOrder; +export const sortBySelector = (state) => state.stream.sortBy; diff --git a/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js index e0ff635e0..f652b2b66 100644 --- a/plugin-api/beta/client/utils/index.js +++ b/plugin-api/beta/client/utils/index.js @@ -3,5 +3,6 @@ export { insertCommentsSorted, getSlotFragmentSpreads, forEachError, + capitalize, getDefinitionName, } from 'coral-framework/utils'; diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js index 9a2280233..6d9781ec4 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js @@ -18,7 +18,7 @@ class TabPaneContainer extends React.Component { limit: 5, cursor: this.props.asset.featuredComments.endCursor, asset_id: this.props.asset.id, - sort: this.props.data.variables.sort, + sortOrder: this.props.data.variables.sortOrder, sortBy: this.props.data.variables.sortBy, excludeIgnored: this.props.data.variables.excludeIgnored, }, @@ -52,7 +52,7 @@ const LOAD_MORE_QUERY = gql` $limit: Int = 5 $cursor: Cursor $asset_id: ID - $sort: SORT_ORDER + $sortOrder: SORT_ORDER $sortBy: SORT_COMMENTS_BY $excludeIgnored: Boolean ) { @@ -62,7 +62,7 @@ const LOAD_MORE_QUERY = gql` cursor: $cursor tags: ["FEATURED"] asset_id: $asset_id, - sort: $sort + sort: $sortOrder sortBy: $sortBy excludeIgnored: $excludeIgnored } @@ -100,7 +100,7 @@ const enhance = compose( featuredComments: comments( query: { tags: ["FEATURED"] - sort: $sort + sort: $sortOrder sortBy: $sortBy } deep: true diff --git a/plugins/talk-plugin-offtopic/client/components/styles.css b/plugins/talk-plugin-offtopic/client/components/styles.css index c2f3e4e36..22d1f787a 100644 --- a/plugins/talk-plugin-offtopic/client/components/styles.css +++ b/plugins/talk-plugin-offtopic/client/components/styles.css @@ -19,7 +19,6 @@ } .viewingOption { - padding: 5px 0; } :global(.talk-plugin-off-topic-comment) { diff --git a/plugins/talk-plugin-offtopic/client/index.js b/plugins/talk-plugin-offtopic/client/index.js index 24bfc095f..f7156a0ec 100644 --- a/plugins/talk-plugin-offtopic/client/index.js +++ b/plugins/talk-plugin-offtopic/client/index.js @@ -6,7 +6,7 @@ import reducer from './reducer'; /** * talk-plugin-offtopic depends on talk-plugin-viewing-options - * in other to display filter and use the streamViewingOptions slot + * in other to display filter. */ export default { @@ -15,6 +15,6 @@ export default { slots: { commentInputDetailArea: [OffTopicCheckbox], commentInfoBar: [OffTopicTag], - viewingOptions: [OffTopicFilter] + viewingOptionsFilter: [OffTopicFilter] } }; diff --git a/plugins/talk-plugin-sort-oldest/client/.babelrc b/plugins/talk-plugin-sort-oldest/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-sort-oldest/client/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} \ No newline at end of file diff --git a/plugins/talk-plugin-sort-oldest/client/.eslintrc.json b/plugins/talk-plugin-sort-oldest/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-sort-oldest/client/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + } + }, + "parser": "babel-eslint", + "plugins": [ + "react" + ], + "rules": { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "no-console": ["warn", { "allow": ["warn", "error"] }] + } +} diff --git a/plugins/talk-plugin-sort-oldest/client/components/SortOption.css b/plugins/talk-plugin-sort-oldest/client/components/SortOption.css new file mode 100644 index 000000000..b8d6cacc5 --- /dev/null +++ b/plugins/talk-plugin-sort-oldest/client/components/SortOption.css @@ -0,0 +1,9 @@ + +.input { + cursor: pointer; +} + +.label { + cursor: pointer; + padding-left: 4px; +} diff --git a/plugins/talk-plugin-sort-oldest/client/components/SortOption.js b/plugins/talk-plugin-sort-oldest/client/components/SortOption.js new file mode 100644 index 000000000..42e1c7ba5 --- /dev/null +++ b/plugins/talk-plugin-sort-oldest/client/components/SortOption.js @@ -0,0 +1,15 @@ +import React from 'react'; +import styles from './SortOption.css'; + +export default class SortOption extends React.Component { + render() { + return ( +
    + + +
    + ); + } +} diff --git a/plugins/talk-plugin-sort-oldest/client/containers/SortOption.js b/plugins/talk-plugin-sort-oldest/client/containers/SortOption.js new file mode 100644 index 000000000..9c4567f18 --- /dev/null +++ b/plugins/talk-plugin-sort-oldest/client/containers/SortOption.js @@ -0,0 +1,4 @@ +import {withSortOption} from 'plugin-api/beta/client/hocs'; +import SortOption from '../components/SortOption'; + +export default withSortOption({by: 'CREATED_AT', order: 'ASC', label: 'Oldest first'})(SortOption); diff --git a/plugins/talk-plugin-sort-oldest/client/containers/SortOptionNewest.js b/plugins/talk-plugin-sort-oldest/client/containers/SortOptionNewest.js new file mode 100644 index 000000000..7ab5dcad7 --- /dev/null +++ b/plugins/talk-plugin-sort-oldest/client/containers/SortOptionNewest.js @@ -0,0 +1,4 @@ +import {withSortOption} from 'plugin-api/beta/client/hocs'; +import SortOption from '../components/SortOption'; + +export default withSortOption({by: 'CREATED_AT', order: 'DESC', label: 'Newest first'})(SortOption); diff --git a/plugins/talk-plugin-sort-oldest/client/index.js b/plugins/talk-plugin-sort-oldest/client/index.js new file mode 100644 index 000000000..5d3f08ee8 --- /dev/null +++ b/plugins/talk-plugin-sort-oldest/client/index.js @@ -0,0 +1,15 @@ +import translations from './translations.yml'; +import SortOption from './containers/SortOption'; +import SortOptionNewest from './containers/SortOptionNewest'; + +/** + * talk-plugin-sort-oldest depends on talk-plugin-viewing-options + * in other to display sort option. + */ + +export default { + translations, + slots: { + viewingOptionsSort: [SortOptionNewest, SortOption] + } +}; diff --git a/plugins/talk-plugin-sort-oldest/client/translations.yml b/plugins/talk-plugin-sort-oldest/client/translations.yml new file mode 100644 index 000000000..50645be77 --- /dev/null +++ b/plugins/talk-plugin-sort-oldest/client/translations.yml @@ -0,0 +1,4 @@ +en: + talk-plugin-sort-oldest: +es: + talk-plugin-sort-oldest: diff --git a/plugins/talk-plugin-sort-oldest/index.js b/plugins/talk-plugin-sort-oldest/index.js new file mode 100644 index 000000000..7be35b6b6 --- /dev/null +++ b/plugins/talk-plugin-sort-oldest/index.js @@ -0,0 +1,2 @@ +module.exports = { +}; diff --git a/plugins/talk-plugin-viewing-options/client/components/Category.css b/plugins/talk-plugin-viewing-options/client/components/Category.css new file mode 100644 index 000000000..758975fce --- /dev/null +++ b/plugins/talk-plugin-viewing-options/client/components/Category.css @@ -0,0 +1,15 @@ +.title { + font-weight: bold; + padding: 12px 8px 0px 8px; +} + +.list { + padding: 0; + margin: 0; +} + +.listItem { + padding: 10px; + list-style: none; + white-space: nowrap; +} diff --git a/plugins/talk-plugin-viewing-options/client/components/Category.js b/plugins/talk-plugin-viewing-options/client/components/Category.js new file mode 100644 index 000000000..0ef08b6d0 --- /dev/null +++ b/plugins/talk-plugin-viewing-options/client/components/Category.js @@ -0,0 +1,16 @@ +import React from 'react'; +import styles from './Category.css'; +import {Slot, IfSlotIsNotEmpty} from 'plugin-api/beta/client/components'; + +const childFactory = (child) =>
  • {child}
  • ; + +const ViewingOptions = ({slot, title}) => { + return ( + +
    {title}
    + +
    + ); +}; + +export default ViewingOptions; diff --git a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css index c51d87f11..c71094e33 100644 --- a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css +++ b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css @@ -9,13 +9,15 @@ cursor: pointer; } -.list { +.menu { background: white; position: absolute; box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15); right: 0px; top: 20px; z-index: 10; + min-height: 32px; + min-width: 64px; } .list > ul, .list > ul > li { diff --git a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js index 8988dd1ec..a7d6040e1 100644 --- a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js +++ b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js @@ -3,51 +3,60 @@ import cn from 'classnames'; import styles from './ViewingOptions.css'; import {t} from 'plugin-api/beta/client/services'; import {Icon} from 'plugin-api/beta/client/components/ui'; -import {Slot, ClickOutside} from 'plugin-api/beta/client/components'; +import {ClickOutside} from 'plugin-api/beta/client/components'; +import {capitalize} from 'plugin-api/beta/client/utils'; +import Category from './Category'; -const ViewingOptions = (props) => { - const toggleOpen = () => { - if (!props.open) { - props.openMenu(); +class ViewingOptions extends React.Component { + + categories = { + sort: t('talk-plugin-viewing-options.sort'), + filter: t('talk-plugin-viewing-options.filter'), + }; + + toggleOpen = () => { + const {open, openMenu, closeMenu} = this.props; + if (!open) { + openMenu(); } else { - props.closeMenu(); + closeMenu(); } }; - const handleClickOutside = () => { - if (props.open) { - props.closeMenu(); + handleClickOutside = () => { + const {open, closeMenu} = this.props; + if (open) { + closeMenu(); } }; - return ( - -
    -
    - -
    - { - props.open ? ( -
    -
      + render() { + const {open} = this.props; + return ( + +
      +
      + +
      + { + open ? ( +
      { - React.Children.map(, (component) => { - return React.createElement('li', { - className: 'talk-plugin-viewing-options-item' - }, component); - }) + Object.keys(this.categories).map((category) => + + ) } -
    -
    - ) : null - } -
    -
    - ); -}; +
    + ) : null + } +
    + + ); + } +} export default ViewingOptions; diff --git a/plugins/talk-plugin-viewing-options/client/translations.yml b/plugins/talk-plugin-viewing-options/client/translations.yml index d1b1c1896..8560b79cc 100644 --- a/plugins/talk-plugin-viewing-options/client/translations.yml +++ b/plugins/talk-plugin-viewing-options/client/translations.yml @@ -1,6 +1,8 @@ en: talk-plugin-viewing-options: viewing_options: "Viewing Options" + sort: Sort + filter: Filter es: talk-plugin-viewing-options: viewing_options: "Opciones de visualización" From e73c113e0d8250dee304478dcf6544ab1929722c Mon Sep 17 00:00:00 2001 From: Jeff Nelson Date: Fri, 25 Aug 2017 08:30:29 -0400 Subject: [PATCH 078/121] use mailgun since postmark has been deprecated --- app.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app.json b/app.json index 5faa23bc2..a2e75404b 100644 --- a/app.json +++ b/app.json @@ -14,8 +14,7 @@ "required": true }, "NODE_ENV": "production", - "TALK_SMTP_PORT": "2525", - "REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN", + "REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD", "NPM_CONFIG_PRODUCTION": "false" }, "addons": [{ @@ -25,8 +24,8 @@ "plan": "rediscloud:30", "as": "REDIS" }, { - "plan": "postmark:10k", - "as": "POSTMARK" + "plan": "mailgun:starter", + "as": "MAILGUN" }], "image": "heroku/nodejs", "success_url": "/admin/install" From 9e2f6f1f63922239f2a6fcc003a24659bc00d798 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 25 Aug 2017 19:40:55 +0700 Subject: [PATCH 079/121] Use factory --- client/coral-embed-stream/src/actions/stream.js | 2 +- client/coral-embed-stream/src/reducers/stream.js | 4 ++-- plugin-api/beta/client/factories/index.js | 4 ++++ plugin-api/beta/client/hocs/withSortOption.js | 6 +++--- .../client/components/SortOption.css | 9 --------- .../client/components/SortOption.js | 15 --------------- .../client/containers/SortOption.js | 4 ---- .../client/containers/SortOptionNewest.js | 4 ---- plugins/talk-plugin-sort-oldest/client/index.js | 14 +++++++++----- .../client/translations.yml | 1 + 10 files changed, 20 insertions(+), 43 deletions(-) create mode 100644 plugin-api/beta/client/factories/index.js delete mode 100644 plugins/talk-plugin-sort-oldest/client/components/SortOption.css delete mode 100644 plugins/talk-plugin-sort-oldest/client/components/SortOption.js delete mode 100644 plugins/talk-plugin-sort-oldest/client/containers/SortOption.js delete mode 100644 plugins/talk-plugin-sort-oldest/client/containers/SortOptionNewest.js diff --git a/client/coral-embed-stream/src/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js index d7301f8a1..5e5bede81 100644 --- a/client/coral-embed-stream/src/actions/stream.js +++ b/client/coral-embed-stream/src/actions/stream.js @@ -5,7 +5,7 @@ import queryString from 'query-string'; export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id}); -export const setSort = ({order, by}) => ({type: actions.SET_SORT, order, by}); +export const setSort = ({sortOrder, sortBy}) => ({type: actions.SET_SORT, sortOrder, sortBy}); export const viewAllComments = () => { diff --git a/client/coral-embed-stream/src/reducers/stream.js b/client/coral-embed-stream/src/reducers/stream.js index dd1817769..578c66017 100644 --- a/client/coral-embed-stream/src/reducers/stream.js +++ b/client/coral-embed-stream/src/reducers/stream.js @@ -71,8 +71,8 @@ export default function stream(state = initialState, action) { case actions.SET_SORT : return { ...state, - sortOrder: action.order ? action.order : state.order, - sortBy: action.by ? action.by : state.by, + sortOrder: action.sortOrder ? action.sortOrder : state.sortOrder, + sortBy: action.sortBy ? action.sortBy : state.sortBy, }; default: return state; diff --git a/plugin-api/beta/client/factories/index.js b/plugin-api/beta/client/factories/index.js new file mode 100644 index 000000000..485651821 --- /dev/null +++ b/plugin-api/beta/client/factories/index.js @@ -0,0 +1,4 @@ +import withSortOption from '../hocs/withSortOption'; +import SortOption from '../components/SortOption'; + +export const createSortOption = (label, sort) => withSortOption({...sort, label})(SortOption); diff --git a/plugin-api/beta/client/hocs/withSortOption.js b/plugin-api/beta/client/hocs/withSortOption.js index 16162710d..7870b668e 100644 --- a/plugin-api/beta/client/hocs/withSortOption.js +++ b/plugin-api/beta/client/hocs/withSortOption.js @@ -18,14 +18,14 @@ const mapDispatchToProps = (dispatch) => dispatch ); -export default ({by = 'created_at', order = 'DESC', label}) => hoistStatics((WrappedComponent) => { +export default ({sortBy = 'created_at', sortOrder = 'DESC', label}) => hoistStatics((WrappedComponent) => { class WithSortOption extends React.Component { setSort = () => { - this.props.setSort({by, order}); + this.props.setSort({sortBy, sortOrder}); } render() { - const active = this.props.sortOrder === order && this.props.sortBy === by; + const active = this.props.sortOrder === sortOrder && this.props.sortBy === sortBy; const resolvedLabel = typeof label === 'function' ? label() : label; return ( - - -
    - ); - } -} diff --git a/plugins/talk-plugin-sort-oldest/client/containers/SortOption.js b/plugins/talk-plugin-sort-oldest/client/containers/SortOption.js deleted file mode 100644 index 9c4567f18..000000000 --- a/plugins/talk-plugin-sort-oldest/client/containers/SortOption.js +++ /dev/null @@ -1,4 +0,0 @@ -import {withSortOption} from 'plugin-api/beta/client/hocs'; -import SortOption from '../components/SortOption'; - -export default withSortOption({by: 'CREATED_AT', order: 'ASC', label: 'Oldest first'})(SortOption); diff --git a/plugins/talk-plugin-sort-oldest/client/containers/SortOptionNewest.js b/plugins/talk-plugin-sort-oldest/client/containers/SortOptionNewest.js deleted file mode 100644 index 7ab5dcad7..000000000 --- a/plugins/talk-plugin-sort-oldest/client/containers/SortOptionNewest.js +++ /dev/null @@ -1,4 +0,0 @@ -import {withSortOption} from 'plugin-api/beta/client/hocs'; -import SortOption from '../components/SortOption'; - -export default withSortOption({by: 'CREATED_AT', order: 'DESC', label: 'Newest first'})(SortOption); diff --git a/plugins/talk-plugin-sort-oldest/client/index.js b/plugins/talk-plugin-sort-oldest/client/index.js index 5d3f08ee8..a1aac7709 100644 --- a/plugins/talk-plugin-sort-oldest/client/index.js +++ b/plugins/talk-plugin-sort-oldest/client/index.js @@ -1,15 +1,19 @@ import translations from './translations.yml'; -import SortOption from './containers/SortOption'; -import SortOptionNewest from './containers/SortOptionNewest'; +import {createSortOption} from 'plugin-api/beta/client/factories'; +import {t} from 'plugin-api/beta/client/services'; + +const SortOption = createSortOption( + () => t('talk-plugin-sort-oldest.label'), + {sortBy: 'CREATED_AT', sortOrder: 'ASC'}, +); /** - * talk-plugin-sort-oldest depends on talk-plugin-viewing-options - * in other to display sort option. + * talk-plugin-sort-oldest depends on talk-plugin-viewing-options. */ export default { translations, slots: { - viewingOptionsSort: [SortOptionNewest, SortOption] + viewingOptionsSort: [SortOption] } }; diff --git a/plugins/talk-plugin-sort-oldest/client/translations.yml b/plugins/talk-plugin-sort-oldest/client/translations.yml index 50645be77..66e59b823 100644 --- a/plugins/talk-plugin-sort-oldest/client/translations.yml +++ b/plugins/talk-plugin-sort-oldest/client/translations.yml @@ -1,4 +1,5 @@ en: talk-plugin-sort-oldest: + label: Oldest first es: talk-plugin-sort-oldest: From faecd80543171e533807cd6247c8a9cf86e96585 Mon Sep 17 00:00:00 2001 From: Jeff Nelson Date: Fri, 25 Aug 2017 08:51:36 -0400 Subject: [PATCH 080/121] better handling for heroku dynamic port --- config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.js b/config.js index 4472af26e..f86f41f30 100644 --- a/config.js +++ b/config.js @@ -80,7 +80,7 @@ const CONFIG = { //------------------------------------------------------------------------------ // Port to bind to. - PORT: process.env.TALK_PORT || '3000', + PORT: process.env.TALK_PORT || || process.env.PORT || '3000', // The URL for this Talk Instance as viewable from the outside. ROOT_URL: process.env.TALK_ROOT_URL || null, From 59ddcea59216b6072def5af350739dcf3e7a17f0 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Fri, 25 Aug 2017 13:55:19 +0100 Subject: [PATCH 081/121] Update version to 3.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 71477361e..4402a797a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "talk", - "version": "3.2.0", + "version": "3.3.0", "description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net", "main": "app.js", "scripts": { From 4b7cc9ffcce37d8dbaee01df996047b05ee9e939 Mon Sep 17 00:00:00 2001 From: Jeff Nelson Date: Fri, 25 Aug 2017 08:55:26 -0400 Subject: [PATCH 082/121] whoops --- app.json | 2 +- config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app.json b/app.json index a2e75404b..2316d1d1a 100644 --- a/app.json +++ b/app.json @@ -14,7 +14,7 @@ "required": true }, "NODE_ENV": "production", - "REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD", + "REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD", "NPM_CONFIG_PRODUCTION": "false" }, "addons": [{ diff --git a/config.js b/config.js index f86f41f30..faa911c83 100644 --- a/config.js +++ b/config.js @@ -80,7 +80,7 @@ const CONFIG = { //------------------------------------------------------------------------------ // Port to bind to. - PORT: process.env.TALK_PORT || || process.env.PORT || '3000', + PORT: process.env.TALK_PORT || process.env.PORT || '3000', // The URL for this Talk Instance as viewable from the outside. ROOT_URL: process.env.TALK_ROOT_URL || null, From e51e8c543f9243f0da463bc84d7d2b7d80d7d483 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 25 Aug 2017 20:15:07 +0700 Subject: [PATCH 083/121] Add more sort options and improve styling --- .eslintignore | 3 ++ .gitignore | 3 ++ .../beta/client/components/SortOption.css | 10 ++++-- .../beta/client/components/SortOption.js | 15 +++++---- .../client/components/OffTopicCheckbox.css | 8 +++++ .../client/components/OffTopicCheckbox.js | 2 +- .../client/components/OffTopicFilter.css | 16 ++++++++++ .../client/components/OffTopicFilter.js | 17 ++++++---- .../{styles.css => OffTopicTag.css} | 16 ---------- .../client/components/OffTopicTag.js | 2 +- .../client/.babelrc | 14 ++++++++ .../client/.eslintrc.json | 23 +++++++++++++ .../client/index.js | 19 +++++++++++ .../client/translations.yml | 5 +++ plugins/talk-plugin-sort-most-liked/index.js | 2 ++ .../client/.babelrc | 14 ++++++++ .../client/.eslintrc.json | 23 +++++++++++++ .../client/index.js | 19 +++++++++++ .../client/translations.yml | 5 +++ .../talk-plugin-sort-most-replied/index.js | 2 ++ .../talk-plugin-sort-newest/client/.babelrc | 14 ++++++++ .../client/.eslintrc.json | 23 +++++++++++++ .../talk-plugin-sort-newest/client/index.js | 19 +++++++++++ .../client/translations.yml | 5 +++ plugins/talk-plugin-sort-newest/index.js | 2 ++ .../client/components/Category.css | 11 +++++-- .../client/components/Category.js | 2 +- .../client/components/Menu.css | 11 +++++++ .../client/components/Menu.js | 31 ++++++++++++++++++ .../client/components/ViewingOptions.css | 32 +++---------------- .../client/components/ViewingOptions.js | 29 +++++------------ .../client/translations.yml | 4 +-- 32 files changed, 314 insertions(+), 87 deletions(-) create mode 100644 plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.css create mode 100644 plugins/talk-plugin-offtopic/client/components/OffTopicFilter.css rename plugins/talk-plugin-offtopic/client/components/{styles.css => OffTopicTag.css} (50%) create mode 100644 plugins/talk-plugin-sort-most-liked/client/.babelrc create mode 100644 plugins/talk-plugin-sort-most-liked/client/.eslintrc.json create mode 100644 plugins/talk-plugin-sort-most-liked/client/index.js create mode 100644 plugins/talk-plugin-sort-most-liked/client/translations.yml create mode 100644 plugins/talk-plugin-sort-most-liked/index.js create mode 100644 plugins/talk-plugin-sort-most-replied/client/.babelrc create mode 100644 plugins/talk-plugin-sort-most-replied/client/.eslintrc.json create mode 100644 plugins/talk-plugin-sort-most-replied/client/index.js create mode 100644 plugins/talk-plugin-sort-most-replied/client/translations.yml create mode 100644 plugins/talk-plugin-sort-most-replied/index.js create mode 100644 plugins/talk-plugin-sort-newest/client/.babelrc create mode 100644 plugins/talk-plugin-sort-newest/client/.eslintrc.json create mode 100644 plugins/talk-plugin-sort-newest/client/index.js create mode 100644 plugins/talk-plugin-sort-newest/client/translations.yml create mode 100644 plugins/talk-plugin-sort-newest/index.js create mode 100644 plugins/talk-plugin-viewing-options/client/components/Menu.css create mode 100644 plugins/talk-plugin-viewing-options/client/components/Menu.js diff --git a/.eslintignore b/.eslintignore index 3811f290c..f6362a2e3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,6 +13,9 @@ plugins/* !plugins/talk-plugin-comment-content !plugins/talk-plugin-permalink !plugins/talk-plugin-featured-comments +!plugins/talk-plugin-sort-newest !plugins/talk-plugin-sort-oldest +!plugins/talk-plugin-sort-most-liked +!plugins/talk-plugin-sort-most-replied node_modules diff --git a/.gitignore b/.gitignore index 3f5cbc085..c75ffc78b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ plugins/* !plugins/talk-plugin-comment-content !plugins/talk-plugin-permalink !plugins/talk-plugin-featured-comments +!plugins/talk-plugin-sort-newest !plugins/talk-plugin-sort-oldest +!plugins/talk-plugin-sort-most-liked +!plugins/talk-plugin-sort-most-replied **/node_modules/* diff --git a/plugin-api/beta/client/components/SortOption.css b/plugin-api/beta/client/components/SortOption.css index f2df56082..d1bbab6c4 100644 --- a/plugin-api/beta/client/components/SortOption.css +++ b/plugin-api/beta/client/components/SortOption.css @@ -1,8 +1,12 @@ -.input { +.label { + display: block; cursor: pointer; + width: 100%; + padding: 4px 16px 4px 10px; + box-sizing: border-box; } -.label { +.input { cursor: pointer; - padding-left: 4px; + margin-right: 6px; } diff --git a/plugin-api/beta/client/components/SortOption.js b/plugin-api/beta/client/components/SortOption.js index 42e1c7ba5..6ea0b14d1 100644 --- a/plugin-api/beta/client/components/SortOption.js +++ b/plugin-api/beta/client/components/SortOption.js @@ -4,12 +4,15 @@ import styles from './SortOption.css'; export default class SortOption extends React.Component { render() { return ( -
    - - -
    + ); } } diff --git a/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.css b/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.css new file mode 100644 index 000000000..1a6bda2a6 --- /dev/null +++ b/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.css @@ -0,0 +1,8 @@ +.offTopic { + height: 100%; +} + +.offTopicLabel { + padding: 10px 20px; + display: block; +} diff --git a/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.js b/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.js index 70876f739..7b59d87d6 100644 --- a/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.js +++ b/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.js @@ -1,5 +1,5 @@ import React from 'react'; -import styles from './styles.css'; +import styles from './OffTopicCheckbox.css'; import {t} from 'plugin-api/beta/client/services'; diff --git a/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.css b/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.css new file mode 100644 index 000000000..d31b1a4dd --- /dev/null +++ b/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.css @@ -0,0 +1,16 @@ +.label { + display: block; + cursor: pointer; + width: 100%; + padding: 4px 16px 4px 10px; + box-sizing: border-box; +} + +.input { + cursor: pointer; + margin-right: 6px; +} + +:global(.talk-plugin-off-topic-comment) { + display: none; +} diff --git a/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.js b/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.js index 426dea29a..0ed882e28 100644 --- a/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.js +++ b/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.js @@ -1,5 +1,5 @@ import React from 'react'; -import styles from './styles.css'; +import styles from './OffTopicFilter.css'; export default class OffTopicFilter extends React.Component { @@ -20,12 +20,15 @@ export default class OffTopicFilter extends React.Component { render() { return ( -
    - -
    + ); } } diff --git a/plugins/talk-plugin-offtopic/client/components/styles.css b/plugins/talk-plugin-offtopic/client/components/OffTopicTag.css similarity index 50% rename from plugins/talk-plugin-offtopic/client/components/styles.css rename to plugins/talk-plugin-offtopic/client/components/OffTopicTag.css index 22d1f787a..c1cddce8d 100644 --- a/plugins/talk-plugin-offtopic/client/components/styles.css +++ b/plugins/talk-plugin-offtopic/client/components/OffTopicTag.css @@ -1,12 +1,3 @@ -.offTopic { - height: 100%; -} - -.offTopicLabel { - padding: 10px 20px; - display: block; -} - .tag { background: #D2D7D3; font-size: 12px; @@ -17,10 +8,3 @@ padding: 5px 5px; border-radius: 2px; } - -.viewingOption { -} - -:global(.talk-plugin-off-topic-comment) { - display: none; -} diff --git a/plugins/talk-plugin-offtopic/client/components/OffTopicTag.js b/plugins/talk-plugin-offtopic/client/components/OffTopicTag.js index 0277072eb..5b8de8875 100644 --- a/plugins/talk-plugin-offtopic/client/components/OffTopicTag.js +++ b/plugins/talk-plugin-offtopic/client/components/OffTopicTag.js @@ -1,5 +1,5 @@ import React from 'react'; -import styles from './styles.css'; +import styles from './OffTopicTag.css'; import {t} from 'plugin-api/beta/client/services'; import {isTagged} from 'plugin-api/beta/client/utils'; diff --git a/plugins/talk-plugin-sort-most-liked/client/.babelrc b/plugins/talk-plugin-sort-most-liked/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-sort-most-liked/client/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} \ No newline at end of file diff --git a/plugins/talk-plugin-sort-most-liked/client/.eslintrc.json b/plugins/talk-plugin-sort-most-liked/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-sort-most-liked/client/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + } + }, + "parser": "babel-eslint", + "plugins": [ + "react" + ], + "rules": { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "no-console": ["warn", { "allow": ["warn", "error"] }] + } +} diff --git a/plugins/talk-plugin-sort-most-liked/client/index.js b/plugins/talk-plugin-sort-most-liked/client/index.js new file mode 100644 index 000000000..36faf7a88 --- /dev/null +++ b/plugins/talk-plugin-sort-most-liked/client/index.js @@ -0,0 +1,19 @@ +import translations from './translations.yml'; +import {createSortOption} from 'plugin-api/beta/client/factories'; +import {t} from 'plugin-api/beta/client/services'; + +const SortOption = createSortOption( + () => t('talk-plugin-sort-most-liked.label'), + {sortBy: 'LIKES', sortOrder: 'DESC'}, +); + +/** + * talk-plugin-sort-newest depends on talk-plugin-viewing-options. + */ + +export default { + translations, + slots: { + viewingOptionsSort: [SortOption] + } +}; diff --git a/plugins/talk-plugin-sort-most-liked/client/translations.yml b/plugins/talk-plugin-sort-most-liked/client/translations.yml new file mode 100644 index 000000000..836f96474 --- /dev/null +++ b/plugins/talk-plugin-sort-most-liked/client/translations.yml @@ -0,0 +1,5 @@ +en: + talk-plugin-sort-most-liked: + label: Most liked first +es: + talk-plugin-sort-most-liked: diff --git a/plugins/talk-plugin-sort-most-liked/index.js b/plugins/talk-plugin-sort-most-liked/index.js new file mode 100644 index 000000000..7be35b6b6 --- /dev/null +++ b/plugins/talk-plugin-sort-most-liked/index.js @@ -0,0 +1,2 @@ +module.exports = { +}; diff --git a/plugins/talk-plugin-sort-most-replied/client/.babelrc b/plugins/talk-plugin-sort-most-replied/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-sort-most-replied/client/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} \ No newline at end of file diff --git a/plugins/talk-plugin-sort-most-replied/client/.eslintrc.json b/plugins/talk-plugin-sort-most-replied/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-sort-most-replied/client/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + } + }, + "parser": "babel-eslint", + "plugins": [ + "react" + ], + "rules": { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "no-console": ["warn", { "allow": ["warn", "error"] }] + } +} diff --git a/plugins/talk-plugin-sort-most-replied/client/index.js b/plugins/talk-plugin-sort-most-replied/client/index.js new file mode 100644 index 000000000..52b837410 --- /dev/null +++ b/plugins/talk-plugin-sort-most-replied/client/index.js @@ -0,0 +1,19 @@ +import translations from './translations.yml'; +import {createSortOption} from 'plugin-api/beta/client/factories'; +import {t} from 'plugin-api/beta/client/services'; + +const SortOption = createSortOption( + () => t('talk-plugin-sort-most-replied.label'), + {sortBy: 'REPLIES', sortOrder: 'DESC'}, +); + +/** + * talk-plugin-sort-newest depends on talk-plugin-viewing-options. + */ + +export default { + translations, + slots: { + viewingOptionsSort: [SortOption] + } +}; diff --git a/plugins/talk-plugin-sort-most-replied/client/translations.yml b/plugins/talk-plugin-sort-most-replied/client/translations.yml new file mode 100644 index 000000000..b77043d6d --- /dev/null +++ b/plugins/talk-plugin-sort-most-replied/client/translations.yml @@ -0,0 +1,5 @@ +en: + talk-plugin-sort-most-replied: + label: Most replied first +es: + talk-plugin-sort-most-replied: diff --git a/plugins/talk-plugin-sort-most-replied/index.js b/plugins/talk-plugin-sort-most-replied/index.js new file mode 100644 index 000000000..7be35b6b6 --- /dev/null +++ b/plugins/talk-plugin-sort-most-replied/index.js @@ -0,0 +1,2 @@ +module.exports = { +}; diff --git a/plugins/talk-plugin-sort-newest/client/.babelrc b/plugins/talk-plugin-sort-newest/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-sort-newest/client/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} \ No newline at end of file diff --git a/plugins/talk-plugin-sort-newest/client/.eslintrc.json b/plugins/talk-plugin-sort-newest/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-sort-newest/client/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + } + }, + "parser": "babel-eslint", + "plugins": [ + "react" + ], + "rules": { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "no-console": ["warn", { "allow": ["warn", "error"] }] + } +} diff --git a/plugins/talk-plugin-sort-newest/client/index.js b/plugins/talk-plugin-sort-newest/client/index.js new file mode 100644 index 000000000..d4979e299 --- /dev/null +++ b/plugins/talk-plugin-sort-newest/client/index.js @@ -0,0 +1,19 @@ +import translations from './translations.yml'; +import {createSortOption} from 'plugin-api/beta/client/factories'; +import {t} from 'plugin-api/beta/client/services'; + +const SortOption = createSortOption( + () => t('talk-plugin-sort-newest.label'), + {sortBy: 'CREATED_AT', sortOrder: 'DESC'}, +); + +/** + * talk-plugin-sort-newest depends on talk-plugin-viewing-options. + */ + +export default { + translations, + slots: { + viewingOptionsSort: [SortOption] + } +}; diff --git a/plugins/talk-plugin-sort-newest/client/translations.yml b/plugins/talk-plugin-sort-newest/client/translations.yml new file mode 100644 index 000000000..fbaed0fe9 --- /dev/null +++ b/plugins/talk-plugin-sort-newest/client/translations.yml @@ -0,0 +1,5 @@ +en: + talk-plugin-sort-newest: + label: Newest first +es: + talk-plugin-sort-newest: diff --git a/plugins/talk-plugin-sort-newest/index.js b/plugins/talk-plugin-sort-newest/index.js new file mode 100644 index 000000000..7be35b6b6 --- /dev/null +++ b/plugins/talk-plugin-sort-newest/index.js @@ -0,0 +1,2 @@ +module.exports = { +}; diff --git a/plugins/talk-plugin-viewing-options/client/components/Category.css b/plugins/talk-plugin-viewing-options/client/components/Category.css index 758975fce..dbae31bc4 100644 --- a/plugins/talk-plugin-viewing-options/client/components/Category.css +++ b/plugins/talk-plugin-viewing-options/client/components/Category.css @@ -1,6 +1,14 @@ +.root { + padding-bottom: 12px; + + &:not(:first-child) { + border-top: 1px solid rgba(0, 0, 0, 0.1); + } +} + .title { font-weight: bold; - padding: 12px 8px 0px 8px; + padding: 12px 8px 4px 8px; } .list { @@ -9,7 +17,6 @@ } .listItem { - padding: 10px; list-style: none; white-space: nowrap; } diff --git a/plugins/talk-plugin-viewing-options/client/components/Category.js b/plugins/talk-plugin-viewing-options/client/components/Category.js index 0ef08b6d0..13beb259a 100644 --- a/plugins/talk-plugin-viewing-options/client/components/Category.js +++ b/plugins/talk-plugin-viewing-options/client/components/Category.js @@ -6,7 +6,7 @@ const childFactory = (child) =>
  • const ViewingOptions = ({slot, title}) => { return ( - +
    {title}
    diff --git a/plugins/talk-plugin-viewing-options/client/components/Menu.css b/plugins/talk-plugin-viewing-options/client/components/Menu.css new file mode 100644 index 000000000..f1f3903a1 --- /dev/null +++ b/plugins/talk-plugin-viewing-options/client/components/Menu.css @@ -0,0 +1,11 @@ +.menu { + background: white; + position: absolute; + box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15); + right: 0px; + top: 20px; + z-index: 10; + min-height: 32px; + min-width: 64px; +} + diff --git a/plugins/talk-plugin-viewing-options/client/components/Menu.js b/plugins/talk-plugin-viewing-options/client/components/Menu.js new file mode 100644 index 000000000..68afe4b3b --- /dev/null +++ b/plugins/talk-plugin-viewing-options/client/components/Menu.js @@ -0,0 +1,31 @@ +import React from 'react'; +import cn from 'classnames'; +import styles from './Menu.css'; +import {capitalize} from 'plugin-api/beta/client/utils'; +import Category from './Category'; +import {t} from 'plugin-api/beta/client/services'; + +class Menu extends React.Component { + categories = { + sort: t('talk-plugin-viewing-options.sort'), + filter: t('talk-plugin-viewing-options.filter'), + }; + + render() { + return ( +
    + { + Object.keys(this.categories).map((category) => + + ) + } +
    + ); + } +} + +export default Menu; diff --git a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css index c71094e33..4de6f15f7 100644 --- a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css +++ b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css @@ -9,29 +9,7 @@ cursor: pointer; } -.menu { - background: white; - position: absolute; - box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15); - right: 0px; - top: 20px; - z-index: 10; - min-height: 32px; - min-width: 64px; -} - -.list > ul, .list > ul > li { - padding: 0; - margin: 0; -} - -.list > ul > li { - padding: 10px; - list-style: none; - white-space: nowrap; -} - -.icon { +.arrowIcon { font-size: 14px; vertical-align: middle; } @@ -39,20 +17,20 @@ @custom-media --small-viewport (max-width: 425px); -.filterText { +.label { display: inline-block; } -.filterIcon { +.icon { vertical-align: middle; display: none; } @media (--small-viewport) { - .filterText { + .label { display: none; } - .filterIcon { + .icon { display: inline-block; } } diff --git a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js index a7d6040e1..0d2faf2a4 100644 --- a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js +++ b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js @@ -4,16 +4,10 @@ import styles from './ViewingOptions.css'; import {t} from 'plugin-api/beta/client/services'; import {Icon} from 'plugin-api/beta/client/components/ui'; import {ClickOutside} from 'plugin-api/beta/client/components'; -import {capitalize} from 'plugin-api/beta/client/utils'; -import Category from './Category'; +import Menu from './Menu'; class ViewingOptions extends React.Component { - categories = { - sort: t('talk-plugin-viewing-options.sort'), - filter: t('talk-plugin-viewing-options.filter'), - }; - toggleOpen = () => { const {open, openMenu, closeMenu} = this.props; if (!open) { @@ -37,22 +31,15 @@ class ViewingOptions extends React.Component {
    - { - open ? ( -
    - { - Object.keys(this.categories).map((category) => - - ) - } -
    - ) : null - } + {open && }
    ); diff --git a/plugins/talk-plugin-viewing-options/client/translations.yml b/plugins/talk-plugin-viewing-options/client/translations.yml index 8560b79cc..c06bffb9e 100644 --- a/plugins/talk-plugin-viewing-options/client/translations.yml +++ b/plugins/talk-plugin-viewing-options/client/translations.yml @@ -1,8 +1,8 @@ en: talk-plugin-viewing-options: viewing_options: "Viewing Options" - sort: Sort - filter: Filter + sort: Sorting + filter: Filtering es: talk-plugin-viewing-options: viewing_options: "Opciones de visualización" From 2f3d7c5329802af6d4d2c53eea2711eaf7645ed7 Mon Sep 17 00:00:00 2001 From: Jeff Nelson Date: Fri, 25 Aug 2017 09:17:51 -0400 Subject: [PATCH 084/121] set default root url --- app.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app.json b/app.json index 2316d1d1a..3c17023c4 100644 --- a/app.json +++ b/app.json @@ -13,6 +13,10 @@ "value": "", "required": true }, + "TALK_ROOT_URL": { + "value": "http://0.0.0.0", + "required": true + }, "NODE_ENV": "production", "REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD", "NPM_CONFIG_PRODUCTION": "false" From 28dafd3e07f73ed4029b6d20bca91d428cd2c9d8 Mon Sep 17 00:00:00 2001 From: Jeff Nelson Date: Fri, 25 Aug 2017 09:27:52 -0400 Subject: [PATCH 085/121] update --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.json b/app.json index 3c17023c4..aefe25861 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,7 @@ { "name": "The Coral Project: Talk", "env": { - "TALK_SESSION_SECRET": { + "TALK_JWT_SECRET": { "description": "The session secret", "generator": "secret" }, From 50caddee56d7d9c47368582c1a502add7aad9c27 Mon Sep 17 00:00:00 2001 From: Jeff Nelson Date: Fri, 25 Aug 2017 09:47:40 -0400 Subject: [PATCH 086/121] update --- app.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app.json b/app.json index aefe25861..b0db1dcde 100644 --- a/app.json +++ b/app.json @@ -5,6 +5,11 @@ "description": "The session secret", "generator": "secret" }, + "TALK_ROOT_URL": { + "description": "Please copy the App Name you choose above. If you did not choose one, please do so now and copy it here. Talk on Heroku will not work without this setting.", + "value":"https://.herokuapp.com", + "required": true + }, "TALK_FACEBOOK_APP_ID": { "value": "", "required": true @@ -13,10 +18,6 @@ "value": "", "required": true }, - "TALK_ROOT_URL": { - "value": "http://0.0.0.0", - "required": true - }, "NODE_ENV": "production", "REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD", "NPM_CONFIG_PRODUCTION": "false" From cc66f1b45953c3bff17dd7de5e1d3f9117206cc4 Mon Sep 17 00:00:00 2001 From: Jeff Nelson Date: Fri, 25 Aug 2017 09:47:59 -0400 Subject: [PATCH 087/121] update --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.json b/app.json index b0db1dcde..6c85e3cc2 100644 --- a/app.json +++ b/app.json @@ -7,7 +7,7 @@ }, "TALK_ROOT_URL": { "description": "Please copy the App Name you choose above. If you did not choose one, please do so now and copy it here. Talk on Heroku will not work without this setting.", - "value":"https://.herokuapp.com", + "value":"https://.herokuapp.com", "required": true }, "TALK_FACEBOOK_APP_ID": { From 091d787e349ae32002b6d2d3d9dbc0a838c57019 Mon Sep 17 00:00:00 2001 From: Jeff Nelson Date: Fri, 25 Aug 2017 10:02:16 -0400 Subject: [PATCH 088/121] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a58ac7c74..d1bcf2956 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/coralproject/talk/tree/feature/heroku-deploy&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore) Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html) From fe0c32898d2de2814213845ac30d82d566c65a26 Mon Sep 17 00:00:00 2001 From: Jeff Nelson Date: Fri, 25 Aug 2017 10:03:46 -0400 Subject: [PATCH 089/121] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d1bcf2956..4b12315e4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/coralproject/talk/tree/feature/heroku-deploy&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore) +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk%2Ftree%2Ffeature%2Fheroku-deploy&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore) Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html) From 6b96fcfad1e9a27d750a4ec61277c8e1c1e7445f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 25 Aug 2017 21:05:30 +0700 Subject: [PATCH 090/121] Only unsubscribe from newest comments --- .../src/containers/Stream.js | 94 +++++++++++++------ 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index b98d7e9cf..321511496 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -29,11 +29,15 @@ const {showSignInDialog, editName} = authActions; const {addNotification} = notificationActions; class StreamContainer extends React.Component { - subscriptions = []; + commentsAddedSubscription = null; + commentsEditedSubscription = null; - subscribeToUpdates() { - const newSubscriptions = [{ + subscribeToCommentsEdited() { + this.commentsEditedSubscription = this.props.data.subscribeToMore({ document: COMMENTS_EDITED_SUBSCRIPTION, + variables: { + assetId: this.props.root.asset.id, + }, updateQuery: (prev, {subscriptionData: {data: {commentEdited}}}) => { // Ignore mutations from me. @@ -51,9 +55,15 @@ class StreamContainer extends React.Component { return removeCommentFromEmbedQuery(prev, commentEdited.id); } }, - }, - { + }); + } + + subscribeToCommentsAdded() { + this.commentsAddedSubscription = this.props.data.subscribeToMore({ document: COMMENTS_ADDED_SUBSCRIPTION, + variables: { + assetId: this.props.root.asset.id, + }, updateQuery: (prev, {subscriptionData: {data: {commentAdded}}}) => { // Ignore mutations from me. @@ -75,21 +85,22 @@ class StreamContainer extends React.Component { } return insertCommentIntoEmbedQuery(prev, commentAdded); - } - }]; - - this.subscriptions = newSubscriptions.map((s) => this.props.data.subscribeToMore({ - document: s.document, - variables: { - assetId: this.props.root.asset.id, }, - updateQuery: s.updateQuery, - })); + }); } - unsubscribe() { - this.subscriptions.forEach((unsubscribe) => unsubscribe()); - this.subscriptions = []; + unsubscribeCommentsAdded() { + if (this.commentsAddedSubscription) { + this.commentsAddedSubscription(); + this.commentsAddedSubscription = null; + } + } + + unsubscribeCommentsEdited() { + if (this.commentsEditedSubscription) { + this.commentsEditedSubscription(); + this.commentsEditedSubscription = null; + } } loadNewReplies = (parent_id) => { @@ -129,32 +140,55 @@ class StreamContainer extends React.Component { }); }; + isSortedByNewestFirst({sortBy, sortOrder} = this.props) { + return sortBy === 'CREATED_AT' && sortOrder === 'DESC'; + } + componentDidMount() { if (this.props.previousTab) { this.props.data.refetch(); } - this.subscribeToUpdates(); + + if (this.isSortedByNewestFirst()) { + this.subscribeToCommentsAdded(); + } + + this.subscribeToCommentsEdited(); } componentWillUnmount() { - this.unsubscribe(); + this.unsubscribeCommentsAdded(); + this.unsubscribeCommentsEdited(); clearInterval(this.countPoll); } componentWillReceiveProps(nextProps) { - if (nextProps.sortBy === 'CREATED_AT' && nextProps.sortOrder === 'DESC') { + const prevSortedNewest = this.isSortedByNewestFirst(this.props); + const nextSortedNewest = this.isSortedByNewestFirst(nextProps); - // When switching to 'Newest first' we refetch and subscribe so that we always have the newest comments. - if (this.props.sortOrder !== nextProps.sortOrder || this.props.sortBy !== nextProps.sortBy) { - nextProps.data.refetch(); - this.subscribeToUpdates(); - } - } else { - - // TODO: only unsubscribe from posting comments. - // We only subscribe to new comments during 'Newest first'. - this.unsubscribe(); + // When switching to 'Newest first' we refetch and subscribe so that + // we always have the newest comments. + if (!prevSortedNewest && nextSortedNewest) { + nextProps.data.refetch(); + this.subscribeToCommentsAdded(); } + + // When switching away from 'Newest first' unsubscribe from newest comments. + if (prevSortedNewest && !nextSortedNewest) { + this.unsubscribeCommentsAdded(); + } + } + + shouldComponentUpdate(nextProps) { + const prevSortedNewest = this.isSortedByNewestFirst(this.props); + const nextSortedNewest = this.isSortedByNewestFirst(nextProps); + if (!prevSortedNewest && nextSortedNewest) { + + // When switching to 'Newest first' we refetch => skip + // rendering this frame and wait for refetch to kick in. + return false; + } + return true; } userIsDegraged({auth: {user}} = this.props) { From 42951aaf0bfee901999251bcac9af02b90c562e7 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 25 Aug 2017 21:17:15 +0700 Subject: [PATCH 091/121] Adding proptypes --- plugin-api/beta/client/components/SortOption.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugin-api/beta/client/components/SortOption.js b/plugin-api/beta/client/components/SortOption.js index 6ea0b14d1..31aa509c0 100644 --- a/plugin-api/beta/client/components/SortOption.js +++ b/plugin-api/beta/client/components/SortOption.js @@ -1,5 +1,6 @@ import React from 'react'; import styles from './SortOption.css'; +import PropTypes from 'prop-types'; export default class SortOption extends React.Component { render() { @@ -16,3 +17,9 @@ export default class SortOption extends React.Component { ); } } + +SortOption.propTypes = { + setSort: PropTypes.func.isRequired, + active: PropTypes.bool.isRequired, + label: PropTypes.string.isRequired, +}; From a767ab73dadd9b40489cecef3a0f017f7827623f Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Fri, 25 Aug 2017 15:20:05 +0100 Subject: [PATCH 092/121] Update default plugins to include new sorts --- plugins.default.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins.default.json b/plugins.default.json index 05da159b7..a4ba0b525 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -4,7 +4,11 @@ "talk-plugin-respect", "talk-plugin-offtopic", "talk-plugin-facebook-auth", - "talk-plugin-featured-comments" + "talk-plugin-featured-comments", + "talk-plugin-sort-newest", + "talk-plugin-sort-oldest", + "talk-plugin-sort-most-liked", + "talk-plugin-sort-most-replied" ], "client": [ "talk-plugin-respect", @@ -13,6 +17,10 @@ "talk-plugin-viewing-options", "talk-plugin-comment-content", "talk-plugin-permalink", - "talk-plugin-featured-comments" + "talk-plugin-featured-comments", + "talk-plugin-sort-newest", + "talk-plugin-sort-oldest", + "talk-plugin-sort-most-liked", + "talk-plugin-sort-most-replied" ] } From e3227322a53d90bb5f1f371ed0a4fe31e7bc7fe0 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 25 Aug 2017 21:22:48 +0700 Subject: [PATCH 093/121] Some docs --- plugin-api/beta/client/components/SortOption.js | 6 ++++++ plugin-api/beta/client/factories/index.js | 8 ++++++++ plugin-api/beta/client/hocs/withSortOption.js | 8 ++++++++ 3 files changed, 22 insertions(+) diff --git a/plugin-api/beta/client/components/SortOption.js b/plugin-api/beta/client/components/SortOption.js index 31aa509c0..7c8b4d761 100644 --- a/plugin-api/beta/client/components/SortOption.js +++ b/plugin-api/beta/client/components/SortOption.js @@ -19,7 +19,13 @@ export default class SortOption extends React.Component { } SortOption.propTypes = { + + // A simple callback to be called when clicking on this sort option. setSort: PropTypes.func.isRequired, + + // Whether or not this sort option is active. active: PropTypes.bool.isRequired, + + // Label to show next to the input control. label: PropTypes.string.isRequired, }; diff --git a/plugin-api/beta/client/factories/index.js b/plugin-api/beta/client/factories/index.js index 485651821..d413258c6 100644 --- a/plugin-api/beta/client/factories/index.js +++ b/plugin-api/beta/client/factories/index.js @@ -1,4 +1,12 @@ import withSortOption from '../hocs/withSortOption'; import SortOption from '../components/SortOption'; +/** + * A factory creating a sort option component. + * @param {string|function} label label to display, can be a callback for lazy evaluation. + * @param {Object} sort sort parameters + * @param {string} sort.sortBy + * @param {string} sort.sortOrder + * @return {Object} Component + */ export const createSortOption = (label, sort) => withSortOption({...sort, label})(SortOption); diff --git a/plugin-api/beta/client/hocs/withSortOption.js b/plugin-api/beta/client/hocs/withSortOption.js index 7870b668e..076d921e7 100644 --- a/plugin-api/beta/client/hocs/withSortOption.js +++ b/plugin-api/beta/client/hocs/withSortOption.js @@ -18,6 +18,14 @@ const mapDispatchToProps = (dispatch) => dispatch ); +/** + * A HOC providing props to implement a sort option. + * Provides the props `active`, `setSort`, `label`. + * @param {Object} sort + * @param {Object} sort.sortBy + * @param {string} sort.sortOrder + * @return {Object} HOC + */ export default ({sortBy = 'created_at', sortOrder = 'DESC', label}) => hoistStatics((WrappedComponent) => { class WithSortOption extends React.Component { setSort = () => { From 7f899b04404542bd62629a77299ad673acc916d6 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 25 Aug 2017 21:40:57 +0700 Subject: [PATCH 094/121] Attach to fragments tree --- .../src/containers/Stream.js | 1 + .../client/components/Category.js | 11 +++++-- .../client/components/ViewingOptions.js | 4 +-- .../client/containers/ViewingOptions.js | 29 +++++++++++++++++-- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 321511496..61abc3764 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -291,6 +291,7 @@ const LOAD_MORE_QUERY = gql` const slots = [ 'streamTabs', 'streamTabPanes', + 'streamFilter', ]; const fragments = { diff --git a/plugins/talk-plugin-viewing-options/client/components/Category.js b/plugins/talk-plugin-viewing-options/client/components/Category.js index 13beb259a..184b5cadc 100644 --- a/plugins/talk-plugin-viewing-options/client/components/Category.js +++ b/plugins/talk-plugin-viewing-options/client/components/Category.js @@ -4,11 +4,18 @@ import {Slot, IfSlotIsNotEmpty} from 'plugin-api/beta/client/components'; const childFactory = (child) =>
  • {child}
  • ; -const ViewingOptions = ({slot, title}) => { +const ViewingOptions = ({slot, title, data, asset, root}) => { return (
    {title}
    - +
    ); }; diff --git a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js index 0d2faf2a4..30997d5a2 100644 --- a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js +++ b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js @@ -25,7 +25,7 @@ class ViewingOptions extends React.Component { }; render() { - const {open} = this.props; + const {open, data, root, asset} = this.props; return (
    @@ -39,7 +39,7 @@ class ViewingOptions extends React.Component { }
    - {open && } + {open && }
    ); diff --git a/plugins/talk-plugin-viewing-options/client/containers/ViewingOptions.js b/plugins/talk-plugin-viewing-options/client/containers/ViewingOptions.js index 22608828e..8749d0137 100644 --- a/plugins/talk-plugin-viewing-options/client/containers/ViewingOptions.js +++ b/plugins/talk-plugin-viewing-options/client/containers/ViewingOptions.js @@ -1,7 +1,14 @@ -import {connect} from 'plugin-api/beta/client/hocs'; +import {connect, withFragments} from 'plugin-api/beta/client/hocs'; import {bindActionCreators} from 'redux'; import ViewingOptions from '../components/ViewingOptions'; import {openMenu, closeMenu} from '../actions'; +import {compose, gql} from 'react-apollo'; +import {getSlotFragmentSpreads} from 'plugin-api/beta/client/utils'; + +const slots = [ + 'viewingOptionsSort', + 'viewingOptionsFilter', +]; const mapStateToProps = ({talkPluginViewingOptions: state}) => ({ open: state.open @@ -10,4 +17,22 @@ const mapStateToProps = ({talkPluginViewingOptions: state}) => ({ const mapDispatchToProps = (dispatch) => bindActionCreators({openMenu, closeMenu}, dispatch); -export default connect(mapStateToProps, mapDispatchToProps)(ViewingOptions); +const withViewingOptionsFragments = withFragments({ + root: gql` + fragment TalkViewingOptions_ViewingOptions_root on RootQuery { + __typename + ${getSlotFragmentSpreads(slots, 'root')} + }`, + asset: gql` + fragment TalkViewingOptions_ViewingOptions_asset on Asset { + __typename + ${getSlotFragmentSpreads(slots, 'asset')} + }`, +}); + +const enhance = compose( + connect(mapStateToProps, mapDispatchToProps), + withViewingOptionsFragments, +); + +export default enhance(ViewingOptions); From 8a5db167ff1adfe673d184b59b192c0f0e43df69 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 25 Aug 2017 09:59:37 -0600 Subject: [PATCH 095/121] changed sort -> sortOrder --- .../routes/Dashboard/containers/Dashboard.js | 4 ++-- .../Moderation/containers/Moderation.js | 14 +++++------ .../src/routes/Moderation/graphql.js | 18 +++++++------- .../src/containers/Stream.js | 4 ++-- graph/loaders/comments.js | 22 ++++++++--------- graph/loaders/metrics.js | 24 +++++++++---------- graph/loaders/users.js | 6 ++--- graph/resolvers/comment.js | 14 ++++------- graph/resolvers/comment_status_history.js | 2 +- graph/resolvers/root_query.js | 14 +++++------ graph/typeDefs.graphql | 10 ++++---- plugin-api/beta/server/getReactionConfig.js | 4 ++-- .../client/containers/TabPane.js | 4 ++-- test/server/graph/loaders/metrics.js | 14 +++++------ 14 files changed, 75 insertions(+), 79 deletions(-) diff --git a/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js b/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js index 74faedcf7..fe3680c76 100644 --- a/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js +++ b/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js @@ -22,10 +22,10 @@ class DashboardContainer extends React.Component { export const witDashboardQuery = withQuery(gql` query CoralAdmin_Dashboard($from: Date!, $to: Date!) { - assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) { + assetsByFlag: assetMetrics(from: $from, to: $to, sortBy: FLAG) { ...CoralAdmin_Metrics } - assetsByActivity: assetMetrics(from: $from, to: $to, sort: ACTIVITY) { + assetsByActivity: assetMetrics(from: $from, to: $to, sortBy: ACTIVITY) { ...CoralAdmin_Metrics } } diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index 81e87653f..722faaaa3 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -59,7 +59,7 @@ class ModerationContainer extends Component { return handleCommentChange( root, comment, - this.props.data.variables.sort, + this.props.data.variables.sortOrder, () => notifyText && this.props.notify('info', notifyText), this.props.queueConfig, this.activeTab @@ -168,7 +168,7 @@ class ModerationContainer extends Component { const variables = { limit: 10, cursor: this.props.root[tab].endCursor, - sort: this.props.data.variables.sort, + sortOrder: this.props.data.variables.sortOrder, asset_id: this.props.data.variables.asset_id, statuses: this.props.queueConfig[tab].statuses, action_type: this.props.queueConfig[tab].action_type, @@ -288,8 +288,8 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql` `; const LOAD_MORE_QUERY = gql` - query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) { - comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) { + query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) { + comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type}) { nodes { ...${getDefinitionName(Comment.fragments.comment)} } @@ -314,14 +314,14 @@ const commentConnectionFragment = gql` `; const withModQueueQuery = withQuery(({queueConfig}) => gql` - query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER, $allAssets: Boolean!) { + query CoralAdmin_Moderation($asset_id: ID, $sortOrder: SORT_ORDER, $allAssets: Boolean!) { ${Object.keys(queueConfig).map((queue) => ` ${queue}: comments(query: { ${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''} ${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''} ${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''} asset_id: $asset_id, - sort: $sort + sortOrder: $sortOrder }) { ...CoralAdmin_Moderation_CommentConnection } @@ -354,7 +354,7 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql` return { variables: { asset_id: id, - sort: props.moderation.sortOrder, + sortOrder: props.moderation.sortOrder, allAssets: id === null } }; diff --git a/client/coral-admin/src/routes/Moderation/graphql.js b/client/coral-admin/src/routes/Moderation/graphql.js index c9adb57e2..28ca259d8 100644 --- a/client/coral-admin/src/routes/Moderation/graphql.js +++ b/client/coral-admin/src/routes/Moderation/graphql.js @@ -28,29 +28,29 @@ function removeCommentFromQueue(root, queue, id) { }); } -function shouldCommentBeAdded(root, queue, comment, sort) { +function shouldCommentBeAdded(root, queue, comment, sortOrder) { if (root[`${queue}Count`] < limit) { // Adding all comments until first limit has reached. return true; } const cursor = new Date(root[queue].endCursor); - return sort === 'ASC' + return sortOrder === 'ASC' ? new Date(comment.created_at) <= cursor : new Date(comment.created_at) >= cursor; } -function addCommentToQueue(root, queue, comment, sort) { +function addCommentToQueue(root, queue, comment, sortOrder) { if (queueHasComment(root, queue, comment.id)) { return root; } - const sortAlgo = sort === 'ASC' ? ascending : descending; + const sortAlgo = sortOrder === 'ASC' ? ascending : descending; const changes = { [`${queue}Count`]: {$set: root[`${queue}Count`] + 1}, }; - if (shouldCommentBeAdded(root, queue, comment, sort)) { + if (shouldCommentBeAdded(root, queue, comment, sortOrder)) { const nodes = root[queue].nodes.concat(comment).sort(sortAlgo); changes[queue] = { nodes: {$set: nodes}, @@ -90,13 +90,13 @@ function getCommentQueues(comment, queueConfig) { * Assimilate comment changes into current store. * @param {Object} root current state of the store * @param {Object} comment comment that was changed - * @param {string} sort current sort order of the queues + * @param {string} sortOrder current sort order of the queues * @param {string} notify callback to show notification * in the current active queue besides the 'all' queue. * @param {Object} queueConfig queue configuration * @return {Object} next state of the store */ -export function handleCommentChange(root, comment, sort, notify, queueConfig, activeQueue) { +export function handleCommentChange(root, comment, sortOrder, notify, queueConfig, activeQueue) { let next = root; const nextQueues = getCommentQueues(comment, queueConfig); @@ -113,8 +113,8 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac Object.keys(queueConfig).forEach((queue) => { if (nextQueues.indexOf(queue) >= 0) { if (!queueHasComment(next, queue, comment.id)) { - next = addCommentToQueue(next, queue, comment, sort); - if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sort)) { + next = addCommentToQueue(next, queue, comment, sortOrder); + if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sortOrder)) { showNotificationOnce(); } } diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 5d5ed5cde..41f450436 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -272,7 +272,7 @@ const LOAD_MORE_QUERY = gql` cursor: $cursor parent_id: $parent_id asset_id: $asset_id - sort: $sortOrder + sortOrder: $sortOrder sortBy: $sortBy excludeIgnored: $excludeIgnored } @@ -328,7 +328,7 @@ const fragments = { } commentCount @skip(if: $hasComment) totalCommentCount @skip(if: $hasComment) - comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sort: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) { + comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) { nodes { ...CoralEmbedStream_Stream_comment } diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index c71777b6b..cafd647af 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -189,11 +189,11 @@ const getEndCursor = (ctx, nodes, {cursor, sortBy}) => { * @param {Object} query the current mongoose query object * @param {Object} params the params from the client describing the query */ -const applySort = (ctx, query, {cursor, sort, sortBy}) => { +const applySort = (ctx, query, {cursor, sortOrder, sortBy}) => { switch (sortBy) { case 'CREATED_AT': { if (cursor) { - if (sort === 'DESC') { + if (sortOrder === 'DESC') { query = query.where({ created_at: { $lt: cursor, @@ -208,14 +208,14 @@ const applySort = (ctx, query, {cursor, sort, sortBy}) => { } } - return query.sort({created_at: sort === 'DESC' ? -1 : 1}); + return query.sort({created_at: sortOrder === 'DESC' ? -1 : 1}); } case 'REPLIES': { if (cursor) { query = query.skip(cursor); } - return query.sort({reply_count: sort === 'DESC' ? -1 : 1, created_at: sort === 'DESC' ? -1 : 1}); + return query.sort({reply_count: sortOrder === 'DESC' ? -1 : 1, created_at: sortOrder === 'DESC' ? -1 : 1}); } } @@ -224,7 +224,7 @@ const applySort = (ctx, query, {cursor, sort, sortBy}) => { throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`); } - return ctx.plugins.Sort.Comments[SORT_KEY].sort(ctx, query, {cursor, sort}); + return ctx.plugins.Sort.Comments[SORT_KEY].sort(ctx, query, {cursor, sortOrder}); }; /** @@ -236,10 +236,10 @@ const applySort = (ctx, query, {cursor, sort, sortBy}) => { * @param {Object} query the current mongoose query object * @param {Object} params the params from the client describing the query */ -const executeWithSort = async (ctx, query, {cursor, sort, sortBy, limit}) => { +const executeWithSort = async (ctx, query, {cursor, sortOrder, sortBy, limit}) => { // Apply the sort to the query. - query = applySort(ctx, query, {cursor, sort, sortBy}); + query = applySort(ctx, query, {cursor, sortOrder, sortBy}); // Apply the limit (if it exists, as it's applied universally). if (limit) { @@ -263,8 +263,8 @@ const executeWithSort = async (ctx, query, {cursor, sort, sortBy, limit}) => { // Use the generator functions below to extract the cursor details based on // the current sortBy parameter. return { - startCursor: getStartCursor(ctx, nodes, {cursor, sort, sortBy, limit}), - endCursor: getEndCursor(ctx, nodes, {cursor, sort, sortBy, limit}), + startCursor: getStartCursor(ctx, nodes, {cursor, sortOrder, sortBy, limit}), + endCursor: getEndCursor(ctx, nodes, {cursor, sortOrder, sortBy, limit}), hasNextPage, nodes, }; @@ -277,7 +277,7 @@ const executeWithSort = async (ctx, query, {cursor, sort, sortBy, limit}) => { * @param {Object} context graph context * @param {Object} query query terms to apply to the comments query */ -const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, sortBy, excludeIgnored, tags, action_type}) => { +const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sortOrder, sortBy, excludeIgnored, tags, action_type}) => { let comments = CommentModel.find(); // Only administrators can search for comments with statuses that are not @@ -343,7 +343,7 @@ const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, auth }); } - return executeWithSort(ctx, comments, {cursor, sort, sortBy, limit}); + return executeWithSort(ctx, comments, {cursor, sortOrder, sortBy, limit}); }; /** diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js index e91a1adf4..685f875e3 100644 --- a/graph/loaders/metrics.js +++ b/graph/loaders/metrics.js @@ -54,13 +54,13 @@ const getAssetActivityMetrics = ({loaders: {Assets}}, {from, to, limit}) => { /** * Returns a list of assets with action metadata included on the models. */ -const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to, sort, limit}) => { +const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to, sortBy, limit}) => { // Get the recent actions. let actionSummaries = await Metrics.getRecentActions.load({from, to}); let commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => { - if (action_type !== sort) { + if (action_type !== sortBy) { return acc; } @@ -94,9 +94,9 @@ const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to return {action_summaries, id: asset_id}; }) - + .filter((asset) => { - let contextActionSummary = asset.action_summaries.find((({action_type}) => action_type === sort)); + let contextActionSummary = asset.action_summaries.find((({action_type}) => action_type === sortBy)); if (contextActionSummary === null || contextActionSummary.actionCount === 0) { return false; } @@ -108,8 +108,8 @@ const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to // if the action summary does not exist on the object, that it is less // prefered over the one that does have it. .sort((a, b) => { - let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort)); - let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort)); + let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sortBy)); + let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sortBy)); // Both of them had an actionCount, hence we can determine that we could // compare the actual values directly. @@ -144,15 +144,15 @@ const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to * Returns a list of comments that are retrieved based on most activity within * the indicated time range. */ -const getCommentMetrics = async ({loaders: {Metrics, Comments}}, {from, to, sort, limit}) => { +const getCommentMetrics = async ({loaders: {Metrics, Comments}}, {from, to, sortBy, limit}) => { let commentActionSummaries = {}; let actionSummaries = await Metrics.getRecentActions.load({from, to}); actionSummaries.sort((a, b) => { - let aActionSummary = a.action_type === sort ? a : null; - let bActionSummary = b.action_type === sort ? b : null; + let aActionSummary = a.action_type === sortBy ? a : null; + let bActionSummary = b.action_type === sortBy ? b : null; // If either a or b don't have this action type, then one of them will // automatically win. @@ -178,7 +178,7 @@ const getCommentMetrics = async ({loaders: {Metrics, Comments}}, {from, to, sort // Grab the comment id's for comment where they have at least one of the // actions being sorted by. let commentIDs = Object.keys(commentActionSummaries).filter((item_id) => { - let contextActionSummary = commentActionSummaries[item_id].find(({action_type}) => action_type === sort); + let contextActionSummary = commentActionSummaries[item_id].find(({action_type}) => action_type === sortBy); if (contextActionSummary == null) { return false; } @@ -247,11 +247,11 @@ module.exports = (context) => ({ cacheKeyFn: objectCacheKeyFn('from', 'to') }), Assets: { - get: ({from, to, sort, limit}) => getAssetMetrics(context, {from, to, sort, limit}), + get: ({from, to, sortBy, limit}) => getAssetMetrics(context, {from, to, sortBy, limit}), getActivity: ({from, to, limit}) => getAssetActivityMetrics(context, {from, to, limit}), }, Comments: { - get: ({from, to, sort, limit}) => getCommentMetrics(context, {from, to, sort, limit}), + get: ({from, to, sortBy, limit}) => getCommentMetrics(context, {from, to, sortBy, limit}), } } }); diff --git a/graph/loaders/users.js b/graph/loaders/users.js index fe7a6285d..f11effbd1 100644 --- a/graph/loaders/users.js +++ b/graph/loaders/users.js @@ -15,7 +15,7 @@ const genUserByIDs = (context, ids) => UsersService * @param {Object} context graph context * @param {Object} query query terms to apply to the users query */ -const getUsersByQuery = ({user}, {ids, limit, cursor, statuses = null, sort}) => { +const getUsersByQuery = ({user}, {ids, limit, cursor, statuses = null, sortOrder}) => { let users = UserModel.find(); @@ -36,7 +36,7 @@ const getUsersByQuery = ({user}, {ids, limit, cursor, statuses = null, sort}) => } if (cursor) { - if (sort === 'DESC') { + if (sortOrder === 'DESC') { users = users.where({ created_at: { $lt: cursor @@ -52,7 +52,7 @@ const getUsersByQuery = ({user}, {ids, limit, cursor, statuses = null, sort}) => } return users - .sort({created_at: sort === 'DESC' ? -1 : 1}) + .sort({created_at: sortOrder === 'DESC' ? -1 : 1}) .limit(limit); }; diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 988824a57..cd1e93141 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -14,7 +14,7 @@ const Comment = { user({author_id}, _, {loaders: {Users}}) { return Users.getByID.load(author_id); }, - replies({id, asset_id, reply_count}, {query: {sort, sortBy, limit, excludeIgnored}}, {loaders: {Comments}}) { + replies({id, asset_id, reply_count}, {query}, {loaders: {Comments}}) { // Don't bother looking up replies if there aren't any there! if (reply_count === 0) { @@ -24,14 +24,10 @@ const Comment = { }; } - return Comments.getByQuery({ - asset_id, - parent_id: id, - sort, - sortBy, - limit, - excludeIgnored, - }); + query.asset_id = asset_id; + query.parent_id = id; + + return Comments.getByQuery(query); }, replyCount({reply_count}) { diff --git a/graph/resolvers/comment_status_history.js b/graph/resolvers/comment_status_history.js index fb431b628..d51e71752 100644 --- a/graph/resolvers/comment_status_history.js +++ b/graph/resolvers/comment_status_history.js @@ -2,7 +2,7 @@ const {SEARCH_OTHER_USERS} = require('../../perms/constants'); const CommentStatusHistory = { assigned_by({assigned_by}, _, {user, loaders: {Users}}) { - if (!user || !user.can(SEARCH_OTHER_USERS) || assigned_by != null) { + if (!user || !user.can(SEARCH_OTHER_USERS) || assigned_by == null) { return null; } diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 07dab8eed..1e34e8ecd 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -50,24 +50,25 @@ const RootQuery = { return Comments.getCountByQuery(query); }, - assetMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Assets}}}) { + assetMetrics(_, query, {user, loaders: {Metrics: {Assets}}}) { if (user == null || !user.can(SEARCH_ASSETS)) { return null; } - if (sort === 'ACTIVITY') { - return Assets.getActivity({from, to, limit}); + const {sortBy} = query; + if (sortBy === 'ACTIVITY') { + return Assets.getActivity(query); } - return Assets.get({from, to, sort, limit}); + return Assets.get(query); }, - commentMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Comments}}}) { + commentMetrics(_, query, {user, loaders: {Metrics: {Comments}}}) { if (user == null || !user.can(SEARCH_COMMENT_METRICS)) { return null; } - return Comments.get({from, to, sort, limit}); + return Comments.get(query); }, // This returns the current user, ensure that if we aren't logged in, we @@ -97,7 +98,6 @@ const RootQuery = { } const {action_type} = query; - if (action_type) { query.ids = await Actions.getByTypes({action_type, item_type: 'USERS'}); query.statuses = ['PENDING']; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index b8c421be8..eec968132 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -132,7 +132,7 @@ input UsersQuery { cursor: Cursor # Sort the results by created_at. - sort: SORT_ORDER = DESC + sortOrder: SORT_ORDER = DESC } # AssetsQuery allows teh ability to query assets by specific fields @@ -247,7 +247,7 @@ input CommentsQuery { cursor: Cursor # Sort the results by from largest first. - sort: SORT_ORDER = DESC + sortOrder: SORT_ORDER = DESC # The order to sort the comments by, sorting by default the created at # timestamp. @@ -263,7 +263,7 @@ input CommentsQuery { input RepliesQuery { # Sort the results by from smallest first. - sort: SORT_ORDER = ASC + sortOrder: SORT_ORDER = ASC # The order to sort the comments by, sorting by default the created at # timestamp. @@ -737,11 +737,11 @@ type RootQuery { # Asset metrics related to user actions are saturated into the assets # returned. Parameters `from` and `to` are related to the action created_at field. - assetMetrics(from: Date!, to: Date!, sort: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!] + assetMetrics(from: Date!, to: Date!, sortBy: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!] # Comment metrics related to user actions are saturated into the comments # returned. Parameters `from` and `to` are related to the action created_at field. - commentMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Comment!] + commentMetrics(from: Date!, to: Date!, sortBy: ACTION_TYPE!, limit: Int = 10): [Comment!] } ################################################################################ diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js index 46bf0b892..a6df8969e 100644 --- a/plugin-api/beta/server/getReactionConfig.js +++ b/plugin-api/beta/server/getReactionConfig.js @@ -129,12 +129,12 @@ function getReactionConfig(reaction) { endCursor(ctx, nodes, {cursor}) { return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null; }, - sort(ctx, query, {cursor, sort}) { + sort(ctx, query, {cursor, sortOrder}) { if (cursor) { query = query.skip(cursor); } - return query.sort({[`action_counts.${reaction}`]: sort === 'DESC' ? -1 : 1, created_at: sort === 'DESC' ? -1 : 1}); + return query.sort({[`action_counts.${reaction}`]: sortOrder === 'DESC' ? -1 : 1, created_at: sortOrder === 'DESC' ? -1 : 1}); }, }, }, diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js index 6d9781ec4..5b1686353 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js @@ -62,7 +62,7 @@ const LOAD_MORE_QUERY = gql` cursor: $cursor tags: ["FEATURED"] asset_id: $asset_id, - sort: $sortOrder + sortOrder: $sortOrder sortBy: $sortBy excludeIgnored: $excludeIgnored } @@ -100,7 +100,7 @@ const enhance = compose( featuredComments: comments( query: { tags: ["FEATURED"] - sort: $sortOrder + sortOrder: $sortOrder sortBy: $sortBy } deep: true diff --git a/test/server/graph/loaders/metrics.js b/test/server/graph/loaders/metrics.js index e4036e867..4414428bd 100644 --- a/test/server/graph/loaders/metrics.js +++ b/test/server/graph/loaders/metrics.js @@ -16,7 +16,7 @@ describe('graph.loaders.Metrics', () => { describe('#Comments', () => { const query = ` query CommentMetrics($from: Date!, $to: Date!) { - flagged: commentMetrics(from: $from, to: $to, sort: FLAG) { + flagged: commentMetrics(from: $from, to: $to, sortBy: FLAG) { id } } @@ -24,11 +24,11 @@ describe('graph.loaders.Metrics', () => { describe('different comment states', () => { - beforeEach(() =>[ - CommentModel.create({id: '1', body: 'a new comment!'}), - CommentModel.create({id: '2', body: 'a new comment!'}), - CommentModel.create({id: '3', body: 'a new comment!'}) - ]); + beforeEach(() => CommentModel.create([ + {id: '1', body: 'a new comment!'}, + {id: '2', body: 'a new comment!'}, + {id: '3', body: 'a new comment!'} + ])); [ {flagged: 0, actions: []}, @@ -76,7 +76,7 @@ describe('graph.loaders.Metrics', () => { } query Metrics($from: Date!, $to: Date!) { - assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) { + assetsByFlag: assetMetrics(from: $from, to: $to, sortBy: FLAG) { ...metrics } } From f9bc5f79ec4feabd2e700b66776d64fd8e67698e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 28 Aug 2017 18:08:33 +0700 Subject: [PATCH 096/121] Don't sort and ignore duplicate comments (embed-stream) --- .../coral-embed-stream/src/graphql/utils.js | 13 +++-------- client/coral-framework/utils/index.js | 23 ++++--------------- .../containers/ProfileContainer.js | 4 ++-- plugin-api/beta/client/utils/index.js | 3 ++- .../client/containers/TabPane.js | 4 ++-- .../client/index.js | 4 ++-- 6 files changed, 16 insertions(+), 35 deletions(-) diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js index 11d3d411b..a63b6f8a6 100644 --- a/client/coral-embed-stream/src/graphql/utils.js +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -1,5 +1,5 @@ import update from 'immutability-helper'; -import {insertCommentsSorted} from 'coral-framework/utils'; +import {appendNewNodes} from 'coral-framework/utils'; function determineCommentDepth(comment) { let depth = 0; @@ -159,14 +159,7 @@ function findAndInsertFetchedComments(parent, comments, parent_id) { [connectionField]: { hasNextPage: {$set: comments.hasNextPage}, endCursor: {$set: comments.endCursor}, - nodes: {$apply: (nodes) => { - if (isAsset) { - return nodes.concat(comments.nodes); - } - return insertCommentsSorted(nodes, comments.nodes.filter( - (comment) => !nodes.some((node) => node.id === comment.id) - )); - }}, + nodes: {$apply: (nodes) => appendNewNodes(nodes, comments.nodes)}, }, }); } @@ -198,7 +191,7 @@ export function attachCommentToParent(topLevelComment, comment) { return update(topLevelComment, { replies: { nodes: { - $apply: (nodes) => insertCommentsSorted(nodes, comment), + $apply: (nodes) => appendNewNodes(nodes, [comment]), }, }, replyCount: { diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 4b771cef1..533ed85db 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -154,25 +154,12 @@ export function getErrorMessages(error) { return result; } -const ascending = (a, b) => { - const dateA = new Date(a.created_at); - const dateB = new Date(b.created_at); - if (dateA < dateB) { return -1; } - if (dateA > dateB) { return 1; } - return 0; -}; +export function appendNewNodes(nodesA, nodesB) { + return nodesA.concat(nodesB.filter((nodeB) => !nodesA.some((nodeA) => nodeA.id === nodeB.id))); +} -const descending = (a, b) => ascending(a, b) * -1; - -export function insertCommentsSorted(nodes, comments, sortOrder = 'ASC') { - const added = nodes.concat(comments); - if (sortOrder === 'ASC') { - return added.sort(ascending); - } - if (sortOrder === 'DESC') { - return added.sort(descending); - } - throw new Error(`Unknown sort order ${sortOrder}`); +export function prependNewNodes(nodesA, nodesB) { + return nodesB.filter((nodeB) => !nodesA.some((nodeA) => nodeA.id === nodeB.id)).concat(nodesA); } export const isTagged = (tags, which) => tags.some((t) => t.tag.name === which); diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index c59327338..f19aabff8 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -15,7 +15,7 @@ import CommentHistory from 'talk-plugin-history/CommentHistory'; // TODO: Auth logic needs refactoring. import {showSignInDialog, checkLogin} from 'coral-embed-stream/src/actions/auth'; -import {insertCommentsSorted} from 'plugin-api/beta/client/utils'; +import {appendNewNodes} from 'plugin-api/beta/client/utils'; import update from 'immutability-helper'; import {getSlotFragmentSpreads} from 'coral-framework/utils'; @@ -42,7 +42,7 @@ class ProfileContainer extends Component { me: { comments: { nodes: { - $apply: (nodes) => insertCommentsSorted(nodes, comments.nodes, 'DESC'), + $apply: (nodes) => appendNewNodes(nodes, comments.nodes), }, hasNextPage: {$set: comments.hasNextPage}, endCursor: {$set: comments.endCursor}, diff --git a/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js index fe1ba8bad..6dd18f3a1 100644 --- a/plugin-api/beta/client/utils/index.js +++ b/plugin-api/beta/client/utils/index.js @@ -1,6 +1,7 @@ export { isTagged, - insertCommentsSorted, + prependNewNodes, + appendNewNodes, getSlotFragmentSpreads, forEachError, capitalize, diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js index 5b1686353..d66a0fd49 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js @@ -6,7 +6,7 @@ import {withFragments, connect} from 'plugin-api/beta/client/hocs'; import Comment from '../containers/Comment'; import {addNotification} from 'plugin-api/beta/client/actions/notification'; import {viewComment} from 'coral-embed-stream/src/actions/stream'; -import {getDefinitionName} from 'plugin-api/beta/client/utils'; +import {appendNewNodes, getDefinitionName} from 'plugin-api/beta/client/utils'; import update from 'immutability-helper'; class TabPaneContainer extends React.Component { @@ -27,7 +27,7 @@ class TabPaneContainer extends React.Component { asset: { featuredComments: { nodes: { - $push: comments.nodes, + $apply: (nodes) => appendNewNodes(nodes, comments.nodes), }, hasNextPage: {$set: comments.hasNextPage}, endCursor: {$set: comments.endCursor}, diff --git a/plugins/talk-plugin-featured-comments/client/index.js b/plugins/talk-plugin-featured-comments/client/index.js index 30d59c546..c9598b1dd 100644 --- a/plugins/talk-plugin-featured-comments/client/index.js +++ b/plugins/talk-plugin-featured-comments/client/index.js @@ -9,7 +9,7 @@ import ModTag from './containers/ModTag'; import ModSubscription from './containers/ModSubscription'; import {findCommentInEmbedQuery} from 'coral-embed-stream/src/graphql/utils'; -import {insertCommentsSorted} from 'plugin-api/beta/client/utils'; +import {prependNewNodes} from 'plugin-api/beta/client/utils'; export default { reducer, @@ -60,7 +60,7 @@ export default { asset: { featuredComments: { nodes: { - $apply: (nodes) => insertCommentsSorted(nodes, comment, 'DESC') + $apply: (nodes) => prependNewNodes(nodes, [comment]), } }, featuredCommentsCount: { From 0dffc1583628b4f1e675c87adea74cc67d1f2078 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 28 Aug 2017 18:22:30 +0700 Subject: [PATCH 097/121] Hover styles --- plugin-api/beta/client/components/SortOption.css | 6 +++++- .../client/components/OffTopicFilter.css | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/plugin-api/beta/client/components/SortOption.css b/plugin-api/beta/client/components/SortOption.css index d1bbab6c4..2628d33f1 100644 --- a/plugin-api/beta/client/components/SortOption.css +++ b/plugin-api/beta/client/components/SortOption.css @@ -2,8 +2,12 @@ display: block; cursor: pointer; width: 100%; - padding: 4px 16px 4px 10px; + padding: 6px 16px 6px 10px; box-sizing: border-box; + + &:hover { + background-color: rgba(0, 0, 0, 0.05); + } } .input { diff --git a/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.css b/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.css index d31b1a4dd..7fca0aed4 100644 --- a/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.css +++ b/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.css @@ -2,8 +2,12 @@ display: block; cursor: pointer; width: 100%; - padding: 4px 16px 4px 10px; + padding: 6px 16px 6px 10px; box-sizing: border-box; + + &:hover { + background-color: rgba(0, 0, 0, 0.05); + } } .input { From bb42cffc17220f8365a5dfe443a7f1e62e72f45e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 28 Aug 2017 18:50:01 +0700 Subject: [PATCH 098/121] Add sort plugins for other reactions --- .eslintignore | 4 +++- .gitignore | 4 +++- plugins.default.json | 2 +- .../client/index.js | 2 +- .../client/.babelrc | 14 +++++++++++ .../client/.eslintrc.json | 23 +++++++++++++++++++ .../client/index.js | 19 +++++++++++++++ .../client/translations.yml | 5 ++++ plugins/talk-plugin-sort-most-loved/index.js | 2 ++ .../client/index.js | 2 +- .../client/.babelrc | 14 +++++++++++ .../client/.eslintrc.json | 23 +++++++++++++++++++ .../client/index.js | 19 +++++++++++++++ .../client/translations.yml | 5 ++++ .../talk-plugin-sort-most-respected/index.js | 2 ++ .../talk-plugin-sort-newest/client/index.js | 2 +- .../talk-plugin-sort-oldest/client/index.js | 2 +- 17 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 plugins/talk-plugin-sort-most-loved/client/.babelrc create mode 100644 plugins/talk-plugin-sort-most-loved/client/.eslintrc.json create mode 100644 plugins/talk-plugin-sort-most-loved/client/index.js create mode 100644 plugins/talk-plugin-sort-most-loved/client/translations.yml create mode 100644 plugins/talk-plugin-sort-most-loved/index.js create mode 100644 plugins/talk-plugin-sort-most-respected/client/.babelrc create mode 100644 plugins/talk-plugin-sort-most-respected/client/.eslintrc.json create mode 100644 plugins/talk-plugin-sort-most-respected/client/index.js create mode 100644 plugins/talk-plugin-sort-most-respected/client/translations.yml create mode 100644 plugins/talk-plugin-sort-most-respected/index.js diff --git a/.eslintignore b/.eslintignore index f6362a2e3..e09212b62 100644 --- a/.eslintignore +++ b/.eslintignore @@ -15,7 +15,9 @@ plugins/* !plugins/talk-plugin-featured-comments !plugins/talk-plugin-sort-newest !plugins/talk-plugin-sort-oldest -!plugins/talk-plugin-sort-most-liked !plugins/talk-plugin-sort-most-replied +!plugins/talk-plugin-sort-most-liked +!plugins/talk-plugin-sort-most-loved +!plugins/talk-plugin-sort-most-respected node_modules diff --git a/.gitignore b/.gitignore index c75ffc78b..3fdb05c89 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,9 @@ plugins/* !plugins/talk-plugin-featured-comments !plugins/talk-plugin-sort-newest !plugins/talk-plugin-sort-oldest -!plugins/talk-plugin-sort-most-liked !plugins/talk-plugin-sort-most-replied +!plugins/talk-plugin-sort-most-liked +!plugins/talk-plugin-sort-most-loved +!plugins/talk-plugin-sort-most-respected **/node_modules/* diff --git a/plugins.default.json b/plugins.default.json index a4ba0b525..89028dd7d 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -20,7 +20,7 @@ "talk-plugin-featured-comments", "talk-plugin-sort-newest", "talk-plugin-sort-oldest", - "talk-plugin-sort-most-liked", + "talk-plugin-sort-most-respected", "talk-plugin-sort-most-replied" ] } diff --git a/plugins/talk-plugin-sort-most-liked/client/index.js b/plugins/talk-plugin-sort-most-liked/client/index.js index 36faf7a88..04836df53 100644 --- a/plugins/talk-plugin-sort-most-liked/client/index.js +++ b/plugins/talk-plugin-sort-most-liked/client/index.js @@ -8,7 +8,7 @@ const SortOption = createSortOption( ); /** - * talk-plugin-sort-newest depends on talk-plugin-viewing-options. + * This plugin depends on talk-plugin-viewing-options. */ export default { diff --git a/plugins/talk-plugin-sort-most-loved/client/.babelrc b/plugins/talk-plugin-sort-most-loved/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-sort-most-loved/client/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} \ No newline at end of file diff --git a/plugins/talk-plugin-sort-most-loved/client/.eslintrc.json b/plugins/talk-plugin-sort-most-loved/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-sort-most-loved/client/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + } + }, + "parser": "babel-eslint", + "plugins": [ + "react" + ], + "rules": { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "no-console": ["warn", { "allow": ["warn", "error"] }] + } +} diff --git a/plugins/talk-plugin-sort-most-loved/client/index.js b/plugins/talk-plugin-sort-most-loved/client/index.js new file mode 100644 index 000000000..121a76239 --- /dev/null +++ b/plugins/talk-plugin-sort-most-loved/client/index.js @@ -0,0 +1,19 @@ +import translations from './translations.yml'; +import {createSortOption} from 'plugin-api/beta/client/factories'; +import {t} from 'plugin-api/beta/client/services'; + +const SortOption = createSortOption( + () => t('talk-plugin-sort-most-loved.label'), + {sortBy: 'LOVES', sortOrder: 'DESC'}, +); + +/** + * This plugin depends on talk-plugin-viewing-options. + */ + +export default { + translations, + slots: { + viewingOptionsSort: [SortOption] + } +}; diff --git a/plugins/talk-plugin-sort-most-loved/client/translations.yml b/plugins/talk-plugin-sort-most-loved/client/translations.yml new file mode 100644 index 000000000..095091ac5 --- /dev/null +++ b/plugins/talk-plugin-sort-most-loved/client/translations.yml @@ -0,0 +1,5 @@ +en: + talk-plugin-sort-most-loved: + label: Most loved first +es: + talk-plugin-sort-most-loved: diff --git a/plugins/talk-plugin-sort-most-loved/index.js b/plugins/talk-plugin-sort-most-loved/index.js new file mode 100644 index 000000000..7be35b6b6 --- /dev/null +++ b/plugins/talk-plugin-sort-most-loved/index.js @@ -0,0 +1,2 @@ +module.exports = { +}; diff --git a/plugins/talk-plugin-sort-most-replied/client/index.js b/plugins/talk-plugin-sort-most-replied/client/index.js index 52b837410..bdef73f03 100644 --- a/plugins/talk-plugin-sort-most-replied/client/index.js +++ b/plugins/talk-plugin-sort-most-replied/client/index.js @@ -8,7 +8,7 @@ const SortOption = createSortOption( ); /** - * talk-plugin-sort-newest depends on talk-plugin-viewing-options. + * This plugin depends on talk-plugin-viewing-options. */ export default { diff --git a/plugins/talk-plugin-sort-most-respected/client/.babelrc b/plugins/talk-plugin-sort-most-respected/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-sort-most-respected/client/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "es2015" + ], + "plugins": [ + "add-module-exports", + "transform-class-properties", + "transform-decorators-legacy", + "transform-object-assign", + "transform-object-rest-spread", + "transform-async-to-generator", + "transform-react-jsx" + ] +} \ No newline at end of file diff --git a/plugins/talk-plugin-sort-most-respected/client/.eslintrc.json b/plugins/talk-plugin-sort-most-respected/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-sort-most-respected/client/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + } + }, + "parser": "babel-eslint", + "plugins": [ + "react" + ], + "rules": { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "no-console": ["warn", { "allow": ["warn", "error"] }] + } +} diff --git a/plugins/talk-plugin-sort-most-respected/client/index.js b/plugins/talk-plugin-sort-most-respected/client/index.js new file mode 100644 index 000000000..762c94a7f --- /dev/null +++ b/plugins/talk-plugin-sort-most-respected/client/index.js @@ -0,0 +1,19 @@ +import translations from './translations.yml'; +import {createSortOption} from 'plugin-api/beta/client/factories'; +import {t} from 'plugin-api/beta/client/services'; + +const SortOption = createSortOption( + () => t('talk-plugin-sort-most-respected.label'), + {sortBy: 'RESPECTS', sortOrder: 'DESC'}, +); + +/** + * This plugin depends on talk-plugin-viewing-options. + */ + +export default { + translations, + slots: { + viewingOptionsSort: [SortOption] + } +}; diff --git a/plugins/talk-plugin-sort-most-respected/client/translations.yml b/plugins/talk-plugin-sort-most-respected/client/translations.yml new file mode 100644 index 000000000..32366dfc5 --- /dev/null +++ b/plugins/talk-plugin-sort-most-respected/client/translations.yml @@ -0,0 +1,5 @@ +en: + talk-plugin-sort-most-respected: + label: Most respected first +es: + talk-plugin-sort-most-respected: diff --git a/plugins/talk-plugin-sort-most-respected/index.js b/plugins/talk-plugin-sort-most-respected/index.js new file mode 100644 index 000000000..7be35b6b6 --- /dev/null +++ b/plugins/talk-plugin-sort-most-respected/index.js @@ -0,0 +1,2 @@ +module.exports = { +}; diff --git a/plugins/talk-plugin-sort-newest/client/index.js b/plugins/talk-plugin-sort-newest/client/index.js index d4979e299..83c20b166 100644 --- a/plugins/talk-plugin-sort-newest/client/index.js +++ b/plugins/talk-plugin-sort-newest/client/index.js @@ -8,7 +8,7 @@ const SortOption = createSortOption( ); /** - * talk-plugin-sort-newest depends on talk-plugin-viewing-options. + * This plugin depends on talk-plugin-viewing-options. */ export default { diff --git a/plugins/talk-plugin-sort-oldest/client/index.js b/plugins/talk-plugin-sort-oldest/client/index.js index a1aac7709..e6a480340 100644 --- a/plugins/talk-plugin-sort-oldest/client/index.js +++ b/plugins/talk-plugin-sort-oldest/client/index.js @@ -8,7 +8,7 @@ const SortOption = createSortOption( ); /** - * talk-plugin-sort-oldest depends on talk-plugin-viewing-options. + * This plugin depends on talk-plugin-viewing-options. */ export default { From 153c5acce1370dd98bde7f0bebffbbc3d89ee20c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 28 Aug 2017 19:15:29 +0700 Subject: [PATCH 099/121] Only load my own comments in history --- client/coral-settings/containers/ProfileContainer.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index e6dad3899..4a9aea911 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -37,7 +37,7 @@ class ProfileContainer extends Component { limit: 5, cursor: this.props.root.me.comments.endCursor, }, - updateQuery: (previous, {fetchMoreResult:{comments}}) => { + updateQuery: (previous, {fetchMoreResult:{me: {comments}}}) => { const updated = update(previous, { me: { comments: { @@ -124,8 +124,10 @@ const CommentFragment = gql` const LOAD_MORE_QUERY = gql` query TalkSettings_LoadMoreComments($limit: Int, $cursor: Date) { - comments(query: {limit: $limit, cursor: $cursor}) { - ...TalkSettings_CommentConnectionFragment + me { + comments(query: {limit: $limit, cursor: $cursor}) { + ...TalkSettings_CommentConnectionFragment + } } } ${CommentFragment} From 7c9e2297dffea8c8f159a18048e054abf363219b Mon Sep 17 00:00:00 2001 From: Jeff Nelson Date: Mon, 28 Aug 2017 09:54:20 -0400 Subject: [PATCH 100/121] don't hardcode branch --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b12315e4..4bec86d07 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk%2Ftree%2Ffeature%2Fheroku-deploy&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore) +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore) Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html) @@ -20,7 +20,7 @@ endpoint when the server is running with built assets. - Blog: https://blog.coralproject.net -- Community Guides for Journalism: https://guides.coralproject.net/ +- Community Guides for Journalism: https://guides.coralproject.net/ ## License From 7856c9d9479d94ad69a2bb980d96330005036a77 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 28 Aug 2017 21:05:32 +0700 Subject: [PATCH 101/121] Fix live update false comments in permalink view --- client/coral-embed-stream/src/graphql/utils.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js index 11d3d411b..712506692 100644 --- a/client/coral-embed-stream/src/graphql/utils.js +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -36,11 +36,15 @@ function applyToCommentsOrigin(root, callback) { } function findAndInsertComment(parent, comment) { - const [connectionField, countField, action] = parent.__typename === 'Asset' + const isAsset = parent.__typename === 'Asset'; + const [connectionField, countField, action] = isAsset ? ['comments', 'commentCount', '$unshift'] : ['replies', 'replyCount', '$push']; - if (!comment.parent || parent.id === comment.parent.id) { + if ( + !comment.parent && isAsset // A top level comment in the asset. + || comment.parent && parent.id === comment.parent.id // A reply at the correct parent. + ) { return update(parent, { [connectionField]: { nodes: {[action]: [comment]}, From 7911e366e5a6c51a7a31d761189956c7a18d4fce Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 28 Aug 2017 22:04:21 +0700 Subject: [PATCH 102/121] Remove semicolon --- plugins/talk-plugin-auth/client/components/FakeComment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/talk-plugin-auth/client/components/FakeComment.js b/plugins/talk-plugin-auth/client/components/FakeComment.js index 9f1d90ae7..6997ca984 100644 --- a/plugins/talk-plugin-auth/client/components/FakeComment.js +++ b/plugins/talk-plugin-auth/client/components/FakeComment.js @@ -9,7 +9,7 @@ export const FakeComment = ({username, created_at, body}) => (
    {username} - ; +
    {body} From 12d66a49d049b0f8b2b47be3a299620a9aae3b6f Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 28 Aug 2017 12:17:01 -0300 Subject: [PATCH 103/121] Adding commentAuthorTags --- client/coral-embed-stream/src/components/Comment.js | 8 ++++++++ client/coral-embed-stream/src/containers/Comment.js | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 79dfb1b34..3eecc80ea 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -447,6 +447,14 @@ export default class Comment extends React.Component { {isStaff(comment.tags) ? Staff : null} + + { diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index e34218883..f955066aa 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -16,7 +16,8 @@ const slots = [ 'commentContent', 'commentReactions', 'commentAvatar', - 'commentAuthorName' + 'commentAuthorName', + 'commentAuthorTags' ]; /** From 8cfccb7b00df7d9658a4a22f90f99f6ff04d1378 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 28 Aug 2017 09:42:37 -0600 Subject: [PATCH 104/121] fixed query --- graph/resolvers/asset.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js index a48af7674..b5fd92d34 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -21,6 +21,9 @@ const Asset = { query.parent_id = null; } + // Include the asset id in the search. + query.asset_id = id; + return Comments.getByQuery(query); }, commentCount({id, commentCount}, {tags}, {loaders: {Comments}}) { From 44c679895960baf611d9c70cd28eca2475c05f1a Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 29 Aug 2017 00:20:36 +0700 Subject: [PATCH 105/121] Fix action_type query --- graph/loaders/comments.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index cafd647af..396f953ce 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -11,6 +11,7 @@ const { CACHE_EXPIRY_COMMENT_COUNT } = require('../../config'); const ms = require('ms'); +const sc = require('snake-case'); const CommentModel = require('../../models/comment'); @@ -112,10 +113,8 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au if (context.user != null && context.user.can(SEARCH_OTHERS_COMMENTS) && action_type) { query = query.where({ - action_counts: { - [action_type.toLowerCase()]: { - $gt: 0, - } + [`action_counts.${sc(action_type.toLowerCase())}`]: { + $gt: 0, }, }); } @@ -298,10 +297,8 @@ const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, auth if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) { comments = comments.where({ - action_counts: { - [action_type.toLowerCase()]: { - $gt: 0, - } + [`action_counts.${sc(action_type.toLowerCase())}`]: { + $gt: 0, }, }); } From 057d8cabd2a0f952b1675c7716bf5b9ae38a98fc Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 28 Aug 2017 11:47:51 -0600 Subject: [PATCH 106/121] Increased debugging and optimized a query path --- graph/loaders/users.js | 17 +++- graph/setupFunctions.js | 117 +++++++++++++++++++++++ graph/subscriptions.js | 180 +++++++---------------------------- middleware/authentication.js | 6 ++ services/passport.js | 4 + 5 files changed, 178 insertions(+), 146 deletions(-) create mode 100644 graph/setupFunctions.js diff --git a/graph/loaders/users.js b/graph/loaders/users.js index f11effbd1..999ad1938 100644 --- a/graph/loaders/users.js +++ b/graph/loaders/users.js @@ -5,9 +5,20 @@ const util = require('./util'); const UsersService = require('../../services/users'); const UserModel = require('../../models/user'); -const genUserByIDs = (context, ids) => UsersService - .findByIdArray(ids) - .then(util.singleJoinBy(ids, 'id')); +const genUserByIDs = async (context, ids) => { + if (!ids || ids.length === 0) { + return []; + } + + if (ids.length === 1) { + const user = await UsersService.findById(ids[0]); + return [user]; + } + + return UsersService + .findByIdArray(ids) + .then(util.singleJoinBy(ids, 'id')); +}; /** * Retrieves users based on the passed in query that is filtered by the diff --git a/graph/setupFunctions.js b/graph/setupFunctions.js new file mode 100644 index 000000000..273aedfa2 --- /dev/null +++ b/graph/setupFunctions.js @@ -0,0 +1,117 @@ +const { + SUBSCRIBE_COMMENT_ACCEPTED, + SUBSCRIBE_COMMENT_REJECTED, + SUBSCRIBE_COMMENT_FLAGGED, + SUBSCRIBE_ALL_COMMENT_EDITED, + SUBSCRIBE_ALL_COMMENT_ADDED, + SUBSCRIBE_ALL_USER_SUSPENDED, + SUBSCRIBE_ALL_USER_BANNED, + SUBSCRIBE_ALL_USERNAME_REJECTED, +} = require('../perms/constants'); + +const merge = require('lodash/merge'); +const debug = require('debug')('talk:graph:setupFunctions'); +const plugins = require('../services/plugins'); + +/** + * Plugin support requires that we merge in existing setupFunctions with our new + * plugin based ones. This allows plugins to extend existing setupFunctions as well + * as provide new ones. + */ +const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plugin, setupFunctions}) => { + debug(`added plugin '${plugin.name}'`); + + return merge(acc, setupFunctions); +}, { + commentAdded: (options, args) => ({ + commentAdded: { + filter: (comment, context) => { + if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))) { + return false; + } + return !args.asset_id || comment.asset_id === args.asset_id; + } + }, + }), + commentEdited: (options, args) => ({ + commentEdited: { + filter: (comment, context) => { + if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_EDITED))) { + return false; + } + return !args.asset_id || comment.asset_id === args.asset_id; + } + }, + }), + commentFlagged: (options, args) => ({ + commentFlagged: { + filter: (comment, context) => { + if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_FLAGGED)) { + return false; + } + return !args.asset_id || comment.asset_id === args.asset_id; + } + }, + }), + commentAccepted: (options, args) => ({ + commentAccepted: { + filter: (comment, context) => { + if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_ACCEPTED)) { + return false; + } + return !args.asset_id || comment.asset_id === args.asset_id; + } + }, + }), + commentRejected: (options, args) => ({ + commentRejected: { + filter: (comment, context) => { + if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_REJECTED)) { + return false; + } + return !args.asset_id || comment.asset_id === args.asset_id; + } + }, + }), + userSuspended: (options, args) => ({ + userSuspended: { + filter: (user, context) => { + if ( + !context.user + || args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_SUSPENDED) + ) { + return false; + } + return !args.user_id || user.id === args.user_id; + } + }, + }), + userBanned: (options, args) => ({ + userBanned: { + filter: (user, context) => { + if ( + !context.user + || args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_BANNED) + ) { + return false; + } + return !args.user_id || user.id === args.user_id; + } + }, + }), + usernameRejected: (options, args) => ({ + usernameRejected: { + filter: (user, context) => { + if ( + !context.user + || args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USERNAME_REJECTED) + ) { + return false; + } + return !args.user_id || user.id === args.user_id; + } + }, + }), +}); + +module.exports = setupFunctions; diff --git a/graph/subscriptions.js b/graph/subscriptions.js index 71b8abc6a..59dd6e923 100644 --- a/graph/subscriptions.js +++ b/graph/subscriptions.js @@ -1,133 +1,56 @@ const {SubscriptionManager} = require('graphql-subscriptions'); const {SubscriptionServer} = require('subscriptions-transport-ws'); -const _ = require('lodash'); const debug = require('debug')('talk:graph:subscriptions'); const pubsub = require('../services/pubsub'); const schema = require('./schema'); const Context = require('./context'); -const plugins = require('../services/plugins'); const {deserializeUser} = require('../services/subscriptions'); +const setupFunctions = require('./setupFunctions'); const ms = require('ms'); const { KEEP_ALIVE } = require('../config'); -const { - SUBSCRIBE_COMMENT_ACCEPTED, - SUBSCRIBE_COMMENT_REJECTED, - SUBSCRIBE_COMMENT_FLAGGED, - SUBSCRIBE_ALL_COMMENT_EDITED, - SUBSCRIBE_ALL_COMMENT_ADDED, - SUBSCRIBE_ALL_USER_SUSPENDED, - SUBSCRIBE_ALL_USER_BANNED, - SUBSCRIBE_ALL_USERNAME_REJECTED, -} = require('../perms/constants'); - const {BASE_PATH} = require('../url'); -/** - * Plugin support requires that we merge in existing setupFunctions with our new - * plugin based ones. This allows plugins to extend existing setupFunctions as well - * as provide new ones. - */ -const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plugin, setupFunctions}) => { - debug(`added plugin '${plugin.name}'`); +const onConnect = ({token}, connection) => { - return _.merge(acc, setupFunctions); -}, { - commentAdded: (options, args) => ({ - commentAdded: { - filter: (comment, context) => { - if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))) { - return false; - } - return !args.asset_id || comment.asset_id === args.asset_id; - } - }, - }), - commentEdited: (options, args) => ({ - commentEdited: { - filter: (comment, context) => { - if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_EDITED))) { - return false; - } - return !args.asset_id || comment.asset_id === args.asset_id; - } - }, - }), - commentFlagged: (options, args) => ({ - commentFlagged: { - filter: (comment, context) => { - if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_FLAGGED)) { - return false; - } - return !args.asset_id || comment.asset_id === args.asset_id; - } - }, - }), - commentAccepted: (options, args) => ({ - commentAccepted: { - filter: (comment, context) => { - if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_ACCEPTED)) { - return false; - } - return !args.asset_id || comment.asset_id === args.asset_id; - } - }, - }), - commentRejected: (options, args) => ({ - commentRejected: { - filter: (comment, context) => { - if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_REJECTED)) { - return false; - } - return !args.asset_id || comment.asset_id === args.asset_id; - } - }, - }), - userSuspended: (options, args) => ({ - userSuspended: { - filter: (user, context) => { - if ( - !context.user - || args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_SUSPENDED) - ) { - return false; - } - return !args.user_id || user.id === args.user_id; - } - }, - }), - userBanned: (options, args) => ({ - userBanned: { - filter: (user, context) => { - if ( - !context.user - || args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USER_BANNED) - ) { - return false; - } - return !args.user_id || user.id === args.user_id; - } - }, - }), - usernameRejected: (options, args) => ({ - usernameRejected: { - filter: (user, context) => { - if ( - !context.user - || args.user_id !== user.id && !context.user.can(SUBSCRIBE_ALL_USERNAME_REJECTED) - ) { - return false; - } - return !args.user_id || user.id === args.user_id; - } - }, - }), -}); + // Attach the token from the connection options if it was provided. + if (token) { + + debug('token sent via onConnect, attaching to the headers of the upgrade request'); + + // Attach it to the upgrade request. + connection.upgradeReq.headers['authorization'] = `Bearer ${token}`; + } +}; + +const onOperation = (parsedMessage, baseParams, connection) => { + + // Cache the upgrade request. + let upgradeReq = connection.upgradeReq; + + // Attach the context per request. + baseParams.context = async () => { + let req; + + try { + req = await deserializeUser(upgradeReq); + debug(`user ${req.user ? 'was' : 'was not'} on websocket request`); + } catch (e) { + console.error(e); + + return new Context({}); + } + + return new Context(req); + }; + + return baseParams; +}; /** * This creates a new subscription manager. @@ -138,37 +61,8 @@ const createSubscriptionManager = (server) => new SubscriptionServer({ pubsub: pubsub.getClient(), setupFunctions, }), - onConnect: ({token}, connection) => { - - // Attach the token from the connection options if it was provided. - if (token) { - - // Attach it to the upgrade request. - connection.upgradeReq.headers['authorization'] = `Bearer ${token}`; - } - }, - onOperation: (parsedMessage, baseParams, connection) => { - - // Cache the upgrade request. - let upgradeReq = connection.upgradeReq; - - // Attach the context per request. - baseParams.context = async () => { - let req; - - try { - req = await deserializeUser(upgradeReq); - } catch (e) { - console.error(e); - - return new Context({}); - } - - return new Context(req); - }; - - return baseParams; - }, + onConnect, + onOperation, keepAlive: ms(KEEP_ALIVE) }, { server, diff --git a/middleware/authentication.js b/middleware/authentication.js index ca6949a5c..3e4b3fbff 100644 --- a/middleware/authentication.js +++ b/middleware/authentication.js @@ -1,16 +1,22 @@ const {passport} = require('../services/passport'); +const debug = require('debug')('talk:middleware:authentication'); const authentication = (req, res, next) => passport.authenticate('jwt', { session: false }, (err, user) => { if (err) { + debug(`cannot get the user: ${err}`); return next(err); } if (user) { + debug('user was on request'); + // Attach the user to the request object, now that we know it exists. req.user = user; + } else { + debug('user was not on request'); } next(); diff --git a/services/passport.js b/services/passport.js index 14a2134ce..4e5bbd708 100644 --- a/services/passport.js +++ b/services/passport.js @@ -54,11 +54,14 @@ const GenerateToken = (user) => { const SetTokenForSafari = (req, res, token) => { const browser = bowser._detect(req.headers['user-agent']); if (browser.ios || browser.safari) { + debug('browser was safari/ios, setting a cookie'); res.cookie(JWT_SIGNING_COOKIE_NAME, token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', expires: new Date(Date.now() + ms(JWT_EXPIRY)) }); + } else { + debug('browser wasn\'t safari/ios, didn\'t set a cookie'); } }; @@ -170,6 +173,7 @@ const HandleLogout = (req, res, next) => { // Only clear the cookie on logout if enabled. if (JWT_CLEAR_COOKIE_LOGOUT) { + debug('clearing the login cookie'); res.clearCookie(JWT_SIGNING_COOKIE_NAME); } From 93c1ecd12d88dbd3da3af863f8398e860497e6bf Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 28 Aug 2017 16:25:26 -0300 Subject: [PATCH 107/121] My Comment Reactions --- client/coral-framework/utils/index.js | 8 +++++++ .../containers/ProfileContainer.js | 5 ++++ client/talk-plugin-history/Comment.css | 11 +++++++-- client/talk-plugin-history/Comment.js | 24 ++++++++++++++----- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 533ed85db..df264d360 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -184,3 +184,11 @@ export function getShallowChanges(a, b) { return union(Object.keys(a), Object.keys(b)) .filter((key) => a[key] !== b[key]); } + +export function getTotalReactionsCount(actionSummaries) { + return actionSummaries + .filter((s) => s.__typename !== 'FlagActionSummary') + .reduce((total, summary) => { + return total + summary.count; + }, 0); +} diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index f5ea64005..907839a92 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -108,6 +108,11 @@ const CommentFragment = gql` nodes { id body + replyCount + action_summaries { + count + __typename + } asset { id title diff --git a/client/talk-plugin-history/Comment.css b/client/talk-plugin-history/Comment.css index 646e1f054..ba63aaeb1 100644 --- a/client/talk-plugin-history/Comment.css +++ b/client/talk-plugin-history/Comment.css @@ -6,6 +6,7 @@ display: flex; align-items: baseline; justify-content: space-between; + padding-bottom: 20px; } .myComment:last-child { @@ -16,11 +17,17 @@ text-decoration: none; font-weight: bold; font-size: 12px; - color: #2c3e50; + color: #757575; } -.commentBody { +.commentSummary { + font-size: 14px; + margin: 30px 0 10px; + color: #424242; +} +.commentSummaryReactions { + margin-right: 5px; } .sidebar { diff --git a/client/talk-plugin-history/Comment.js b/client/talk-plugin-history/Comment.js index 4cf9d875c..f3bb47914 100644 --- a/client/talk-plugin-history/Comment.js +++ b/client/talk-plugin-history/Comment.js @@ -4,6 +4,8 @@ import styles from './Comment.css'; import Slot from 'coral-framework/components/Slot'; import PubDate from '../talk-plugin-pubdate/PubDate'; import CommentContent from '../coral-embed-stream/src/components/CommentContent'; +import cn from 'classnames'; +import {getTotalReactionsCount} from 'coral-framework/utils'; import t from 'coral-framework/services/i18n'; @@ -11,24 +13,34 @@ class Comment extends React.Component { render() { const {comment, link, data, root} = this.props; + // console.log(comment.action_summaries, getTotalReactionsCount(comment.action_summaries)); + return (
      @@ -38,7 +50,7 @@ class Comment extends React.Component {
    • - + Date: Mon, 28 Aug 2017 16:30:17 -0300 Subject: [PATCH 108/121] Translations added --- client/talk-plugin-history/Comment.js | 8 +++----- locales/en.yml | 3 +++ locales/es.yml | 3 +++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/client/talk-plugin-history/Comment.js b/client/talk-plugin-history/Comment.js index f3bb47914..c9851b43c 100644 --- a/client/talk-plugin-history/Comment.js +++ b/client/talk-plugin-history/Comment.js @@ -13,7 +13,6 @@ class Comment extends React.Component { render() { const {comment, link, data, root} = this.props; - // console.log(comment.action_summaries, getTotalReactionsCount(comment.action_summaries)); return (
      @@ -27,18 +26,17 @@ class Comment extends React.Component { />
      - {getTotalReactionsCount(comment.action_summaries)} reactions + {getTotalReactionsCount(comment.action_summaries)} {t('common.reactions')} - - {comment.replyCount} replies + {comment.replyCount} {t('common.replies')}
      diff --git a/locales/en.yml b/locales/en.yml index 2ec0c816c..da7bd54d1 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -41,6 +41,9 @@ en: common: copy: 'Copy' error: 'An error has occurred.' + replies: 'replies' + reactions: 'reactions' + story: 'Story' community: account_creation_date: "Account Creation Date" active: Active diff --git a/locales/es.yml b/locales/es.yml index 877a42841..8c3683021 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -41,6 +41,9 @@ es: common: copy: 'Copiar' error: 'Ha ocurrido un error.' + replies: 'respuestas' + reactions: 'reacciones' + story: 'Artículo' community: account_creation_date: "Fecha de creación de la cuenta" active: Activa From e77bdf0b900853d329fa9e31a7a4073413ef1b45 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 28 Aug 2017 16:31:46 -0300 Subject: [PATCH 109/121] linting --- client/coral-settings/containers/ProfileContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index 907839a92..571952639 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -109,7 +109,7 @@ const CommentFragment = gql` id body replyCount - action_summaries { + action_summaries { count __typename } From eb586f217b11f741402fca085b54f58f688ad165 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 28 Aug 2017 14:06:54 -0600 Subject: [PATCH 110/121] Upgraded/pruned deps, applied linting fixes --- .eslintrc.json | 61 +- bin/cli-jobs | 21 +- bin/cli-plugins | 2 +- bin/cli-users | 83 +- bin/verifications/database/comments.js | 2 +- client/.eslintrc.json | 1 + client/coral-admin/src/actions/community.js | 12 +- client/coral-admin/src/actions/install.js | 20 +- .../coral-admin/src/components/AdminLogin.js | 28 +- .../src/components/SuspendUserDialog.js | 4 +- .../coral-admin/src/components/UserDetail.js | 48 +- .../src/components/UserDetailComment.js | 12 +- .../coral-admin/src/components/ui/Header.js | 60 +- client/coral-admin/src/containers/Layout.js | 8 +- .../coral-admin/src/containers/UserDetail.js | 14 +- .../Community/components/FlaggedAccounts.js | 28 +- .../src/routes/Community/components/People.js | 4 +- .../components/RejectUsernameDialog.js | 56 +- .../src/routes/Community/components/Table.js | 6 +- .../src/routes/Community/components/User.js | 2 +- .../routes/Community/containers/Community.js | 16 +- .../routes/Configure/components/Configure.js | 48 +- .../Configure/components/StreamSettings.js | 8 +- .../Dashboard/components/ActivityWidget.js | 26 +- .../routes/Dashboard/components/FlagWidget.js | 34 +- .../routes/Dashboard/components/LikeWidget.js | 28 +- .../routes/Dashboard/containers/Dashboard.js | 18 +- .../components/Steps/CreateYourAccount.js | 14 +- .../routes/Moderation/components/Comment.js | 12 +- .../Moderation/components/ModerationQueue.js | 4 +- .../Moderation/components/StorySearch.js | 26 +- .../Moderation/containers/StorySearch.js | 16 +- .../src/routes/Stories/components/Stories.js | 26 +- .../components/ConfigureCommentStream.js | 14 +- .../components/QuestionBoxBuilder.js | 48 +- .../containers/ConfigureStreamContainer.js | 2 +- .../src/components/AllCommentsPane.js | 52 +- .../src/components/Comment.js | 54 +- .../src/components/EditableCommentContent.js | 8 +- .../src/components/NewCount.js | 12 +- .../src/components/Stream.js | 26 +- .../src/components/SuspendedAccount.js | 54 +- .../src/components/TopRightMenu.js | 2 +- .../coral-embed-stream/src/graphql/utils.js | 4 +- client/coral-framework/helpers/validate.js | 2 +- client/coral-framework/hocs/withMutation.js | 44 +- client/coral-framework/hocs/withQuery.js | 28 +- client/coral-framework/services/plugins.js | 12 +- client/coral-framework/utils/index.js | 2 +- client/coral-framework/utils/user.js | 2 +- .../coral-settings/components/IgnoredUsers.js | 2 +- .../containers/ProfileContainer.js | 12 +- client/coral-ui/components/CoralLogo.js | 52 +- client/coral-ui/components/Dialog.js | 2 +- client/coral-ui/components/Pager.js | 14 +- client/coral-ui/components/Wizard.js | 12 +- .../components/FlagButton.js | 44 +- .../components/FlagComment.js | 26 +- client/talk-plugin-infobox/InfoBox.js | 8 +- .../talk-plugin-moderation/ModerationLink.js | 12 +- graph/loaders/comments.js | 8 +- graph/loaders/metrics.js | 16 +- graph/loaders/users.js | 2 +- graph/mutators/comment.js | 2 +- graph/mutators/tag.js | 2 +- graph/mutators/user.js | 6 +- package.json | 49 +- plugin-api/beta/client/hocs/withTags.js | 28 +- plugin-api/beta/server/getReactionConfig.js | 67 +- .../client/components/ForgotContent.js | 8 +- .../client/components/SignInButton.js | 4 +- .../client/components/SignInContainer.js | 3 + .../client/components/SignInContent.js | 104 +- .../client/components/ModTag.js | 8 +- .../client/components/Tag.js | 20 +- .../client/components/PermalinkButton.js | 2 +- routes/api/users/index.js | 12 +- routes/assets/index.js | 49 +- services/cache.js | 110 +- services/mailer.js | 20 +- services/metadata.js | 2 +- services/passport.js | 39 +- services/scraper.js | 46 +- services/users.js | 18 +- services/wordlist.js | 51 +- test/helpers/mongoose.js | 12 +- test/server/graph/context.js | 12 +- test/server/graph/mutations/createComment.js | 32 +- test/server/routes/api/account/index.js | 12 +- test/server/routes/api/auth/index.js | 23 +- test/server/routes/api/user/index.js | 18 +- test/server/services/users.js | 149 +- yarn.lock | 1235 +++++++++-------- 93 files changed, 1758 insertions(+), 1709 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 12355ac34..237650932 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,51 +7,53 @@ "parserOptions": { "ecmaVersion": 2017 }, + "plugins": [ + "promise", + "json" + ], "rules": { "indent": ["error", 2 ], - "no-console": [ - 0 - ], + "no-console": "off", "linebreak-style": ["error", "unix"], "quotes": ["error", "single"], "semi": ["error", "always"], - "no-template-curly-in-string": [1], - "no-unsafe-negation": [1], - "array-callback-return": [1], + "no-template-curly-in-string": "warn", + "no-unsafe-negation": "warn", + "array-callback-return": "warn", "arrow-parens": ["warn", "always"], "template-curly-spacing": "warn", - "eqeqeq": [2, "smart"], - "no-eval": [2], - "no-global-assign": [2], - "no-implied-eval": [2], + "eqeqeq": ["error", "smart"], + "no-eval": "error", + "no-global-assign": "error", + "no-implied-eval": "error", "lines-around-comment": ["warn", {"beforeLineComment": true}], "spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }], - "no-script-url": [2], - "no-throw-literal": [2], - "yoda": [1], - "no-path-concat": [2], - "eol-last": [1], - "no-nested-ternary": [1], - "no-tabs": [2], - "no-unneeded-ternary": [1], - "object-curly-spacing": [1], + "no-script-url": "error", + "no-throw-literal": "error", + "yoda": "warn", + "no-path-concat": "error", + "eol-last": "warn", + "no-nested-ternary": "warn", + "no-tabs": "error", + "no-unneeded-ternary": "warn", + "object-curly-spacing": "warn", "space-infix-ops": ["error"], "space-in-parens": ["error", "never"], "space-unary-ops": ["error", { "words": true, "nonwords": false }], - "no-const-assign": [2], - "no-duplicate-imports": [2], - "prefer-template": [1], + "no-const-assign": "error", + "no-duplicate-imports": "error", + "prefer-template": "warn", "comma-spacing": ["error", { "after": true }], - "no-var": [2], - "no-lonely-if": [2], - "curly": [2], + "no-var": "error", + "no-lonely-if": "error", + "curly": "error", "no-unused-vars": ["error", { "argsIgnorePattern": "^_|next", "varsIgnorePattern": "^_" @@ -61,6 +63,13 @@ }], "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 - }] + }], + "promise/no-return-wrap": "error", + "promise/param-names": "error", + "promise/catch-or-return": "error", + "promise/no-native": "off", + "promise/no-nesting": "warn", + "promise/no-promise-in-callback": "warn", + "promise/no-callback-in-promise": "warn" } } diff --git a/bin/cli-jobs b/bin/cli-jobs index f22e1068b..c5aefc518 100755 --- a/bin/cli-jobs +++ b/bin/cli-jobs @@ -78,18 +78,23 @@ function rangeJobsByState(state = 'complete', limit) { /** * Cleans up the jobs that are in the queue. */ -function cleanupJobs(options) { +async function cleanupJobs(options) { const n = 100; - Promise.all([ - rangeJobsByState('complete', n), - options.stuck ? rangeJobsByState('failed', n) : false - ]) - .then((joblists) => joblists.filter((jobs) => jobs).map(removeJobs)) - .then(() => { + try { + const joblists = await Promise.all([ + rangeJobsByState('complete', n), + options.stuck ? rangeJobsByState('failed', n) : false + ]); + + await joblists.filter((jobs) => jobs).map(removeJobs); + util.shutdown(); console.log('Removed old jobs'); - }); + } catch (err) { + console.error(err); + util.shutdown(1); + } } //============================================================================== diff --git a/bin/cli-plugins b/bin/cli-plugins index 1de1c97d4..b7e301fce 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -53,7 +53,7 @@ function versionMatch(name, version) { } } -const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc. +const EXTERNAL = /^\w[a-z\-0-9.]+$/; // Match "react", "path", "fs", "lodash.random", etc. function reconcilePackages({quiet = false, upgradeRemote = false}) { const fetchable = []; diff --git a/bin/cli-users b/bin/cli-users index 057c0a806..c6555ab28 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -98,39 +98,32 @@ function getUserCreateAnswers(options) { /** * Prompts for input and registers a user based on those. */ -function createUser(options) { - getUserCreateAnswers(options) - .then((answers) => { - if (answers.password !== answers.confirmPassword) { - return Promise.reject(new Error('Passwords do not match')); - } +async function createUser(options) { + try { + const answers = await getUserCreateAnswers(options); + if (answers.password !== answers.confirmPassword) { + throw new Error('Passwords do not match'); + } - return answers; - }) - .then((answers) => { - return UsersService - .createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim()) - .then((user) => { - console.log(`Created user ${user.id}.`); + const user = await UsersService.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim()); + console.log(`Created user ${user.id}.`); - if (answers.roles.length > 0) { - return Promise.all(answers.roles.map((role) => { - return UsersService - .addRoleToUser(user.id, role) - .then(() => { - console.log(`Added the role ${role} to User ${user.id}.`); - }); - })); - } - }); - }) - .then(() => { - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(); - }); + if (answers.roles.length > 0) { + return Promise.all(answers.roles.map((role) => { + return UsersService + .addRoleToUser(user.id, role) + .then(() => { + console.log(`Added the role ${role} to User ${user.id}.`); + }); + })); + } + + util.shutdown(); + + } catch (err) { + console.error(err); + util.shutdown(); + } } /** @@ -169,21 +162,21 @@ function passwd(userID) { validate: validateRequired('Confirm Password is required') } ]) - .then((answers) => { - if (answers.password !== answers.confirmPassword) { - return Promise.reject(new Error('Password mismatch')); - } + .then((answers) => { + if (answers.password !== answers.confirmPassword) { + throw new Error('Password mismatch'); + } - return UsersService.changePassword(userID, answers.password); - }) - .then(() => { - console.log('Password changed.'); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); - }); + return UsersService.changePassword(userID, answers.password); + }) + .then(() => { + console.log('Password changed.'); + util.shutdown(); + }) + .catch((err) => { + console.error(err); + util.shutdown(1); + }); } /** diff --git a/bin/verifications/database/comments.js b/bin/verifications/database/comments.js index f7dd4d397..231badabb 100644 --- a/bin/verifications/database/comments.js +++ b/bin/verifications/database/comments.js @@ -125,7 +125,7 @@ module.exports = async ({fix, limit, batch}) => { // Check that the action summaries match the cached counts. if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) { - // Batch updates for those changes. + // Batch updates for those changes. commentOperations.push({ [`action_counts.${ACTION_COUNT_FIELD}`]: count, }); diff --git a/client/.eslintrc.json b/client/.eslintrc.json index 5735a91a1..cde3942fa 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -4,6 +4,7 @@ "es6": true, "mocha": true }, + "extends": "../.eslintrc.json", "parserOptions": { "sourceType": "module", "ecmaFeatures": { diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 402a32e5d..890d834b4 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -52,16 +52,16 @@ export const newPage = () => ({ export const setRole = (id, role) => (dispatch, _, {rest}) => { return rest(`/users/${id}/role`, {method: 'POST', body: {role}}) - .then(() => { - return dispatch({type: SET_ROLE, id, role}); - }); + .then(() => { + return dispatch({type: SET_ROLE, id, role}); + }); }; export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => { return rest(`/users/${id}/status`, {method: 'POST', body: {status}}) - .then(() => { - return dispatch({type: SET_COMMENTER_STATUS, id, status}); - }); + .then(() => { + return dispatch({type: SET_COMMENTER_STATUS, id, status}); + }); }; // Ban User Dialog diff --git a/client/coral-admin/src/actions/install.js b/client/coral-admin/src/actions/install.js index d7210782a..d6b27115a 100644 --- a/client/coral-admin/src/actions/install.js +++ b/client/coral-admin/src/actions/install.js @@ -26,19 +26,19 @@ const validation = (formData, dispatch, next) => { // Required Validation const empty = validKeys - .filter((name) => { - const cond = !formData[name].length; + .filter((name) => { + const cond = !formData[name].length; - if (cond) { + if (cond) { - // Adding Error - dispatch(addError(name, 'This field is required.')); - } else { - dispatch(addError(name, '')); - } + // Adding Error + dispatch(addError(name, 'This field is required.')); + } else { + dispatch(addError(name, '')); + } - return cond; - }); + return cond; + }); if (empty.length) { dispatch(hasError()); diff --git a/client/coral-admin/src/components/AdminLogin.js b/client/coral-admin/src/components/AdminLogin.js index e75e66d23..014773a1b 100644 --- a/client/coral-admin/src/components/AdminLogin.js +++ b/client/coral-admin/src/components/AdminLogin.js @@ -69,23 +69,23 @@ class AdminLogin extends React.Component { ); const requestPasswordForm = ( this.props.passwordRequestSuccess - ?

      { - location.href = location.href; - }}> + ?

      { + location.href = location.href; + }}> {this.props.passwordRequestSuccess} Sign in

      - :
      - this.setState({email: e.target.value})} /> - - + :
      + this.setState({email: e.target.value})} /> + + ); return ( diff --git a/client/coral-admin/src/components/SuspendUserDialog.js b/client/coral-admin/src/components/SuspendUserDialog.js index d2ef23891..0ed947506 100644 --- a/client/coral-admin/src/components/SuspendUserDialog.js +++ b/client/coral-admin/src/components/SuspendUserDialog.js @@ -65,7 +65,7 @@ class SuspendUserDialog extends React.Component { {t('suspenduser.title_suspend')}

      - {t('suspenduser.description_suspend', username)} + {t('suspenduser.description_suspend', username)}

      {t('suspenduser.select_duration')} @@ -103,7 +103,7 @@ class SuspendUserDialog extends React.Component { {t('suspenduser.title_notify')}

      - {t('suspenduser.description_notify', username)} + {t('suspenduser.description_notify', username)}

      {t('suspenduser.write_message')} diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 11cd444e1..a7eadbac3 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -139,29 +139,29 @@ export default class UserDetail extends React.Component {
      { selectedCommentIds.length === 0 - ? ( -
        -
      • All
      • -
      • Rejected
      • -
      - ) - : ( -
      - - - {`${selectedCommentIds.length} comments selected`} -
      - ) + ? ( +
        +
      • All
      • +
      • Rejected
      • +
      + ) + : ( +
      + + + {`${selectedCommentIds.length} comments selected`} +
      + ) }
      @@ -190,7 +190,7 @@ export default class UserDetail extends React.Component { className={styles.loadMore} loadMore={loadMore} showLoadMore={hasNextPage} - /> + /> ); diff --git a/client/coral-admin/src/components/UserDetailComment.js b/client/coral-admin/src/components/UserDetailComment.js index dc809b9c7..4e31d338b 100644 --- a/client/coral-admin/src/components/UserDetailComment.js +++ b/client/coral-admin/src/components/UserDetailComment.js @@ -54,8 +54,8 @@ class UserDetailComment extends React.Component { { (comment.editing && comment.editing.edited) - ?  ({t('comment.edited')}) - : null + ?  ({t('comment.edited')}) + : null }
      @@ -121,10 +121,10 @@ class UserDetailComment extends React.Component {
      {flagActions && flagActions.length ? + actions={flagActions} + actionSummaries={flagActionSummaries} + viewUserDetail={viewUserDetail} + /> : null}
    • ); diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index ca1cc1f30..b43fa8b35 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -13,9 +13,9 @@ const CoralHeader = ({ }) => (
      -
      - { - auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? +
      + { + auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? : null - } -
      -
      +
      ); diff --git a/client/coral-admin/src/containers/Layout.js b/client/coral-admin/src/containers/Layout.js index 2c5e1621a..f2d71c93b 100644 --- a/client/coral-admin/src/containers/Layout.js +++ b/client/coral-admin/src/containers/Layout.js @@ -55,10 +55,10 @@ class LayoutContainer extends Component { toggleShortcutModal={toggleShortcutModal} {...this.props} > - - - - {this.props.children} + + + + {this.props.children} ); } else if (loggedIn) { diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index e07b47261..0eb81a9a1 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -88,13 +88,13 @@ class UserDetailContainer extends React.Component { }); } }) - .then(() => { - this.isLoadingMore = false; - }) - .catch((err) => { - this.isLoadingMore = false; - throw err; - }); + .then(() => { + this.isLoadingMore = false; + }) + .catch((err) => { + this.isLoadingMore = false; + throw err; + }); }; componentWillReceiveProps(next) { diff --git a/client/coral-admin/src/routes/Community/components/FlaggedAccounts.js b/client/coral-admin/src/routes/Community/components/FlaggedAccounts.js index bb8504293..826e1299a 100644 --- a/client/coral-admin/src/routes/Community/components/FlaggedAccounts.js +++ b/client/coral-admin/src/routes/Community/components/FlaggedAccounts.js @@ -14,21 +14,21 @@ const FlaggedAccounts = (props) => {
      { hasResults - ? commenters.map((commenter, index) => { - return { + return ; - }) - : {t('community.no_flagged_accounts')} + }) + : {t('community.no_flagged_accounts')} }
    diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js index 464f3bc5b..aef4d250a 100644 --- a/client/coral-admin/src/routes/Community/components/People.js +++ b/client/coral-admin/src/routes/Community/components/People.js @@ -45,12 +45,12 @@ const People = ({commenters, searchValue, onSearchChange, ...props}) => {
    { hasResults - ? - : {t('community.no_results')} + : {t('community.no_results')} } this.setState({stage: stage + 1}); const suspend = () => { rejectUsername({id: user.user.id, message: this.state.email}) - .then(() => { - this.props.handleClose(); - }); + .then(() => { + this.props.handleClose(); + }); }; const suspendModalActions = [ @@ -71,21 +71,21 @@ class RejectUsernameDialog extends Component { const {stage} = this.state; return -
    - {t(stages[stage].title, t('reject_username.username'))} -
    -
    -
    - {t(stages[stage].description, t('reject_username.username'))} -
    - { - stage === 1 && + className={styles.suspendDialog} + id="rejectUsernameDialog" + open={open} + onClose={handleClose} + onCancel={handleClose} + title={t('reject_username.suspend_user')}> +
    + {t(stages[stage].title, t('reject_username.username'))} +
    +
    +
    + {t(stages[stage].description, t('reject_username.username'))} +
    + { + stage === 1 &&
    {t('reject_username.write_message')}
    @@ -96,16 +96,16 @@ class RejectUsernameDialog extends Component { onChange={this.onEmailChange}/>
    - } -
    - {Object.keys(stages[stage].options).map((key, i) => ( - - ))} -
    -
    -
    ; + } +
    + {Object.keys(stages[stage].options).map((key, i) => ( + + ))} +
    + + ; } } diff --git a/client/coral-admin/src/routes/Community/components/Table.js b/client/coral-admin/src/routes/Community/components/Table.js index f851b1f84..c5bb494c0 100644 --- a/client/coral-admin/src/routes/Community/components/Table.js +++ b/client/coral-admin/src/routes/Community/components/Table.js @@ -9,9 +9,9 @@ export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onComm
    {headers.map((header, i) =>( ))} diff --git a/client/coral-admin/src/routes/Community/components/User.js b/client/coral-admin/src/routes/Community/components/User.js index 2d921a1c7..5225e0ee9 100644 --- a/client/coral-admin/src/routes/Community/components/User.js +++ b/client/coral-admin/src/routes/Community/components/User.js @@ -59,7 +59,7 @@ const User = (props) => {
    flag{t('community.flags')}({ user.actions.length }): - { user.action_summaries.map( + { user.action_summaries.map( (action, i) => { return {shortReasons[action.reason]} ({action.count}) diff --git a/client/coral-admin/src/routes/Community/containers/Community.js b/client/coral-admin/src/routes/Community/containers/Community.js index b04fc0281..bfb634f18 100644 --- a/client/coral-admin/src/routes/Community/containers/Community.js +++ b/client/coral-admin/src/routes/Community/containers/Community.js @@ -77,14 +77,14 @@ export const withCommunityQuery = withQuery(gql` } } `, { - options: ({params: {action_type = 'FLAG'}}) => { - return { - variables: { - action_type: action_type - } - }; - } -}); + options: ({params: {action_type = 'FLAG'}}) => { + return { + variables: { + action_type: action_type + } + }; + } + }); const mapStateToProps = (state) => ({ community: state.community, diff --git a/client/coral-admin/src/routes/Configure/components/Configure.js b/client/coral-admin/src/routes/Configure/components/Configure.js index 84e47dfb7..15ff69b69 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.js +++ b/client/coral-admin/src/routes/Configure/components/Configure.js @@ -111,20 +111,20 @@ export default class Configure extends Component { (bool, error) => this.state.errors[error] ? false : bool, this.state.changed); return ( -
    -
    - - - {t('configure.stream_settings')} - - - {t('configure.moderation_settings')} - - - {t('configure.tech_settings')} - - -
    +
    +
    + + + {t('configure.stream_settings')} + + + {t('configure.moderation_settings')} + + + {t('configure.tech_settings')} + + +
    { showSave ? - : + : + {t('configure.save_changes')} + } -
    +
    -
    -
    - { this.props.saveFetchingError } - { this.props.fetchSettingsError } - { section } -
    +
    + { this.props.saveFetchingError } + { this.props.fetchSettingsError } + { section } +
    +
    ); } } diff --git a/client/coral-admin/src/routes/Configure/components/StreamSettings.js b/client/coral-admin/src/routes/Configure/components/StreamSettings.js index 9c216a853..1427f949e 100644 --- a/client/coral-admin/src/routes/Configure/components/StreamSettings.js +++ b/client/coral-admin/src/routes/Configure/components/StreamSettings.js @@ -93,15 +93,15 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => { value={settings.charCount} disabled={settings.charCountEnable ? '' : 'disabled'} /> - {t('configure.comment_count_text_post')} - { - errors.charCount && + {t('configure.comment_count_text_post')} + { + errors.charCount &&
    {t('configure.comment_count_error')}
    - } + }

    diff --git a/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js b/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js index d92b2bd14..5b43a605c 100644 --- a/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js +++ b/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js @@ -14,19 +14,19 @@ const ActivityWidget = ({assets}) => {
    { assets.length - ? assets.map((asset) => { - return ( -
    - Moderate -

    {asset.commentCount}

    - -

    {asset.title}

    -
    -

    {asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

    -
    - ); - }) - :
    {t('dashboard.no_activity')}
    + ? assets.map((asset) => { + return ( +
    + Moderate +

    {asset.commentCount}

    + +

    {asset.title}

    +
    +

    {asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

    +
    + ); + }) + :
    {t('dashboard.no_activity')}
    }
    diff --git a/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js b/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js index ef4a6857e..5dfbfb3ac 100644 --- a/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js +++ b/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js @@ -16,24 +16,24 @@ const FlagWidget = ({assets}) => {
    { assets.length - ? assets.map((asset) => { - let flagSummary = null; - if (asset.action_summaries) { - flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary'); - } + ? assets.map((asset) => { + let flagSummary = null; + if (asset.action_summaries) { + flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary'); + } - return ( -
    - Moderate -

    {flagSummary ? flagSummary.actionCount : 0}

    - -

    {asset.title}

    -
    -

    {asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

    -
    - ); - }) - :
    {t('dashboard.no_flags')}
    + return ( +
    + Moderate +

    {flagSummary ? flagSummary.actionCount : 0}

    + +

    {asset.title}

    +
    +

    {asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

    +
    + ); + }) + :
    {t('dashboard.no_flags')}
    }
    diff --git a/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js b/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js index a61f888bf..aeb31c318 100644 --- a/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js +++ b/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js @@ -15,20 +15,20 @@ const LikeWidget = ({assets}) => {
    { assets.length - ? assets.map((asset) => { - const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary'); - return ( -
    - Moderate -

    {likeSummary ? likeSummary.actionCount : 0}

    - -

    {asset.title}

    -
    -

    {asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

    -
    - ); - }) - :
    {t('dashboard.no_likes')}
    + ? assets.map((asset) => { + const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary'); + return ( +
    + Moderate +

    {likeSummary ? likeSummary.actionCount : 0}

    + +

    {asset.title}

    +
    +

    {asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}

    +
    + ); + }) + :
    {t('dashboard.no_likes')}
    }
    diff --git a/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js b/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js index fe3680c76..2bb192f2e 100644 --- a/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js +++ b/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js @@ -42,15 +42,15 @@ export const witDashboardQuery = withQuery(gql` } } `, { - options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => { - return { - variables: { - from: dashboardWindowStart, - to: dashboardWindowEnd - } - }; - } -}); + options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => { + return { + variables: { + from: dashboardWindowStart, + to: dashboardWindowEnd + } + }; + } + }); const mapStateToProps = (state) => { return { diff --git a/client/coral-admin/src/routes/Install/components/Steps/CreateYourAccount.js b/client/coral-admin/src/routes/Install/components/Steps/CreateYourAccount.js index 2a5b87e5f..d4d43dc92 100644 --- a/client/coral-admin/src/routes/Install/components/Steps/CreateYourAccount.js +++ b/client/coral-admin/src/routes/Install/components/Steps/CreateYourAccount.js @@ -19,7 +19,7 @@ const InitialStep = (props) => { showErrors={install.showErrors} errorMsg={install.errors.email} noValidate - /> + /> { onChange={handleUserChange} showErrors={install.showErrors} errorMsg={install.errors.username} - /> + /> { onChange={handleUserChange} showErrors={install.showErrors} errorMsg={install.errors.password} - /> + /> { onChange={handleUserChange} showErrors={install.showErrors} errorMsg={install.errors.confirmPassword} - /> + /> { !props.install.isLoading ? - - : - + + : + } {props.install.installRequest === 'FAILURE' &&
    Error: {props.install.installRequestError}
    } diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index c73d15eaa..0fa2ba8bb 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -91,8 +91,8 @@ class Comment extends React.Component { { (comment.editing && comment.editing.edited) - ?  ({t('comment.edited')}) - : null + ?  ({t('comment.edited')}) + : null } {currentUserId !== comment.user.id && @@ -192,10 +192,10 @@ class Comment extends React.Component { /> {flagActions && flagActions.length ? + actions={flagActions} + actionSummaries={flagActionSummaries} + viewUserDetail={viewUserDetail} + /> : null} ); diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js index 7f96c7c90..e8b5b8191 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js @@ -97,7 +97,7 @@ class ModerationQueue extends React.Component { rejectComment={props.rejectComment} currentAsset={props.currentAsset} currentUserId={this.props.currentUserId} - />; + />; }) } @@ -110,7 +110,7 @@ class ModerationQueue extends React.Component { + /> ); } diff --git a/client/coral-admin/src/routes/Moderation/components/StorySearch.js b/client/coral-admin/src/routes/Moderation/components/StorySearch.js index f19560549..748f80b0f 100644 --- a/client/coral-admin/src/routes/Moderation/components/StorySearch.js +++ b/client/coral-admin/src/routes/Moderation/components/StorySearch.js @@ -61,20 +61,20 @@ const StorySearch = (props) => { { loading - ? - : assets.map((story, i) => { - const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date(); + ? + : assets.map((story, i) => { + const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date(); - return ; - }) + return ; + }) } {assets.length === 0 &&
    No results
    } diff --git a/client/coral-admin/src/routes/Moderation/containers/StorySearch.js b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js index 4175479e6..04472d219 100644 --- a/client/coral-admin/src/routes/Moderation/containers/StorySearch.js +++ b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js @@ -98,14 +98,14 @@ export const withAssetSearchQuery = withQuery(gql` } } `, { - options: ({moderation: {storySearchString = ''}}) => { - return { - variables: { - value: storySearchString - } - }; - } -}); + options: ({moderation: {storySearchString = ''}}) => { + return { + variables: { + value: storySearchString + } + }; + } + }); export default compose( withRouter, diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index 7f9098fba..5b931e6fc 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -134,20 +134,20 @@ export default class Stories extends Component { {t('streams.closed')}
    {t('streams.sort_by')}
    - - {t('streams.newest')} - {t('streams.oldest')} - - + + {t('streams.newest')} + {t('streams.oldest')} + + { assetsIds.length - ?
    + ?
    {t('streams.article')} @@ -162,7 +162,7 @@ export default class Stories extends Component { page={this.state.page} onNewPageHandler={this.onPageClick} />
    - : {t('streams.empty_result')} + : {t('streams.empty_result')} }
    ); diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js index 726b54e19..ec5a43c15 100644 --- a/client/coral-configure/components/ConfigureCommentStream.js +++ b/client/coral-configure/components/ConfigureCommentStream.js @@ -56,13 +56,13 @@ export default ({handleChange, handleApply, changed, ...props}) => ( title: t('configure.enable_questionbox'), description: t('configure.enable_questionbox_description') }} /> - { - props.questionBoxEnable && - } + { + props.questionBoxEnable && + } diff --git a/client/coral-configure/components/QuestionBoxBuilder.js b/client/coral-configure/components/QuestionBoxBuilder.js index 8ccc6ba31..285df40ed 100644 --- a/client/coral-configure/components/QuestionBoxBuilder.js +++ b/client/coral-configure/components/QuestionBoxBuilder.js @@ -41,39 +41,39 @@ class QuestionBoxBuilder extends React.Component {
    • + styles.qbItemIcon, + {[styles.qbItemIconActive]: questionBoxIcon === 'default'} + )} + id="qboxicon" + onClick={handleChange} + data-icon="default" >
    • + styles.qbItemIcon, + {[styles.qbItemIconActive]: questionBoxIcon === 'forum'} + )} + id="qboxicon" + onClick={handleChange} + data-icon="forum" >
    • + styles.qbItemIcon, + {[styles.qbItemIconActive]: questionBoxIcon === 'build'} + )} + id="qboxicon" + onClick={handleChange} + data-icon="build" >
    • + styles.qbItemIcon, + {[styles.qbItemIconActive]: questionBoxIcon === 'format_quote'} + )} + id="qboxicon" + onClick={handleChange} + data-icon="format_quote" >
    diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index 2d4a16795..c009bbb76 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -115,7 +115,7 @@ class ConfigureStreamContainer extends Component { />

    {closedAt === 'open' ? t('configure.close') : t('configure.open')} {t('configure.comment_stream')}

    - {(closedAt === 'open' && closedTimeout) ?

    {t('configure.comment_stream_will_close')} {this.getClosedIn()}.

    : ''} + {(closedAt === 'open' && closedTimeout) ?

    {t('configure.comment_stream_will_close')} {this.getClosedIn()}.

    : ''} : ; + commentClassNames={commentClassNames} + data={data} + root={root} + disableReply={disableReply} + setActiveReplyBox={setActiveReplyBox} + activeReplyBox={activeReplyBox} + notify={notify} + depth={0} + postComment={postComment} + asset={asset} + currentUser={currentUser} + postFlag={postFlag} + postDontAgree={postDontAgree} + ignoreUser={ignoreUser} + commentIsIgnored={commentIsIgnored} + loadMore={loadNewReplies} + deleteAction={deleteAction} + showSignInDialog={showSignInDialog} + key={comment.id} + comment={comment} + charCountEnable={charCountEnable} + maxCharCount={maxCharCount} + editComment={editComment} + emit={emit} + />; })} { (comment.editing && comment.editing.edited) - ?  ({t('comment.edited')}) - : null + ?  ({t('comment.edited')}) + : null } @@ -492,7 +492,7 @@ export default class Comment extends React.Component {
    { this.state.isEditing - ? - :
    - + :
    +
    }
    @@ -562,23 +562,23 @@ export default class Comment extends React.Component { {activeReplyBox === comment.id ? + commentPostedHandler={this.commentPostedHandler} + charCountEnable={charCountEnable} + maxCharCount={maxCharCount} + setActiveReplyBox={setActiveReplyBox} + parentId={(depth < THREADING_LEVEL) ? comment.id : parentId} + notify={notify} + postComment={postComment} + currentUser={currentUser} + assetId={asset.id} + /> : null} - {view.map((reply) => { - return commentIsIgnored(reply) - ? - : { + return commentIsIgnored(reply) + ? + : ; - })} + })}
    { this.isEditWindowExpired() - ? + ? {t('edit_comment.edit_window_expired')} { typeof this.props.stopEditing === 'function' - ?  {t('edit_comment.edit_window_expired_close')} - : null + ?  {t('edit_comment.edit_window_expired_close')} + : null } - : + : {t('edit_comment.edit_window_timer_prefix')} { return
    { count ? - - : null + + : null }
    ; }; diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 6959ff662..62c43813b 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -257,16 +257,16 @@ class Stream extends React.Component { {open ?
    - - - {!banned && + + + {!banned && temporarilySuspended && {t( @@ -275,13 +275,13 @@ class Stream extends React.Component { timeago(user.suspension.until) )} } - {banned && + {banned && } - {showCommentBox && + {showCommentBox && } -
    +
    :

    {asset.settings.closedMessage}

    } { - canEditName ? + canEditName ? t('framework.edit_name.msg') : {t('framework.banned_account_header')}
    {t('framework.banned_account_body')}
    - }
    + } { canEditName ? -
    -
    - {alert} -
    - - this.setState({username: e.target.value})} - rows={3}/>
    - -
    : null +
    +
    + {alert} +
    + + this.setState({username: e.target.value})} + rows={3}/>
    + +
    : null } ; } diff --git a/client/coral-embed-stream/src/components/TopRightMenu.js b/client/coral-embed-stream/src/components/TopRightMenu.js index 7ae123b90..47147e853 100644 --- a/client/coral-embed-stream/src/components/TopRightMenu.js +++ b/client/coral-embed-stream/src/components/TopRightMenu.js @@ -51,7 +51,7 @@ export class TopRightMenu extends React.Component { user={comment.user} cancel={reset} ignoreUser={ignoreUserAndCloseMenuAndNotifyOnError} - /> + />
    ); diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js index ac13382bc..0c46b45e3 100644 --- a/client/coral-embed-stream/src/graphql/utils.js +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -60,7 +60,7 @@ function findAndInsertComment(parent, comment) { [connectionField]: { nodes: { $apply: (nodes) => - nodes.map((node) => findAndInsertComment(node, comment)) + nodes.map((node) => findAndInsertComment(node, comment)) }, }, }); @@ -176,7 +176,7 @@ function findAndInsertFetchedComments(parent, comments, parent_id) { [connectionField]: { nodes: { $apply: (nodes) => - nodes.map((node) => findAndInsertFetchedComments(node, comments, parent_id)) + nodes.map((node) => findAndInsertFetchedComments(node, comments, parent_id)) }, }, }); diff --git a/client/coral-framework/helpers/validate.js b/client/coral-framework/helpers/validate.js index dd36ae07f..6a8d37f53 100644 --- a/client/coral-framework/helpers/validate.js +++ b/client/coral-framework/helpers/validate.js @@ -1,5 +1,5 @@ export default { - email: (email) => (/^([A-Za-z0-9_\-\.\+])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)), + email: (email) => (/^([A-Za-z0-9_\-.+])+@([A-Za-z0-9_\-.])+\.([A-Za-z]{2,4})$/.test(email)), password: (pass) => (/^(?=.{8,}).*$/.test(pass)), confirmPassword: () => true, username: (username) => (/^[a-zA-Z0-9_]+$/.test(username)), diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js index 46ec19eb7..f16809322 100644 --- a/client/coral-framework/hocs/withMutation.js +++ b/client/coral-framework/hocs/withMutation.js @@ -84,8 +84,8 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { const updateCallbacks = [base.update || config.options.update] - .concat(...configs.map((cfg) => cfg.update)) - .filter((i) => i); + .concat(...configs.map((cfg) => cfg.update)) + .filter((i) => i); const update = (proxy, result) => { if (getResponseErrors(result)) { @@ -101,28 +101,28 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { base.updateQueries || config.options.updateQueries, ...configs.map((cfg) => cfg.updateQueries) ] - .filter((i) => i) - .reduce((res, map) => { - Object.keys(map).forEach((key) => { - if (!(key in res)) { - res[key] = (prev, result) => { - if (getResponseErrors(result.mutationResult)) { + .filter((i) => i) + .reduce((res, map) => { + Object.keys(map).forEach((key) => { + if (!(key in res)) { + res[key] = (prev, result) => { + if (getResponseErrors(result.mutationResult)) { // Do not run updates when we have mutation errors. - return prev; - } - return map[key](prev, result) || prev; - }; - } else { - const existing = res[key]; - res[key] = (prev, result) => { - const next = existing(prev, result); - return map[key](next, result) || next; - }; - } - }); - return res; - }, {}); + return prev; + } + return map[key](prev, result) || prev; + }; + } else { + const existing = res[key]; + res[key] = (prev, result) => { + const next = existing(prev, result); + return map[key](next, result) || next; + }; + } + }); + return res; + }, {}); const wrappedConfig = { variables, diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index c0825f28b..101dc45a9 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -129,18 +129,18 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { ...lmArgs, query: resolvedDocument, }) - .then((res) => { - this.context.eventEmitter.emit( - `query.${name}.fetchMore.${fetchName}.success`, - {variables: lmArgs.variables, data: res.data}); - return Promise.resolve(res); - }) - .catch((err) => { - this.context.eventEmitter.emit( - `query.${name}.fetchMore.${fetchName}.error`, - {variables: lmArgs.variables, error: err}); - throw err; - }); + .then((res) => { + this.context.eventEmitter.emit( + `query.${name}.fetchMore.${fetchName}.success`, + {variables: lmArgs.variables, data: res.data}); + return Promise.resolve(res); + }) + .catch((err) => { + this.context.eventEmitter.emit( + `query.${name}.fetchMore.${fetchName}.error`, + {variables: lmArgs.variables, error: err}); + throw err; + }); }, }; } @@ -171,8 +171,8 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { const configs = this.graphqlRegistry.getQueryOptions(name); const reducerCallbacks = [base.reducer || ((i) => i)] - .concat(...configs.map((cfg) => cfg.reducer)) - .filter((i) => i); + .concat(...configs.map((cfg) => cfg.reducer)) + .filter((i) => i); const reducer = withSkipOnErrors( reducerCallbacks.reduce( diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js index 1aae0c662..301428c12 100644 --- a/client/coral-framework/services/plugins.js +++ b/client/coral-framework/services/plugins.js @@ -81,11 +81,11 @@ class PluginsService { const pluginConfig = reduxState.config.plugin_config || emptyConfig; return flatten(this.plugins - // Filter out components that have slots and have been disabled in `plugin_config` - .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components)) + // Filter out components that have slots and have been disabled in `plugin_config` + .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components)) - .filter((o) => o.module.slots[slot]) - .map((o) => o.module.slots[slot]) + .filter((o) => o.module.slots[slot]) + .map((o) => o.module.slots[slot]) ) .filter((component) => { if(!component.isExcluded) { @@ -114,8 +114,8 @@ class PluginsService { config: pluginConfig, ...( component.fragments - ? pick(queryData, Object.keys(component.fragments)) - : withWarnings(component, queryData) + ? pick(queryData, Object.keys(component.fragments)) + : withWarnings(component, queryData) ) }; } diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 533ed85db..93ae55d7a 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -26,7 +26,7 @@ export const getMyActionSummary = (type, comment) => { .find((a) => a.current_user); }; - /** +/** * getActionSummary * retrieves the action summaries based on the type and the comment * array could be length > 1, as in the case of FlagActionSummary diff --git a/client/coral-framework/utils/user.js b/client/coral-framework/utils/user.js index 044583026..ccdf8ba42 100644 --- a/client/coral-framework/utils/user.js +++ b/client/coral-framework/utils/user.js @@ -1,4 +1,4 @@ - /** +/** * getReliability * retrieves reliability value as string */ diff --git a/client/coral-settings/components/IgnoredUsers.js b/client/coral-settings/components/IgnoredUsers.js index 32e485e4c..a8723161f 100644 --- a/client/coral-settings/components/IgnoredUsers.js +++ b/client/coral-settings/components/IgnoredUsers.js @@ -2,7 +2,7 @@ import React, {Component, PropTypes} from 'react'; import t from 'coral-framework/services/i18n'; import styles from './IgnoredUsers.css'; -export class IgnoredUsers extends Component { +class IgnoredUsers extends Component { static propTypes = { users: PropTypes.arrayOf(PropTypes.shape({ username: PropTypes.string, diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index f5ea64005..c5bfa257c 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -79,12 +79,12 @@ class ProfileContainer extends Component { {me.ignoredUsers && me.ignoredUsers.length ?
    -

    {t('framework.ignored_users')}

    - -
    +

    {t('framework.ignored_users')}

    + + : null}
    diff --git a/client/coral-ui/components/CoralLogo.js b/client/coral-ui/components/CoralLogo.js index 002b0cabc..91331523f 100644 --- a/client/coral-ui/components/CoralLogo.js +++ b/client/coral-ui/components/CoralLogo.js @@ -3,34 +3,34 @@ import styles from './CoralLogo.css'; const CoralLogo = ({className = ''}) => ( - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - + ); CoralLogo.propTypes = { diff --git a/client/coral-ui/components/Dialog.js b/client/coral-ui/components/Dialog.js index 85ab570d3..6e2489fea 100644 --- a/client/coral-ui/components/Dialog.js +++ b/client/coral-ui/components/Dialog.js @@ -56,7 +56,7 @@ export default class Dialog extends Component { className={`mdl-dialog ${className}`} {...rest} > - {children} + {children} ); } diff --git a/client/coral-ui/components/Pager.js b/client/coral-ui/components/Pager.js index ffd1b9d0a..d14f2fc60 100644 --- a/client/coral-ui/components/Pager.js +++ b/client/coral-ui/components/Pager.js @@ -3,7 +3,7 @@ import styles from './Pager.css'; const Rows = (curr, total, onClickHandler) => Array.from(Array(total)).map((e, i) =>
  • onClickHandler(i + 1)}> + key={i} onClick={() => onClickHandler(i + 1)}> {i + 1}
  • ); @@ -17,18 +17,18 @@ const Pager = ({totalPages, page, onNewPageHandler}) => ( onClick={() => onNewPageHandler(page - 1)}> Prev - : + : null } {Rows(page, totalPages, onNewPageHandler)} { (page < totalPages && totalPages > 1) ? -
  • onNewPageHandler(page + 1)}> +
  • onNewPageHandler(page + 1)}> Next -
  • - : - null + + : + null } diff --git a/client/coral-ui/components/Wizard.js b/client/coral-ui/components/Wizard.js index eefa2a0b1..bc87a8bcc 100644 --- a/client/coral-ui/components/Wizard.js +++ b/client/coral-ui/components/Wizard.js @@ -7,12 +7,12 @@ const Wizard = (props) => { {React.Children.toArray(children) .filter((child, i) => i === currentStep) .map((child, i) => - React.cloneElement(child, { - i, - currentStep, - ...rest - }) - )} + React.cloneElement(child, { + i, + currentStep, + ...rest + }) + )} ); }; diff --git a/client/talk-plugin-flags/components/FlagButton.js b/client/talk-plugin-flags/components/FlagButton.js index bf3da6f01..a225bda41 100644 --- a/client/talk-plugin-flags/components/FlagButton.js +++ b/client/talk-plugin-flags/components/FlagButton.js @@ -93,18 +93,24 @@ export default class FlagButton extends Component { }; if (reason === 'COMMENT_NOAGREE') { postDontAgree(action) - .then(({data}) => { - if (itemType === 'COMMENTS') { - this.setState({localPost: data.createDontAgree.dontagree.id}); - } - }); + .then(({data}) => { + if (itemType === 'COMMENTS') { + this.setState({localPost: data.createDontAgree.dontagree.id}); + } + }) + .catch((err) => { + console.error(err); + }); } else { postFlag({...action, reason}) - .then(({data}) => { - if (itemType === 'COMMENTS') { - this.setState({localPost: data.createFlag.flag.id}); - } - }); + .then(({data}) => { + if (itemType === 'COMMENTS') { + this.setState({localPost: data.createFlag.flag.id}); + } + }) + .catch((err) => { + console.error(err); + }); } } } @@ -153,15 +159,15 @@ export default class FlagButton extends Component { className={cn(`${name}-button`, {[`${name}-button-flagged`]: flagged}, styles.button)}> { flagged - ? {t('reported')} - : {t('report')} + ? {t('reported')} + : {t('report')} } flag + aria-hidden={true}>flag { this.state.showMenu && @@ -190,10 +196,10 @@ export default class FlagButton extends Component { } { this.state.reason &&
    -
    -
    onHeaderClickHandler({field: header.field})}> + key={i} + className="mdl-data-table__cell--non-numeric" + onClick={() => onHeaderClickHandler({field: header.field})}> {header.title}