From b126cc3cb3fc5c73dc19b52ba8e5ddca0457c4f1 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 16 Aug 2017 15:47:56 -0600 Subject: [PATCH 01/73] 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 02/73] 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 03/73] 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 04/73] 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 05/73] 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 06/73] 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 07/73] 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 08/73] 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 09/73] 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 10/73] 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 11/73] 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 12/73] 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 84e4091ea6eea48ccf0e38a0f53399ba65bf5b2b Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 21 Aug 2017 20:06:53 +0700 Subject: [PATCH 13/73] 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 14/73] 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 15/73] 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 16/73] 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 b3e15de9f3cf4835026206780077287f281c8d26 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 21 Aug 2017 10:40:18 -0600 Subject: [PATCH 17/73] 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 18/73] 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 15111c81a278fdd58f37009476c6a6471eed5bfa Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 21 Aug 2017 11:32:16 -0600 Subject: [PATCH 19/73] 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 01ed9880f4e6ddd4b69c19d7d7b7ad5172848b51 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 21 Aug 2017 15:26:04 -0600 Subject: [PATCH 20/73] 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 21/73] 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 22/73] 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 23/73] 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 54ea0b1a3fbf9be4ca695cf3eb0d4dcc602713d3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 22 Aug 2017 21:59:41 +0700 Subject: [PATCH 24/73] 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 25/73] 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 26/73] 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 27/73] 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 28/73] 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 a882b3650103785991f30d82512a0cc21f555253 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 02:20:43 +0700 Subject: [PATCH 29/73] 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 30/73] 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 31/73] 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 f428bfa6c9cfc1aab7bfb46f4db6ac42d241170d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 03:16:19 +0700 Subject: [PATCH 32/73] 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 33/73] 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 34/73] 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 35/73] 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 0811c9302d375762f0691ae682cdf1b4146296e2 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 17:30:46 +0700 Subject: [PATCH 36/73] 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 37/73] 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 38/73] 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 acf792089deb44b870a8ab12faa56432a9743c80 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 23 Aug 2017 15:42:30 -0600 Subject: [PATCH 39/73] 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 40/73] 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 41/73] 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 42/73] 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 43/73] 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 44/73] 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 45/73] 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 7991188f8d2f59160e9fecc43678eb62f518b312 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 24 Aug 2017 14:45:47 -0300 Subject: [PATCH 46/73] 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 5cb71eaee8b3411ad3cf6ea6314300d29cfe0161 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 24 Aug 2017 16:32:07 -0300 Subject: [PATCH 47/73] 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 48/73] 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 49/73] 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 50/73] 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 51/73] 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 52/73] 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 53/73] 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 54/73] 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 55/73] 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 9e2f6f1f63922239f2a6fcc003a24659bc00d798 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 25 Aug 2017 19:40:55 +0700 Subject: [PATCH 56/73] 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 59ddcea59216b6072def5af350739dcf3e7a17f0 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Fri, 25 Aug 2017 13:55:19 +0100 Subject: [PATCH 57/73] 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 e51e8c543f9243f0da463bc84d7d2b7d80d7d483 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 25 Aug 2017 20:15:07 +0700 Subject: [PATCH 58/73] 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 6b96fcfad1e9a27d750a4ec61277c8e1c1e7445f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 25 Aug 2017 21:05:30 +0700 Subject: [PATCH 59/73] 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 60/73] 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 61/73] 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 62/73] 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 63/73] 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 64/73] 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 65/73] 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 66/73] 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 67/73] 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 68/73] 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 7856c9d9479d94ad69a2bb980d96330005036a77 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 28 Aug 2017 21:05:32 +0700 Subject: [PATCH 69/73] 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 70/73] 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 8cfccb7b00df7d9658a4a22f90f99f6ff04d1378 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 28 Aug 2017 09:42:37 -0600 Subject: [PATCH 71/73] 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 72/73] 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 73/73] 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); }