diff --git a/.dockerignore b/.dockerignore index 80bb20e2c..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. @@ -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..e09212b62 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,5 @@ dist docs -client/lib **/*.html plugins/* !plugins/talk-plugin-facebook-auth @@ -14,5 +13,11 @@ 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-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 f5482fd36..3fdb05c89 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ client/coral-framework/graphql/introspection.json *.swp *.DS_STORE -test/e2e/reports coverage/ plugins.json @@ -30,5 +29,11 @@ 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-replied +!plugins/talk-plugin-sort-most-liked +!plugins/talk-plugin-sort-most-loved +!plugins/talk-plugin-sort-most-respected **/node_modules/* diff --git a/Dockerfile b/Dockerfile index 2dbd7bbbb..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 @@ -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 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..6e67e0016 --- /dev/null +++ b/bin/cli-verify @@ -0,0 +1,49 @@ +#!/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({fix = false, limit = Infinity, batch = 1000}) { + try { + for (const verification of databaseVerifications) { + await verification({fix, limit, batch}); + } + } catch (err) { + console.error(`Failed to process all the ${databaseVerifications.length} verifications`, err); + util.shutdown(1); + return; + } + + util.shutdown(); +} + +//============================================================================== +// Setting up the program command line arguments. +//============================================================================== + +program + .command('db') + .description('verifies the database integrity') + .option('-f, --fix', 'fix the problems found with database inconsistencies') + .option('-l, --limit [size]', 'limit the amount of documents to process in a single pass, this will ensure only a maximum number of batch operations are issued [default: inf]', parseInt) + .option('-b, --batch [size]', 'batch size to process verifications and repairs of documents [default: 1000]', parseInt) + .action(database); + +program.parse(process.argv); + +// If there is no command listed, output help. +if (!process.argv.slice(2).length) { + program.outputHelp(); + util.shutdown(); +} diff --git a/bin/verifications/database/comments.js b/bin/verifications/database/comments.js new file mode 100644 index 000000000..f7dd4d397 --- /dev/null +++ b/bin/verifications/database/comments.js @@ -0,0 +1,193 @@ +const CommentModel = require('../../../models/comment'); +const ActionsService = require('../../../services/actions'); +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, 'reply_count': 1}) + .limit(limit) + .skip(offset) + .sort('created_at'); + +module.exports = async ({fix, limit, batch}) => { + let operations = []; + + // Count how many comments there are to process. + const totalCount = await CommentModel.count(); + + let offset = 0; + let comments = []; + let commentIDs = []; + + console.log(`Processing ${totalCount} comments in batches of ${limit}...`); + + // Keep processing documents until there are is none left. + while (offset < totalCount) { + + // Get a batch of comments. + comments = await getBatch(batch, offset); + commentIDs = comments.map(({id}) => id); + + // Get their reply counts. + let allReplyCounts = await CommentModel + .aggregate([ + { + $match: { + parent_id: { + $in: commentIDs, + }, + status: { + $in: ['NONE', 'ACCEPTED'] + } + } + }, + { + $group: { + _id: '$parent_id', + count: { + $sum: 1 + } + } + } + ]) + .then(singleJoinBy(commentIDs, '_id')) + .then((results) => results.map((result) => result ? result.count : 0)); + + // Get their action summaries. + let allActionSummaries = await ActionsService + .getActionSummaries(commentIDs) + .then(arrayJoinBy(commentIDs, 'item_id')); + + // Loop over the comments, with their action summaries. + for (let i = 0; i < comments.length; i++) { + let comment = comments[i]; + let actionSummaries = allActionSummaries[i]; + let replyCount = allReplyCounts[i]; + + // And check to see if the action summaries we just computed match what is + // 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()); + + if (GROUP_ID.length <= 0) { + continue; + } + + // And we add a new batch operation if the action summary is associated + // with a group. + const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`; + + // Check that the action summaries match the cached counts. + if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== actionSummary.count) { + + // Batch updates for those changes. + commentOperations.push({ + [`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count, + }); + } + } + + // Group all the action summaries together from all the different group + // ids. + let groupedActionSummaries = actionSummaries.reduce((acc, actionSummary) => { + const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase()); + + if (!(ACTION_TYPE in acc)) { + acc[ACTION_TYPE] = 0; + } + + acc[ACTION_TYPE] += actionSummary.count; + + return acc; + }, {}); + + for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) { + const count = groupedActionSummaries[ACTION_COUNT_FIELD]; + + // Check that the action summaries match the cached counts. + if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) { + + // Batch updates for those changes. + commentOperations.push({ + [`action_counts.${ACTION_COUNT_FIELD}`]: count, + }); + } + } + + // If this comment has action summaries that should be updated, then + // perform an update! + if (commentOperations.length > 0) { + operations.push({ + updateOne: { + filter: { + id: comment.id + }, + update: { + $set: Object.assign({}, ...commentOperations), + }, + }, + }); + } + } + + debug(`Processed batch of ${comments.length} comments.`); + + if (operations.length >= limit) { + debug(`Queued operations are ${operations.length}, reached limit of ${limit}, not processing any more.`); + + if (operations.length > limit) { + debug(`${operations.length - limit} operations have been truncated to enforce the limit`); + } + + break; + } + + offset += batch; + } + + const OPERATIONS_LENGTH = operations.length; + + if (limit < Infinity && offset + comments.length < totalCount) { + console.log(`Processed ${offset + comments.length}/${totalCount} comments because we reached the update limit of ${limit}.`); + } else { + console.log(`Processed all ${totalCount} comments.`); + } + + console.log(`${OPERATIONS_LENGTH} documents need fixing.`); + + // If fix was enabled, execute the batch writes. + if (OPERATIONS_LENGTH > 0) { + if (fix) { + debug(`Fixing ${OPERATIONS_LENGTH} documents...`); + + while (operations.length) { + let batchOperations = operations.splice(0, batch); + let result = await CommentModel.collection.bulkWrite(batchOperations); + + debug(`Fixed batch of ${result.modifiedCount} documents.`); + } + + console.log(`Applied all ${OPERATIONS_LENGTH} fixes.`); + } else { + console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors'); + } + } +}; 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/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/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index 01b28f200..e07b47261 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 3a4a3f5e9..43ae81573 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: 'show', - sortOrder: 'REVERSE_CHRONOLOGICAL', + sortOrder: 'DESC', }; export default function moderation (state = initialState, action) { 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/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 0b9fc2f38..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: Date, $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 2039fb0e5..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 === 'CHRONOLOGICAL' + 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 === 'CHRONOLOGICAL' ? 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/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js index 75f353b19..8326d8753 100644 --- a/client/coral-embed-stream/src/actions/stream.js +++ b/client/coral-embed-stream/src/actions/stream.js @@ -4,6 +4,8 @@ import queryString from 'query-string'; export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id}); +export const setSort = ({sortOrder, sortBy}) => ({type: actions.SET_SORT, sortOrder, sortBy}); + export const viewAllComments = () => (dispatch, _, {pym}) => { 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 c75c31296..6959ff662 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, notify, 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, + notify, + 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, + notify, + 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/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index e34218883..6ddba46f8 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -105,7 +105,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/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 71783e64d..cd374291d 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -154,7 +154,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!, + $sortOrder: SORT_ORDER!, + ) { me { id status @@ -166,13 +174,15 @@ const EMBED_QUERY = gql` `; export const withEmbedQuery = withQuery(EMBED_QUERY, { - options: ({auth, commentId, assetId, assetUrl}) => ({ + options: ({auth, commentId, assetId, assetUrl, sortBy, sortOrder}) => ({ variables: { assetId, assetUrl, commentId, hasComment: commentId !== '', excludeIgnored: Boolean(auth && auth.user && auth.user.id), + sortBy, + sortOrder, }, }), }); @@ -183,7 +193,9 @@ const mapStateToProps = (state) => ({ assetId: state.stream.assetId, assetUrl: state.stream.assetUrl, activeTab: state.embed.activeTab, - config: state.config + config: state.config, + sortOrder: state.stream.sortOrder, + 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 43be9018c..271421dea 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 {notify} = 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) => { @@ -102,7 +113,7 @@ class StreamContainer extends React.Component { cursor: comment.replies.endCursor, parent_id, asset_id: this.props.root.asset.id, - sort: 'CHRONOLOGICAL', + sortOrder: 'ASC', excludeIgnored: this.props.data.variables.excludeIgnored, }, updateQuery: (prev, {fetchMoreResult:{comments}}) => { @@ -119,7 +130,8 @@ 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', + sortOrder: this.props.data.variables.sortOrder, + sortBy: this.props.data.variables.sortBy, excludeIgnored: this.props.data.variables.excludeIgnored, }, updateQuery: (prev, {fetchMoreResult:{comments}}) => { @@ -128,36 +140,78 @@ 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) { + 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 (!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) { 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 ; } } @@ -203,8 +257,26 @@ 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) { - 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 + $sortOrder: SORT_ORDER + $sortBy: SORT_COMMENTS_BY = CREATED_AT + $excludeIgnored: Boolean + ) { + comments( + query: { + limit: $limit + cursor: $cursor + parent_id: $parent_id + asset_id: $asset_id + sortOrder: $sortOrder + sortBy: $sortBy + excludeIgnored: $excludeIgnored + } + ) { nodes { ...CoralEmbedStream_Stream_comment } @@ -219,6 +291,7 @@ const LOAD_MORE_QUERY = gql` const slots = [ 'streamTabs', 'streamTabPanes', + 'streamFilter', ]; const fragments = { @@ -254,9 +327,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, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) { nodes { ...CoralEmbedStream_Stream_comment } @@ -299,6 +372,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/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js index 712506692..ac13382bc 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; @@ -163,14 +163,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)}, }, }); } @@ -202,7 +195,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-embed-stream/src/reducers/stream.js b/client/coral-embed-stream/src/reducers/stream.js index 020ca654f..8e21e23b0 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', + sortOrder: 'DESC', }; export default function stream(state = initialState, action) { @@ -66,6 +68,12 @@ export default function stream(state = initialState, action) { ...state.commentClassNames.slice(action.idx + 1) ] }; + case actions.SET_SORT : + return { + ...state, + sortOrder: action.sortOrder ? action.sortOrder : state.sortOrder, + sortBy: action.sortBy ? action.sortBy : state.sortBy, + }; default: return state; } diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index dad5d5cf2..06f1d4cb7 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -29,7 +29,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; } @@ -39,8 +50,16 @@ class Slot extends React.Component { } render() { + const { + inline = false, + className, + reduxState, + component: Component, + childFactory, + defaultComponent: DefaultComponent, + queryData, + } = this.props; 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) { @@ -48,19 +67,45 @@ class Slot extends React.Component { 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 00e95243e..533ed85db 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 @@ -153,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 = 'CHRONOLOGICAL') { - const added = nodes.concat(comments); - if (sortOrder === 'CHRONOLOGICAL') { - return added.sort(ascending); - } - if (sortOrder === 'REVERSE_CHRONOLOGICAL') { - 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 e6dad3899..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, 'REVERSE_CHRONOLOGICAL'), + $apply: (nodes) => appendNewNodes(nodes, comments.nodes), }, hasNextPage: {$set: comments.hasNextPage}, endCursor: {$set: comments.endCursor}, @@ -123,7 +123,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/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; 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/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/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/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)), diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index 7a17cfabd..cafd647af 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,98 +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. - * - * @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)); -}; - -/** - * 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 @@ -213,7 +87,7 @@ const getCountByParentIDPersonalized = async (context, {id, excludeIgnored}) => * @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) { @@ -236,6 +110,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': { @@ -249,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.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.Sort.Comments[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.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.Sort.Comments[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, sortOrder, sortBy}) => { + switch (sortBy) { + case 'CREATED_AT': { + if (cursor) { + if (sortOrder === 'DESC') { + query = query.where({ + created_at: { + $lt: cursor, + }, + }); + } else { + query = query.where({ + created_at: { + $gt: cursor, + }, + }); + } + } + + return query.sort({created_at: sortOrder === 'DESC' ? -1 : 1}); + } + case 'REPLIES': { + if (cursor) { + query = query.skip(cursor); + } + + return query.sort({reply_count: sortOrder === 'DESC' ? -1 : 1, created_at: sortOrder === 'DESC' ? -1 : 1}); + } + } + + const SORT_KEY = sortBy.toLowerCase(); + 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.Sort.Comments[SORT_KEY].sort(ctx, query, {cursor, sortOrder}); +}; + +/** + * 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, sortOrder, sortBy, limit}) => { + + // Apply the sort to the query. + query = applySort(ctx, query, {cursor, sortOrder, 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, sortOrder, sortBy, limit}), + endCursor: getEndCursor(ctx, nodes, {cursor, sortOrder, 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}) => { +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 // `null`, or `'ACCEPTED'`. - if (user != null && user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) && statuses) { + if (ctx.user != null && ctx.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) && statuses && statuses.length > 0) { comments = comments.where({ status: { $in: statuses @@ -274,6 +296,16 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a }); } + if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) { + comments = comments.where({ + action_counts: { + [action_type.toLowerCase()]: { + $gt: 0, + } + }, + }); + } + if (ids) { comments = comments.find({ id: { @@ -291,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}); } @@ -305,151 +337,19 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a comments = comments.where({parent_id}); } - if (excludeIgnored && user && user.ignoresUsers) { + 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 === 'REVERSE_CHRONOLOGICAL') { - comments = comments.where({ - created_at: { - $lt: cursor - } - }); - } else { - comments = comments.where({ - created_at: { - $gt: cursor - } - }); - } - } - - let query = comments - .sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -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, sortOrder, sortBy, limit}); }; /** - * 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, CHRONOLOGICAL'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, CHRONOLOGICAL'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. + * 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 @@ -477,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 */ @@ -486,12 +387,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)), - 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)) + parentCountByAssetID: new SharedCounterDataLoader('Comments.countByAssetID', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getParentCountsByAssetID(context, ids)) } }); 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 c4850eaf0..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 === 'REVERSE_CHRONOLOGICAL') { + 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 === 'REVERSE_CHRONOLOGICAL' ? -1 : 1}) + .sort({created_at: sortOrder === 'DESC' ? -1 : 1}) .limit(limit); }; diff --git a/graph/mutators/action.js b/graph/mutators/action.js index d62d3b4c5..794da581a 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'); @@ -23,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, @@ -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/graph/mutators/comment.js b/graph/mutators/comment.js index 533032412..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); @@ -309,7 +307,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', @@ -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 fb2f6cac5..a48af7674 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -1,55 +1,63 @@ const {decorateWithTags} = require('./util'); -const { - SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS, -} = 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'] - : ['NONE', 'ACCEPTED']; + async comment({id}, {id: commentId}, {loaders: {Comments}}) { - const comments = await Comments.getByQuery({ - asset_id: id, - ids: commentId, - statuses, - }); - - return comments.nodes[0]; - }, - comments({id}, {sort, limit, deep, excludeIgnored, tags}, {loaders: {Comments}}) { - return Comments.getByQuery({ - asset_id: id, - sort, - limit, - parent_id: deep ? undefined : null, - tags, - excludeIgnored, - }); - }, - commentCount({id, commentCount}, {excludeIgnored, tags}, {user, loaders: {Comments}}) { - - // TODO: remove - if ((user && excludeIgnored) || tags) { - return Comments.parentCountByAssetIDPersonalized({assetId: id, excludeIgnored, tags}); + // Load the comment from the database. + const comment = await Comments.get.load(commentId); + if (!comment) { + return null; } + + // If the comment asset mismatches, then don't return it! + if (comment.asset_id !== id) { + return null; + } + + return comment; + }, + 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) { 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: null, + 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 57ebc4d56..cd1e93141 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -14,33 +14,32 @@ 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}}) { - return Comments.getByQuery({ - asset_id, - parent_id: id, - sort, - limit, - excludeIgnored, - }); - }, - replyCount({id}, {excludeIgnored}, {user, loaders: {Comments}}) { + replies({id, asset_id, reply_count}, {query}, {loaders: {Comments}}) { - // TODO: remove - if (user && excludeIgnored) { - return Comments.countByParentIDPersonalized({id, excludeIgnored}); + // Don't bother looking up replies if there aren't any there! + if (reply_count === 0) { + return { + nodes: [], + hasNextPage: false, + }; } - return Comments.countByParentID.load(id); + + query.asset_id = asset_id; + query.parent_id = id; + + return Comments.getByQuery(query); + }, + replyCount({reply_count}) { + + // 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/resolvers/comment_status_history.js b/graph/resolvers/comment_status_history.js index 2e1676d90..d51e71752 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/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/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/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/resolvers/root_query.js b/graph/resolvers/root_query.js index 8bb9d31d3..1e34e8ecd 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, 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; @@ -62,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 @@ -109,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/resolvers/user.js b/graph/resolvers/user.js index c2818bd20..57601c269 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -45,7 +45,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; } diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 9d1376372..4dff623fd 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 ################################################################################ @@ -17,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 } @@ -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 + sortOrder: SORT_ORDER = DESC } # AssetsQuery allows teh ability to query assets by specific fields @@ -208,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. @@ -241,10 +244,14 @@ 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 the results by from largest first. + sortOrder: 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!] @@ -253,6 +260,22 @@ input CommentsQuery { excludeIgnored: Boolean } +input RepliesQuery { + + # Sort the results by from smallest first. + sortOrder: 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 + + # Exclude comments ignored by the requesting user + excludeIgnored: Boolean +} + # CommentCountQuery allows the ability to query comment counts by specific # methods. input CommentCountQuery { @@ -310,14 +333,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 = CHRONOLOGICAL, 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] @@ -351,10 +371,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!]! @@ -570,28 +590,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 = REVERSE_CHRONOLOGICAL, - 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. @@ -656,10 +666,22 @@ type ValidationUserError implements UserError { enum SORT_ORDER { # newest to oldest order. - REVERSE_CHRONOLOGICAL + DESC # oldest to newer order. - CHRONOLOGICAL + 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. @@ -716,11 +738,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!] } ################################################################################ @@ -922,19 +944,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!] } @@ -945,13 +967,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!] } @@ -968,7 +990,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!] } @@ -982,7 +1004,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/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 352172e57..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, @@ -66,15 +66,29 @@ 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, + // The number of replies to this comment directly. + reply_count: { + type: Number, + default: 0, + }, + + // 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: { @@ -94,10 +108,32 @@ 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; }); +// 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/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 1dfb6987b..e10c3f800 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,89 +55,12 @@ "homepage": "https://github.com/coralproject/talk#readme", "dependencies": { "accepts": "^1.3.3", - "app-module-path": "^2.2.0", - "async": "^2.5.0", - "bcryptjs": "^2.4.3", - "body-parser": "^1.17.1", - "bowser": "^1.7.0", - "cli-table": "^0.3.1", - "clipboard": "^1.7.1", - "colors": "^1.1.2", - "commander": "^2.9.0", - "compression": "^1.6.2", - "connect-redis": "^3.1.0", - "cookie-parser": "^1.4.3", - "cross-spawn": "^5.1.0", - "csurf": "^1.9.0", - "dataloader": "^1.3.0", - "debug": "^2.6.3", - "dotenv": "^4.0.0", - "ejs": "^2.5.6", - "env-rewrite": "^1.0.2", - "eventemitter2": "^4.1.2", - "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-errors": "^2.1.0", - "graphql-redis-subscriptions": "^1.1.5", - "graphql-server-express": "^0.6.0", - "graphql-subscriptions": "^0.4.3", - "graphql-tools": "^0.10.1", - "helmet": "^3.5.0", - "immutability-helper": "^2.2.0", - "inquirer": "^3.2.1", - "joi": "^10.4.1", - "jsonwebtoken": "^7.3.0", - "jwt-decode": "^2.2.0", - "kue": "^0.11.5", - "linkify-it": "^2.0.3", - "lodash": "^4.16.6", - "marked": "^0.3.6", - "metascraper": "^1.0.6", - "minimist": "^1.2.0", - "mongoose": "^4.9.8", - "morgan": "^1.8.1", - "ms": "^2.0.0", - "murmurhash-js": "^1.0.0", - "natural": "^0.5.0", - "node-emoji": "^1.5.1", - "node-fetch": "^1.6.3", - "nodemailer": "^2.6.4", - "passport": "^0.3.2", - "passport-jwt": "^2.2.1", - "passport-local": "^1.0.0", - "prop-types": "^15.5.10", - "query-strings": "^0.0.1", - "react-apollo": "^1.4.12", - "react-input-autosize": "^1.1.4", - "react-recaptcha": "^2.2.6", - "react-toastify": "^1.5.0", - "react-transition-group": "^1.1.3", - "recompose": "^0.23.1", - "redis": "^2.7.1", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "simplemde": "^1.11.2", - "smoothscroll-polyfill": "^0.3.5", - "snake-case": "^2.1.0", - "subscriptions-transport-ws": "^0.7.2", - "timekeeper": "^1.0.0", - "url-search-params": "^0.9.0", - "uuid": "^3.0.1", - "yaml-loader": "^0.4.0", - "yamljs": "^0.2.10" - }, - "devDependencies": { "apollo-client": "^1.9.1", + "app-module-path": "^2.2.0", "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-syntax-dynamic-import": "^6.18.0", @@ -154,16 +73,120 @@ "babel-polyfill": "^6.23.0", "babel-preset-es2015": "^6.24.0", "babel-preset-react": "^6.23.0", - "babel-preset-stage-0": "^6.16.0", + "bcryptjs": "^2.4.3", + "body-parser": "^1.17.1", + "bowser": "^1.7.0", + "cli-table": "^0.3.1", + "clipboard": "^1.7.1", + "colors": "^1.1.2", + "commander": "^2.11.0", + "common-tags": "^1.4.0", + "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": "^3.0.0", + "dialog-polyfill": "^0.4.4", + "dotenv": "^4.0.0", + "ejs": "^2.5.7", + "env-rewrite": "^1.0.2", + "eventemitter2": "^4.1.2", + "exports-loader": "^0.6.4", + "express": "^4.15.4", + "file-loader": "^0.11.2", + "form-data": "^2.2.0", + "fs-extra": "^4.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.8.1", + "history": "^3.0.0", + "immutability-helper": "^2.2.0", + "imports-loader": "^0.7.1", + "inquirer": "^3.2.2", + "istanbul": "^1.1.0-alpha.1", + "joi": "^10.6.0", + "json-loader": "^0.5.4", + "jsonwebtoken": "^7.4.3", + "jwt-decode": "^2.2.0", + "keymaster": "^1.6.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.7", + "minimist": "^1.2.0", + "mongoose": "^4.11.7", + "morgan": "^1.8.2", + "ms": "^2.0.0", + "murmurhash-js": "^1.0.0", + "natural": "^0.5.4", + "node-emoji": "^1.8.1", + "node-fetch": "^1.7.2", + "nodemailer": "^2.6.4", + "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", + "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.8.0", + "redux": "^3.6.0", + "redux-thunk": "^2.1.0", + "resolve": "^1.4.0", + "semver": "^5.4.1", + "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.1.0", + "webpack": "^2.3.1", + "webpack-sources": "^1.0.1", + "yaml-loader": "^0.4.0", + "yamljs": "^0.2.10" + }, + "devDependencies": { "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", @@ -174,50 +197,13 @@ "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" + "sinon": "^3.2.1", + "sinon-chai": "^2.13.0", + "supertest": "^2.0.1" }, "engines": { "node": "^8" 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..2628d33f1 --- /dev/null +++ b/plugin-api/beta/client/components/SortOption.css @@ -0,0 +1,16 @@ +.label { + display: block; + cursor: pointer; + width: 100%; + padding: 6px 16px 6px 10px; + box-sizing: border-box; + + &:hover { + background-color: rgba(0, 0, 0, 0.05); + } +} + +.input { + cursor: pointer; + margin-right: 6px; +} diff --git a/plugin-api/beta/client/components/SortOption.js b/plugin-api/beta/client/components/SortOption.js new file mode 100644 index 000000000..7c8b4d761 --- /dev/null +++ b/plugin-api/beta/client/components/SortOption.js @@ -0,0 +1,31 @@ +import React from 'react'; +import styles from './SortOption.css'; +import PropTypes from 'prop-types'; + +export default class SortOption extends React.Component { + render() { + return ( + + ); + } +} + +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/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/factories/index.js b/plugin-api/beta/client/factories/index.js new file mode 100644 index 000000000..d413258c6 --- /dev/null +++ b/plugin-api/beta/client/factories/index.js @@ -0,0 +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/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..076d921e7 --- /dev/null +++ b/plugin-api/beta/client/hocs/withSortOption.js @@ -0,0 +1,48 @@ +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 + ); + +/** + * 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 = () => { + this.props.setSort({sortBy, sortOrder}); + } + + render() { + const active = this.props.sortOrder === sortOrder && this.props.sortBy === sortBy; + 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 eecfe0305..6dd18f3a1 100644 --- a/plugin-api/beta/client/utils/index.js +++ b/plugin-api/beta/client/utils/index.js @@ -1,8 +1,10 @@ export { isTagged, - insertCommentsSorted, + prependNewNodes, + appendNewNodes, getSlotFragmentSpreads, forEachError, + capitalize, getErrorMessages, getDefinitionName, } from 'coral-framework/utils'; diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js index 243b8915b..a6df8969e 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,29 @@ function getReactionConfig(reaction) { return { typeDefs, + context: { + 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, sortOrder}) { + if (cursor) { + query = query.skip(cursor); + } + + return query.sort({[`action_counts.${reaction}`]: sortOrder === 'DESC' ? -1 : 1, created_at: sortOrder === 'DESC' ? -1 : 1}); + }, + }, + }, + }), + }, resolvers: { Subscription: { [`${reaction}ActionCreated`]: ({action}) => { diff --git a/plugins.default.json b/plugins.default.json index 05da159b7..89028dd7d 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-respected", + "talk-plugin-sort-most-replied" ] } 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} 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 63ce30d01..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 {insertCommentsSorted, 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 { @@ -18,7 +18,8 @@ class TabPaneContainer extends React.Component { limit: 5, cursor: this.props.asset.featuredComments.endCursor, asset_id: this.props.asset.id, - sort: 'REVERSE_CHRONOLOGICAL', + sortOrder: this.props.data.variables.sortOrder, + 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, 'REVERSE_CHRONOLOGICAL'), + $apply: (nodes) => appendNewNodes(nodes, 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: Date, $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 + $sortOrder: SORT_ORDER + $sortBy: SORT_COMMENTS_BY + $excludeIgnored: Boolean + ) { + comments( + query: { + limit: $limit + cursor: $cursor + tags: ["FEATURED"] + asset_id: $asset_id, + sortOrder: $sortOrder + 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(tags: ["FEATURED"], excludeIgnored: $excludeIgnored, deep: true) @skip(if: $hasComment) { + featuredComments: comments( + query: { + tags: ["FEATURED"] + sortOrder: $sortOrder + sortBy: $sortBy + } + deep: true + ) @skip(if: $hasComment) { 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 79c3d874f..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, 'REVERSE_CHRONOLOGICAL') + $apply: (nodes) => prependNewNodes(nodes, [comment]), } }, featuredCommentsCount: { 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..7fca0aed4 --- /dev/null +++ b/plugins/talk-plugin-offtopic/client/components/OffTopicFilter.css @@ -0,0 +1,20 @@ +.label { + display: block; + cursor: pointer; + width: 100%; + padding: 6px 16px 6px 10px; + box-sizing: border-box; + + &:hover { + background-color: rgba(0, 0, 0, 0.05); + } +} + +.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/OffTopicTag.css b/plugins/talk-plugin-offtopic/client/components/OffTopicTag.css new file mode 100644 index 000000000..c1cddce8d --- /dev/null +++ b/plugins/talk-plugin-offtopic/client/components/OffTopicTag.css @@ -0,0 +1,10 @@ +.tag { + background: #D2D7D3; + font-size: 12px; + font-weight: bold; + color: #4A4A4A; + display: inline-block; + margin: 0px 5px; + padding: 5px 5px; + border-radius: 2px; +} 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-offtopic/client/components/styles.css b/plugins/talk-plugin-offtopic/client/components/styles.css deleted file mode 100644 index c2f3e4e36..000000000 --- a/plugins/talk-plugin-offtopic/client/components/styles.css +++ /dev/null @@ -1,27 +0,0 @@ -.offTopic { - height: 100%; -} - -.offTopicLabel { - padding: 10px 20px; - display: block; -} - -.tag { - background: #D2D7D3; - font-size: 12px; - font-weight: bold; - color: #4A4A4A; - display: inline-block; - margin: 0px 5px; - padding: 5px 5px; - border-radius: 2px; -} - -.viewingOption { - padding: 5px 0; -} - -:global(.talk-plugin-off-topic-comment) { - display: none; -} 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-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..04836df53 --- /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'}, +); + +/** + * This plugin 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-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/.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..bdef73f03 --- /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'}, +); + +/** + * This plugin 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-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/.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..83c20b166 --- /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'}, +); + +/** + * This plugin 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-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/index.js b/plugins/talk-plugin-sort-oldest/client/index.js new file mode 100644 index 000000000..e6a480340 --- /dev/null +++ b/plugins/talk-plugin-sort-oldest/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-oldest.label'), + {sortBy: 'CREATED_AT', sortOrder: 'ASC'}, +); + +/** + * This plugin depends on talk-plugin-viewing-options. + */ + +export default { + translations, + slots: { + viewingOptionsSort: [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..66e59b823 --- /dev/null +++ b/plugins/talk-plugin-sort-oldest/client/translations.yml @@ -0,0 +1,5 @@ +en: + talk-plugin-sort-oldest: + label: Oldest first +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..dbae31bc4 --- /dev/null +++ b/plugins/talk-plugin-viewing-options/client/components/Category.css @@ -0,0 +1,22 @@ +.root { + padding-bottom: 12px; + + &:not(:first-child) { + border-top: 1px solid rgba(0, 0, 0, 0.1); + } +} + +.title { + font-weight: bold; + padding: 12px 8px 4px 8px; +} + +.list { + padding: 0; + margin: 0; +} + +.listItem { + 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..184b5cadc --- /dev/null +++ b/plugins/talk-plugin-viewing-options/client/components/Category.js @@ -0,0 +1,23 @@ +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, data, asset, root}) => { + return ( + +
    {title}
    + +
    + ); +}; + +export default ViewingOptions; 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 c51d87f11..4de6f15f7 100644 --- a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css +++ b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.css @@ -9,27 +9,7 @@ cursor: pointer; } -.list { - background: white; - position: absolute; - box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15); - right: 0px; - top: 20px; - z-index: 10; -} - -.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; } @@ -37,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 8988dd1ec..30997d5a2 100644 --- a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js +++ b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js @@ -3,51 +3,47 @@ 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 Menu from './Menu'; -const ViewingOptions = (props) => { - const toggleOpen = () => { - if (!props.open) { - props.openMenu(); +class ViewingOptions extends React.Component { + + 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 ( - -
    -
    - + render() { + const {open, data, root, asset} = this.props; + return ( + +
    +
    + +
    + {open && }
    - { - props.open ? ( -
    -
      - { - React.Children.map(, (component) => { - return React.createElement('li', { - className: 'talk-plugin-viewing-options-item' - }, component); - }) - } -
    -
    - ) : null - } -
    - - ); -}; + + ); + } +} export default ViewingOptions; 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); diff --git a/plugins/talk-plugin-viewing-options/client/translations.yml b/plugins/talk-plugin-viewing-options/client/translations.yml index d1b1c1896..c06bffb9e 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: Sorting + filter: Filtering es: talk-plugin-viewing-options: viewing_options: "Opciones de visualización" 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/services/actions.js b/services/actions.js index 74c3bdad3..ba9f1fde5 100644 --- a/services/actions.js +++ b/services/actions.js @@ -1,11 +1,49 @@ const ActionModel = require('../models/action'); const _ = require('lodash'); const errors = require('../errors'); +const events = require('./events'); +const { + ACTIONS_NEW, + ACTIONS_DELETE, +} = require('./events/constants'); + +/** + * 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`. + 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 { /** * Finds an action by the id. + * * @param {String} id identifier of the action (uuid) */ static findById(id) { @@ -14,55 +52,37 @@ 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 * @return {Promise} */ - static 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. - let query = { + let foundAction = await findOnlyOneAndUpdate({ 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. + await 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) { @@ -80,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] @@ -106,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 = '') { @@ -178,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 */ @@ -190,8 +213,31 @@ 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, + user_id, + }); + if (!action) { + return; + } + + // Emit that the action was deleted. + await 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 af7593a8e..db0714233 100644 --- a/services/comments.js +++ b/services/comments.js @@ -1,10 +1,18 @@ 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'); +const sc = require('snake-case'); const errors = require('../errors'); +const events = require('./events'); +const { + ACTIONS_NEW, + ACTIONS_DELETE, + COMMENTS_NEW, + COMMENTS_EDIT, +} = require('./events/constants'); module.exports = class CommentsService { @@ -13,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)) { @@ -35,7 +43,12 @@ module.exports = class CommentsService { }] }, comment)); - return commentModel.save(); + const savedCommentModel = await commentModel.save(); + + // Emit that the comment was created! + await events.emitAsync(COMMENTS_NEW, savedCommentModel); + + return savedCommentModel; } /** @@ -48,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', 'ACCEPTED'], + }, }; // Establish the edit window (if it exists) and add the condition to the @@ -62,9 +78,7 @@ module.exports = class CommentsService { }; } - const { - value: comment - } = await CommentModel.findOneAndUpdate(query, { + const originalComment = await CommentModel.findOneAndUpdate(query, { $set: { body, status, @@ -79,33 +93,54 @@ 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); - if (comment === null) { + 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', '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; } 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(), + }); + + await events.emitAsync(COMMENTS_EDIT, originalComment, editedComment); + + return editedComment; } /** @@ -205,21 +240,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. + await events.emitAsync(COMMENTS_EDIT, originalComment, editedComment); + + return editedComment; } /** @@ -230,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, @@ -238,55 +288,99 @@ 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}); +//============================================================================== +// 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 && action.group_id.length > 0) { + const GROUP_ID = sc(action.group_id.toLowerCase()); + + update[`action_counts.${ACTION_TYPE}_${GROUP_ID}`] = value; } - /** - * 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 + try { + await CommentModel.update({ + id: action.item_id, + }, { + $inc: update, }); - } - - /** - * 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); + } 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; + } + + return incrActionCounts(action, 1); +}); + +// 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; + } + + 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 || 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 || 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 new file mode 100644 index 000000000..a1da8dce6 --- /dev/null +++ b/services/events/constants.js @@ -0,0 +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/services/events/index.js b/services/events/index.js new file mode 100644 index 000000000..214e23d65 --- /dev/null +++ b/services/events/index.js @@ -0,0 +1,22 @@ +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'}`); + }); +} + +// The default error handler. +events.on('error', (err) => { + console.error('events error:', err); +}); + +module.exports = events; diff --git a/services/mongoose.js b/services/mongoose.js index 07f3b2189..fb23a5fe1 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,17 @@ if (WEBPACK) { } else { // Connect to the Mongo instance. - mongoose.connect(MONGO_URL, (err) => { - if (err) { - throw err; - } - - debug('connection established'); - }); - + mongoose + .connect(MONGO_URL, { + useMongoClient: true, + }) + .then(() => { + debug('connection established'); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); } module.exports = mongoose; 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/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/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..2b6128133 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,9 +1,5 @@ test/helpers/*.js - -test/e2e test/server ---compilers js:babel-core/register ---require ignore-styles --recursive --colors --sort 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..4414428bd 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,13 +8,15 @@ 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()); 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 } } @@ -23,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: []}, @@ -75,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 } } 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..8507fdc36 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,21 +8,15 @@ 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; - 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 = ` @@ -119,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, { @@ -133,7 +125,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); @@ -160,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', }, @@ -171,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', @@ -195,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: { @@ -209,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', @@ -230,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( @@ -240,7 +227,7 @@ describe('graph.mutations.editComment', () => { asset_id: asset.id, author_id: user.id, }, - beforeEdit, + beforeEdit )); // now edit @@ -252,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/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..68270f9ba 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 () => { @@ -44,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 @@ -58,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); @@ -81,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 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 3195db0e5..7574c41b4 100644 --- a/test/server/routes/api/account/index.js +++ b/test/server/routes/api/account/index.js @@ -2,15 +2,15 @@ const passport = require('../../../passport'); const app = require('../../../../../app'); -const chai = require('chai'); -chai.use(require('chai-as-promised')); -chai.use(require('chai-http')); -const expect = chai.expect; - const UsersService = require('../../../../../services/users'); const SettingsService = require('../../../../../services/settings'); const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}}; +const chai = require('chai'); +chai.should(); +chai.use(require('chai-http')); +const expect = chai.expect; + describe('/api/v1/account/username', () => { let mockUser; beforeEach(async () => { 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/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); 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..e33089325 100644 --- a/test/server/services/comments.js +++ b/test/server/services/comments.js @@ -1,14 +1,20 @@ 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'); 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')); +chai.use(require('sinon-chai')); +const expect = chai.expect; + +const sinon = require('sinon'); describe('services.CommentsService', () => { const comments = [{ @@ -186,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/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 19545ff3e..27460f049 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 = { 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 818049337..a0150559a 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,26 @@ 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 +586,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 +594,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 +611,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 +779,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 +786,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 +883,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 +894,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 +1000,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 +1074,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 +1144,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" @@ -1412,14 +1184,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" @@ -1430,7 +1202,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 +1227,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 +1254,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 +1489,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,22 +1565,22 @@ 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: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" dependencies: graceful-readlink ">= 1.0.0" +commander@^2.11.0, commander@^2.8.1, commander@^2.9.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" @@ -1838,29 +1595,15 @@ 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" -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" @@ -1871,16 +1614,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" @@ -1907,19 +1651,12 @@ 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" +connect@3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.2.tgz#694e8d20681bfe490282c8ab886be98f09f42fe7" 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" - 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" @@ -1958,10 +1695,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" @@ -2023,7 +1756,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" @@ -2039,10 +1772,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 +1835,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 +1957,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 +2008,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,11 +2016,11 @@ 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: - version "2.6.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.4.tgz#7586a9b3c39741c0282ae33445c4e8ac74734fe0" +debug@*, debug@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.0.tgz#1d2feae53349047b08b264ec41906ba17a8516e4" dependencies: - ms "0.7.3" + ms "2.0.0" debug@2.2.0, debug@~2.2.0: version "2.2.0" @@ -2338,11 +2046,23 @@ 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" +debug@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" dependencies: - ms "0.7.2" + ms "2.0.0" + +debug@2.6.8, debug@^2.2.0, debug@^2.3.3: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +debug@^2.1.1: + version "2.6.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.4.tgz#7586a9b3c39741c0282ae33445c4e8ac74734fe0" + dependencies: + ms "0.7.3" debug@~0.7.4: version "0.7.4" @@ -2352,7 +2072,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 +2109,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" @@ -2444,6 +2141,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" @@ -2465,22 +2166,11 @@ 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: +diff@3.2.0, diff@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" @@ -2592,13 +2282,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@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" +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" @@ -2636,12 +2322,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 +2343,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" @@ -2691,13 +2371,14 @@ error-ex@^1.2.0: is-arrayish "^0.2.1" es-abstract@^1.6.1, es-abstract@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c" + 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.3" + is-regex "^1.0.4" es-to-primitive@^1.1.1: version "1.1.1" @@ -2783,7 +2464,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 +2599,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" @@ -3016,6 +2693,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" @@ -3023,23 +2704,9 @@ 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" +express@^4.12.2, 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" @@ -3047,37 +2714,37 @@ express@^4.12.2, express@^4.15.2: content-type "~1.0.2" cookie "0.3.1" cookie-signature "1.0.6" - debug "2.6.1" - depd "~1.1.0" + debug "2.6.8" + depd "~1.1.1" encodeurl "~1.0.1" escape-html "~1.0.3" etag "~1.8.0" - finalhandler "~1.0.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.3" - qs "6.4.0" + proxy-addr "~1.1.5" + qs "6.5.0" range-parser "~1.2.0" - send "0.15.1" - serve-static "1.12.1" + send "0.15.4" + serve-static "1.12.4" setprototypeof "1.0.3" statuses "~1.3.1" - type-is "~1.6.14" + type-is "~1.6.15" 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" + vary "~1.1.1" 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 +2771,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 +2783,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 +2809,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" @@ -3181,11 +2830,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" @@ -3193,11 +2842,11 @@ finalhandler@1.0.0: statuses "~1.3.1" unpipe "~1.0.0" -finalhandler@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.2.tgz#d0e36f9dbc557f2de14423df6261889e9d60c93a" +finalhandler@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.4.tgz#18574f2e7c4b98b8ae3b230c21f201f31bdb3fb7" dependencies: - debug "2.6.4" + debug "2.6.8" encodeurl "~1.0.1" escape-html "~1.0.3" on-finished "~2.3.0" @@ -3220,12 +2869,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" @@ -3280,7 +2923,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: @@ -3288,6 +2939,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" @@ -3327,9 +2984,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" @@ -3373,24 +3030,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 +3075,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 +3149,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 +3171,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 +3392,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" @@ -3823,28 +3438,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" @@ -3916,22 +3532,14 @@ 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" 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 +3561,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,13 +3570,14 @@ 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" +http-errors@~1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" dependencies: - agent-base "2" - debug "2" - extend "3" + 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" @@ -4001,23 +3602,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" @@ -4037,10 +3626,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" @@ -4148,9 +3733,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" @@ -4189,20 +3774,14 @@ ip@^1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" -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" -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 +3802,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 +3888,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 +3947,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 +4058,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: @@ -4558,9 +4133,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" @@ -4630,30 +4205,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" @@ -4714,14 +4265,14 @@ 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: - version "7.4.0" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.4.0.tgz#515bf2bba070ec615bad97fd2e945027eb476946" +jsonwebtoken@^7.0.0, 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 "^0.7.1" + ms "^2.0.0" xtend "^4.0.1" jsprim@^1.2.2: @@ -4744,6 +4295,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" @@ -4765,9 +4320,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" @@ -4785,9 +4340,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" @@ -4852,11 +4407,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" @@ -4901,25 +4456,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 +4467,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 +4475,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 +4495,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 +4519,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 +4542,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 +4570,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 +4582,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 +4594,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 +4610,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" @@ -5136,6 +4618,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" @@ -5144,14 +4630,18 @@ 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" +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" @@ -5170,7 +4660,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" @@ -5241,9 +4731,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" @@ -5256,7 +4746,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: @@ -5281,14 +4771,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" @@ -5317,7 +4811,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 +4821,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 +4840,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" @@ -5400,62 +4864,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" @@ -5467,17 +4931,17 @@ ms@0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" -ms@0.7.3, ms@^0.7.1: +ms@0.7.3: 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" @@ -5499,6 +4963,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" @@ -5511,13 +4979,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: @@ -5530,30 +4999,20 @@ 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" +nise@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.0.1.tgz#0da92b10a854e97c0f496f6c2845a301280b3eef" 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" + 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" @@ -5571,15 +5030,15 @@ 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.3.3, node-fetch@^1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.6.3.tgz#dc234edd6489982d58e8f0db4f695029abcd8c04" +node-fetch@^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" @@ -5776,7 +5235,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 +5267,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 +5283,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 +5375,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" @@ -6007,9 +5432,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" @@ -6024,9 +5449,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" @@ -6041,10 +5466,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" @@ -6091,10 +5512,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" @@ -6135,14 +5552,18 @@ 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" 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" @@ -6637,7 +6058,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 +6070,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" @@ -6676,25 +6097,12 @@ protocols@^1.1.0, protocols@^1.4.0: version "1.4.5" resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.5.tgz#21de1f441c4ef7094408ed9f1c94f7a114b87557" -proxy-addr@~1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3" +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.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" + ipaddr.js "1.4.0" prr@~0.0.0: version "0.0.0" @@ -6825,14 +6233,10 @@ pym.js@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/pym.js/-/pym.js-1.2.0.tgz#feb1e2c9b396613e5172192b0cdd75408f9c8322" -q@1.1.2: +q@1.1.2, 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,13 +6245,13 @@ q@2.0.3: pop-iterate "^1.0.1" weak-map "^1.0.5" -qs@6.4.0, qs@^6.1.0, qs@^6.2.0, qs@~6.4.0: +qs@6.4.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" +qs@6.5.0, qs@^6.1.0, qs@^6.2.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" @@ -6882,10 +6286,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 +6318,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 +6466,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 +6475,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 +6523,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" @@ -7167,7 +6542,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.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" @@ -7175,13 +6550,13 @@ 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: - version "2.7.1" - resolved "https://registry.yarnpkg.com/redis/-/redis-2.7.1.tgz#7d56f7875b98b20410b71539f1d878ed58ebf46a" +redis@^2.6.3, 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.5.0" + redis-parser "^2.6.0" redis@~2.6.0-2: version "2.6.5" @@ -7212,10 +6587,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 +6612,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 +6620,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 +6687,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" @@ -7422,9 +6752,9 @@ 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: - version "1.3.3" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" +resolve@^1.1.6, resolve@^1.1.7, 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" @@ -7466,10 +6796,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" @@ -7500,15 +6826,23 @@ rx@^2.4.3: version "2.5.3" resolved "https://registry.yarnpkg.com/rx/-/rx-2.5.3.tgz#21adc7d80f02002af50dae97fd9dbf248755f566" -safe-buffer@^5.0.1, safe-buffer@~5.0.1: +safe-buffer@5.1.1, safe-buffer@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +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" -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 +6850,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" @@ -7542,44 +6860,44 @@ semver-regex@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-1.0.0.tgz#92a4969065f9c70c694753d55248fc68f8f652c9" -"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" 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" +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" -send@0.15.1: - version "0.15.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.15.1.tgz#8a02354c26e6f5cca700065f5f0cdeba90ec7b5f" +send@0.15.4: + version "0.15.4" + resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9" dependencies: - debug "2.6.1" - depd "~1.1.0" + 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.1" + http-errors "~1.6.2" mime "1.3.4" - ms "0.7.2" + 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" +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.1" + send "0.15.4" set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" @@ -7593,10 +6911,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 +6960,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" @@ -7662,6 +6968,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" @@ -7709,15 +7033,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: @@ -7738,6 +7054,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" @@ -7756,7 +7076,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 +7133,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 +7164,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" @@ -7887,10 +7193,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" @@ -7901,14 +7203,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 +7341,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 +7373,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 +7385,15 @@ 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-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" -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 +7403,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 +7500,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 +7518,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 +7528,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" @@ -8283,7 +7546,11 @@ type-detect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" -type-is@~1.6.14: +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.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" dependencies: @@ -8323,12 +7590,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 +7640,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,13 +7682,13 @@ 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" -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.0.0, uuid@^3.0.1, 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" @@ -8454,7 +7711,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.1: version "1.1.1" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" @@ -8500,14 +7757,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" @@ -8522,6 +7771,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" @@ -8555,12 +7811,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 +7821,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 +7829,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 +7845,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 +7914,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 +7922,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 +8032,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"