diff --git a/.eslintignore b/.eslintignore index e09212b62..4805e34a4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -19,5 +19,8 @@ plugins/* !plugins/talk-plugin-sort-most-liked !plugins/talk-plugin-sort-most-loved !plugins/talk-plugin-sort-most-respected +!plugins/talk-plugin-author-menu +!plugins/talk-plugin-member-since +!plugins/talk-plugin-ignore-user node_modules diff --git a/.gitignore b/.gitignore index 3fdb05c89..0a2ac00df 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,8 @@ plugins/* !plugins/talk-plugin-sort-most-liked !plugins/talk-plugin-sort-most-loved !plugins/talk-plugin-sort-most-respected +!plugins/talk-plugin-author-menu +!plugins/talk-plugin-member-since +!plugins/talk-plugin-ignore-user **/node_modules/* diff --git a/client/coral-admin/src/routes/Moderation/graphql.js b/client/coral-admin/src/routes/Moderation/graphql.js index 28ca259d8..866218d73 100644 --- a/client/coral-admin/src/routes/Moderation/graphql.js +++ b/client/coral-admin/src/routes/Moderation/graphql.js @@ -69,15 +69,15 @@ function getCommentQueues(comment, queueConfig) { const queues = []; Object.keys(queueConfig).forEach((key) => { const {action_type, statuses, tags} = queueConfig[key]; - let addToQueues = false; - if (statuses && statuses.indexOf(comment.status) >= 0) { - addToQueues = true; + let addToQueues = true; + if (statuses && statuses.indexOf(comment.status) === -1) { + addToQueues = false; } - if (tags && comment.tags && comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0)) { - addToQueues = true; + if (tags && (!comment.tags || !comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0))) { + addToQueues = false; } - if (action_type && comment.actions && comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type}action`)) { - addToQueues = true; + if (action_type && (!comment.actions || !comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type.toLowerCase()}action`))) { + addToQueues = false; } if (addToQueues) { queues.push(key); diff --git a/client/coral-embed-stream/src/components/AllCommentsPane.js b/client/coral-embed-stream/src/components/AllCommentsPane.js index 427d3a68b..7602902df 100644 --- a/client/coral-embed-stream/src/components/AllCommentsPane.js +++ b/client/coral-embed-stream/src/components/AllCommentsPane.js @@ -1,7 +1,6 @@ import React from 'react'; import LoadMore from './LoadMore'; -import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import NewCount from './NewCount'; import {TransitionGroup} from 'react-transition-group'; import {forEachError} from 'coral-framework/utils'; @@ -126,7 +125,6 @@ class AllCommentsPane extends React.Component { root, comments, commentClassNames, - ignoreUser, setActiveReplyBox, activeReplyBox, notify, @@ -139,7 +137,6 @@ class AllCommentsPane extends React.Component { loadNewReplies, deleteAction, showSignInDialog, - commentIsIgnored, charCountEnable, maxCharCount, editComment, @@ -157,9 +154,8 @@ class AllCommentsPane extends React.Component { /> {view.map((comment) => { - return commentIsIgnored(comment) - ? - : ; + /> + ); })} u.id === comment.user.id) + ); + } + hasIgnoredReplies() { return this.props.comment.replies && - this.props.comment.replies.nodes.some((reply) => this.props.commentIsIgnored(reply)); + this.props.comment.replies.nodes.some((reply) => this.commentIsIgnored(reply)); } loadNewReplies = () => { @@ -317,7 +319,6 @@ export default class Comment extends React.Component { comment, postFlag, parentId, - ignoreUser, highlighted, postComment, currentUser, @@ -332,12 +333,15 @@ export default class Comment extends React.Component { charCountEnable, showSignInDialog, liveUpdates, - commentIsIgnored, animateEnter, emit, commentClassNames = [] } = this.props; + if (this.commentIsIgnored(comment)) { + return ; + } + const view = this.getVisibileReplies(); // Inactive comments can be viewed by moderators and admins (e.g. using permalinks). @@ -480,16 +484,6 @@ export default class Comment extends React.Component { } } - { isActive && (currentUser && (comment.user.id !== currentUser.id)) && - - /* TopRightMenu allows currentUser to ignore other users' comments */ - - - - } { !isActive && } @@ -581,9 +575,8 @@ export default class Comment extends React.Component { {view.map((reply) => { - return commentIsIgnored(reply) - ? - : ; + /> + ); })}
diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 4d46c8e87..89e7b13ae 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -40,15 +40,6 @@ class Stream extends React.Component { } } - commentIsIgnored = (comment) => { - const me = this.props.root.me; - return ( - me && - me.ignoredUsers && - me.ignoredUsers.find((u) => u.id === comment.user.id) - ); - }; - renderHighlightedComment() { const { data, @@ -64,7 +55,6 @@ class Stream extends React.Component { postDontAgree, deleteAction, showSignInDialog, - ignoreUser, loadNewReplies, auth: {user}, emit, @@ -90,7 +80,6 @@ class Stream extends React.Component { data={data} root={root} commentClassNames={commentClassNames} - ignoreUser={ignoreUser} setActiveReplyBox={setActiveReplyBox} activeReplyBox={activeReplyBox} notify={notify} @@ -106,7 +95,6 @@ class Stream extends React.Component { deleteAction={deleteAction} showSignInDialog={showSignInDialog} key={topLevelComment.id} - commentIsIgnored={this.commentIsIgnored} comment={topLevelComment} charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} @@ -133,7 +121,6 @@ class Stream extends React.Component { postDontAgree, deleteAction, showSignInDialog, - ignoreUser, activeStreamTab, setActiveStreamTab, loadNewReplies, @@ -183,7 +170,6 @@ class Stream extends React.Component { root={root} comments={comments} commentClassNames={commentClassNames} - ignoreUser={ignoreUser} setActiveReplyBox={setActiveReplyBox} activeReplyBox={activeReplyBox} notify={notify} @@ -197,7 +183,6 @@ class Stream extends React.Component { loadNewReplies={loadNewReplies} deleteAction={deleteAction} showSignInDialog={showSignInDialog} - commentIsIgnored={this.commentIsIgnored} charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} editComment={editComment} @@ -322,9 +307,6 @@ Stream.propTypes = { notify: PropTypes.func.isRequired, postComment: PropTypes.func.isRequired, - // dispatch action to ignore another user - ignoreUser: React.PropTypes.func, - // edit a comment, passed (id, asset_id, { body }) editComment: React.PropTypes.func }; diff --git a/client/coral-embed-stream/src/components/TopRightMenu.js b/client/coral-embed-stream/src/components/TopRightMenu.js deleted file mode 100644 index 47147e853..000000000 --- a/client/coral-embed-stream/src/components/TopRightMenu.js +++ /dev/null @@ -1,60 +0,0 @@ -import React, {PropTypes} from 'react'; -import {IgnoreUserWizard} from './IgnoreUserWizard'; -import Toggleable from './Toggleable'; - -// TopRightMenu appears as a dropdown in the top right of the comment. -// when you click the down cehvron, it expands and shows IgnoreUserWizard -// when you click 'cancel' in the wizard, it closes the menu -export class TopRightMenu extends React.Component { - static propTypes = { - - // comment on which this menu appears - comment: PropTypes.shape({ - user: PropTypes.shape({ - id: PropTypes.string.isRequired, - username: PropTypes.string.isRequired - }).isRequired - }).isRequired, - ignoreUser: PropTypes.func, - - // show notification to the user (e.g. for errors) - notify: PropTypes.func.isRequired, - } - constructor(props) { - super(props); - this.state = { - timesReset: 0 - }; - } - render() { - const {comment, ignoreUser, notify} = this.props; - - // timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable) - const reset = () => this.setState({timesReset: this.state.timesReset + 1}); - const ignoreUserAndCloseMenuAndNotifyOnError = async ({id}) => { - - // close menu - reset(); - - // ignore user - try { - await ignoreUser({id}); - } catch (error) { - notify('error', 'Failed to ignore user'); - throw error; - } - }; - return ( - -
- -
-
- ); - } -} - diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index a4bfbc0eb..733708a97 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -85,13 +85,24 @@ const singleCommentFragment = gql` edited editableUntil } + ${getSlotFragmentSpreads(slots, 'comment')} } `; const withCommentFragments = withFragments({ root: gql` fragment CoralEmbedStream_Comment_root on RootQuery { + me { + ignoredUsers { + id + } + } __typename + me { + ignoredUsers { + id + } + } ${getSlotFragmentSpreads(slots, 'root')} } `, @@ -116,7 +127,6 @@ const withCommentFragments = withFragments({ endCursor } `, THREADING_LEVEL)} - ${getSlotFragmentSpreads(slots, 'comment')} } ${singleCommentFragment} ` diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 194bfc5d9..273d39589 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux'; import {ADDTL_COMMENTS_ON_LOAD_MORE, THREADING_LEVEL} from '../constants/stream'; import { withPostComment, withPostFlag, withPostDontAgree, - withDeleteAction, withIgnoreUser, withEditComment + withDeleteAction, withEditComment } from 'coral-framework/graphql/mutations'; import * as authActions from 'coral-embed-stream/src/actions/auth'; @@ -302,7 +302,7 @@ const fragments = { questionBoxEnable questionBoxContent questionBoxIcon - closeTimeout + closedTimeout closedMessage charCountEnable charCount @@ -374,7 +374,6 @@ export default compose( withPostComment, withPostFlag, withPostDontAgree, - withIgnoreUser, withDeleteAction, withEditComment, )(StreamContainer); diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 9ec9a2132..09470de14 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -108,33 +108,6 @@ export default { `, }, mutations: { - IgnoreUser: ({variables}) => ({ - updateQueries: { - CoralEmbedStream_Embed: (previousData) => { - const ignoredUserId = variables.id; - const updated = update(previousData, {me: {ignoredUsers: {$push: [{ - id: ignoredUserId, - __typename: 'User', - }]}}}); - return updated; - } - } - }), - StopIgnoringUser: ({variables}) => ({ - updateQueries: { - CoralEmbedStream_Profile: (previousData) => { - const noLongerIgnoredUserId = variables.id; - - // remove noLongerIgnoredUserId from ignoredUsers - const updated = update(previousData, {me: {ignoredUsers: { - $apply: (ignoredUsers) => { - return ignoredUsers.filter((u) => u.id !== noLongerIgnoredUserId); - } - }}}); - return updated; - } - } - }), PostComment: ({ variables: {comment: {asset_id, body, parent_id, tags = []}}, state: {auth}, diff --git a/client/coral-embed/src/Snackbar.js b/client/coral-embed/src/Snackbar.js index c518288c6..11219102d 100644 --- a/client/coral-embed/src/Snackbar.js +++ b/client/coral-embed/src/Snackbar.js @@ -38,7 +38,7 @@ export default class Snackbar { } alert(message) { - const [type, text] = message.split('|'); + const {type, text} = JSON.parse(message); this.el.style.transform = 'translate(-50%, 20px)'; this.el.style.opacity = 0; this.el.className = `coral-notif-${type}`; diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 91d2ae0ab..4254400bb 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -293,7 +293,7 @@ export const withIgnoreUser = withMutation( } `, { props: ({mutate}) => ({ - ignoreUser: ({id}) => { + ignoreUser: (id) => { return mutate({ variables: { id, diff --git a/client/coral-framework/services/notification.js b/client/coral-framework/services/notification.js index 5730200ad..a3f1a6520 100644 --- a/client/coral-framework/services/notification.js +++ b/client/coral-framework/services/notification.js @@ -5,14 +5,14 @@ */ export function createNotificationService(pym) { return { - success(msg) { - pym.sendMessage('coral-alert', `success|${msg}`); + success(text) { + pym.sendMessage('coral-alert', JSON.stringify({kind: 'success', text})); }, - error(msg) { - pym.sendMessage('coral-alert', `error|${msg}`); + error(text) { + pym.sendMessage('coral-alert', JSON.stringify({kind: 'error', text})); }, - info(msg) { - pym.sendMessage('coral-alert', `info|${msg}`); + info(text) { + pym.sendMessage('coral-alert', JSON.stringify({kind: 'info', text})); }, }; } diff --git a/client/coral-settings/components/IgnoredUsers.css b/client/coral-settings/components/IgnoredUsers.css deleted file mode 100644 index 716140256..000000000 --- a/client/coral-settings/components/IgnoredUsers.css +++ /dev/null @@ -1,24 +0,0 @@ -.ignoredUser { - display: table-row; -} - -.ignoredUserList { - display: table; -} - -.ignoredUser > * { - display: table-cell; -} - -.stopListening { - color: #D0011B; -} - -.link { - text-decoration: underline; - cursor: pointer; -} - -.stopListening:before { - content: '\00a0\00a0\00a0\00a0'; -} \ No newline at end of file diff --git a/client/coral-settings/components/IgnoredUsers.js b/client/coral-settings/components/IgnoredUsers.js deleted file mode 100644 index a8723161f..000000000 --- a/client/coral-settings/components/IgnoredUsers.js +++ /dev/null @@ -1,43 +0,0 @@ -import React, {Component, PropTypes} from 'react'; -import t from 'coral-framework/services/i18n'; -import styles from './IgnoredUsers.css'; - -class IgnoredUsers extends Component { - static propTypes = { - users: PropTypes.arrayOf(PropTypes.shape({ - username: PropTypes.string, - id: PropTypes.string, - })).isRequired, - - // accepts { id } - stopIgnoring: PropTypes.func.isRequired, - } - render() { - const {users, stopIgnoring} = this.props; - return ( -
- { - users.length - ?

{t('framework.because_you_ignored')}

- : null - } -
- { - users.map(({username, id}) => ( - -
{ username }
-
- stopIgnoring({id})} - className={styles.link}>{t('framework.stop_ignoring')} -
-
- )) - } -
-
- ); - } -} - -export default IgnoredUsers; diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index db47de8e2..aab0e2a6a 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -3,12 +3,10 @@ import {compose, gql} from 'react-apollo'; import React, {Component} from 'react'; import {bindActionCreators} from 'redux'; import {withQuery} from 'coral-framework/hocs'; - -import {withStopIgnoringUser} from 'coral-framework/graphql/mutations'; +import Slot from 'coral-framework/components/Slot'; import {link} from 'coral-framework/services/pym'; import NotLoggedIn from '../components/NotLoggedIn'; -import IgnoredUsers from '../components/IgnoredUsers'; import {Spinner} from 'coral-ui'; import CommentHistory from 'talk-plugin-history/CommentHistory'; @@ -55,7 +53,7 @@ class ProfileContainer extends Component { }; render() { - const {auth, auth: {user}, showSignInDialog, stopIgnoringUser, root, data} = this.props; + const {auth, auth: {user}, showSignInDialog, root, data} = this.props; const {me} = this.props.root; const loading = this.props.data.loading; @@ -77,15 +75,11 @@ class ProfileContainer extends Component {

{user.username}

{emailAddress ?

{emailAddress}

: null} - {me.ignoredUsers && me.ignoredUsers.length - ?
-

{t('framework.ignored_users')}

- -
- : null} +
@@ -98,8 +92,10 @@ class ProfileContainer extends Component { } } -// TODO: This Slot should be included in `talk-plugin-history` instead. const slots = [ + 'profileSections', + + // TODO: This Slot should be included in `talk-plugin-history` instead. 'commentContent', ]; @@ -143,10 +139,6 @@ const withProfileQuery = withQuery( query CoralEmbedStream_Profile { me { id - ignoredUsers { - id, - username, - } comments(query: {limit: 10}) { ...TalkSettings_CommentConnectionFragment } @@ -165,6 +157,5 @@ const mapDispatchToProps = (dispatch) => export default compose( connect(mapStateToProps, mapDispatchToProps), - withStopIgnoringUser, withProfileQuery )(ProfileContainer); diff --git a/graph/loaders/assets.js b/graph/loaders/assets.js index 956d573ef..b2e315ec5 100644 --- a/graph/loaders/assets.js +++ b/graph/loaders/assets.js @@ -25,8 +25,33 @@ const genAssetsByID = (context, ids) => AssetModel.find({ * @param {Object} query the query * @return {Promise} resolves the assets */ -const getAssetsByQuery = (context, query) => { - return AssetsService.search(query); +const getAssetsByQuery = async (context, query) => { + + // If we are requesting based on a limit, ask for one more than we want. + const limit = query.limit; + if (limit) { + query.limit += 1; + } + + const nodes = await AssetsService.search(query); + + // 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); + } + + return { + startCursor: nodes && nodes.length > 0 ? nodes[0].created_at : null, + endCursor: nodes && nodes.length > 0 ? nodes[nodes.length - 1].created_at : null, + hasNextPage, + nodes, + }; }; /** @@ -84,7 +109,7 @@ module.exports = (context) => ({ getByURL: (url) => findOrCreateAssetByURL(context, url), findByUrl: (url) => findByUrl(context, url), - search: (query) => getAssetsByQuery(context, query), + getByQuery: (query) => getAssetsByQuery(context, query), getByID: new DataLoader((ids) => genAssetsByID(context, ids)), getForMetrics: () => getAssetsForMetrics(context), getAll: new util.SingletonResolver(() => AssetModel.find({})) diff --git a/graph/loaders/settings.js b/graph/loaders/settings.js index 83545821d..2d6189b7d 100644 --- a/graph/loaders/settings.js +++ b/graph/loaders/settings.js @@ -1,5 +1,5 @@ const SettingsService = require('../../services/settings'); -const util = require('./util'); +const {SingletonResolver} = require('./util'); /** * Creates a set of loaders based on a GraphQL context. @@ -7,5 +7,5 @@ const util = require('./util'); * @return {Object} object of loaders */ module.exports = () => ({ - Settings: new util.SingletonResolver(() => SettingsService.retrieve()) + Settings: new SingletonResolver(() => SettingsService.retrieve()) }); diff --git a/graph/mutators/asset.js b/graph/mutators/asset.js new file mode 100644 index 000000000..462607f5f --- /dev/null +++ b/graph/mutators/asset.js @@ -0,0 +1,55 @@ +const errors = require('../../errors'); +const { + UPDATE_ASSET_SETTINGS, + UPDATE_ASSET_STATUS, +} = require('../../perms/constants'); + +const AssetsService = require('../../services/assets'); +const AssetModel = require('../../models/asset'); + +/** + * updateSettings will update the settings on an asset. + * + * @param {Object} ctx graphql context + * @param {String} id the asset's id to update + * @param {Object} settings the settings to update on the asset. + */ +const updateSettings = async (ctx, id, settings) => AssetsService.overrideSettings(id, settings); + +/** + * updateStatus will update the status of an asset. + * + * @param {Object} ctx graphql context + * @param {String} id the asset's id to update + * @param {Object} status the status to change on the asset relating to it's + * current state. + */ +const updateStatus = async (ctx, id, {closedAt, closedMessage}) => AssetModel.update({ + id, +}, { + $set: { + closedAt, + closedMessage + } +}); + +module.exports = (ctx) => { + let mutators = { + Asset: { + updateSettings: () => Promise.reject(errors.ErrNotAuthorized), + updateStatus: () => Promise.reject(errors.ErrNotAuthorized) + } + }; + + if (ctx.user) { + if (ctx.user.can(UPDATE_ASSET_SETTINGS)) { + mutators.Asset.updateSettings = (id, settings) => updateSettings(ctx, id, settings); + } + + if (ctx.user.can(UPDATE_ASSET_STATUS)) { + mutators.Asset.updateStatus = (id, status) => updateStatus(ctx, id, status); + } + } + + return mutators; +}; diff --git a/graph/mutators/index.js b/graph/mutators/index.js index e7e5df9e7..0f290c69a 100644 --- a/graph/mutators/index.js +++ b/graph/mutators/index.js @@ -3,6 +3,8 @@ const debug = require('debug')('talk:graph:mutators'); const Comment = require('./comment'); const Action = require('./action'); +const Asset = require('./asset'); +const Settings = require('./settings'); const Tag = require('./tag'); const Token = require('./token'); const User = require('./user'); @@ -14,6 +16,8 @@ let mutators = [ // Load in the core mutators. Comment, Action, + Asset, + Settings, Tag, Token, User, diff --git a/graph/mutators/settings.js b/graph/mutators/settings.js new file mode 100644 index 000000000..f725d791d --- /dev/null +++ b/graph/mutators/settings.js @@ -0,0 +1,33 @@ +const errors = require('../../errors'); + +const { + UPDATE_SETTINGS, + UPDATE_WORDLIST, +} = require('../../perms/constants'); + +const SettingsService = require('../../services/settings'); + +const update = async (ctx, settings) => SettingsService.update(settings); + +const updateWordlist = async (ctx, wordlist) => SettingsService.updateWordlist(wordlist); + +module.exports = (ctx) => { + let mutators = { + Settings: { + update: () => Promise.reject(errors.ErrNotAuthorized), + updateWordlist: () => Promise.reject(errors.ErrNotAuthorized) + } + }; + + if (ctx.user) { + if (ctx.user.can(UPDATE_SETTINGS)) { + mutators.Settings.update = (id, settings) => update(ctx, id, settings); + } + + if (ctx.user.can(UPDATE_WORDLIST)) { + mutators.Settings.updateWordlist = (id, status) => updateWordlist(ctx, id, status); + } + } + + return mutators; +}; diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 685c26a17..bfdc3991e 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -25,6 +25,12 @@ const RootMutation = { rejectUsername(_, {input: {id, message}}, {mutators: {User}}) { return wrapResponse(null)(User.rejectUsername({id, message})); }, + updateAssetSettings(_, {id, input: settings}, {mutators: {Asset}}) { + return wrapResponse(null)(Asset.updateSettings(id, settings)); + }, + updateAssetStatus(_, {id, input: status}, {mutators: {Asset}}) { + return wrapResponse(null)(Asset.updateStatus(id, status)); + }, ignoreUser(_, {id}, {mutators: {User}}) { return wrapResponse(null)(User.ignoreUser({id})); }, @@ -50,6 +56,12 @@ const RootMutation = { removeTag(_, {tag}, {mutators: {Tag}}) { return wrapResponse(null)(Tag.remove(tag)); }, + updateSettings(_, {input: settings}, {mutators: {Settings}}) { + return wrapResponse(null)(Settings.update(settings)); + }, + updateWordlist(_, {input: wordlist}, {mutators: {Settings}}) { + return wrapResponse(null)(Settings.updateWordlist(wordlist)); + }, createToken(_, {input}, {mutators: {Token}}) { return wrapResponse('token')(Token.create(input)); }, diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 239b052c0..13e41a312 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -11,7 +11,7 @@ const RootQuery = { return null; } - return Assets.search(query); + return Assets.getByQuery(query); }, asset(_, query, {loaders: {Assets}}) { if (query.id) { diff --git a/graph/resolvers/settings.js b/graph/resolvers/settings.js index 72608a9c4..30e614c38 100644 --- a/graph/resolvers/settings.js +++ b/graph/resolvers/settings.js @@ -1,3 +1,21 @@ +const { + VIEW_PROTECTED_SETTINGS, +} = require('../../perms/constants'); + +const {decorateWithPermissionCheck} = require('./util'); + const Settings = {}; +// PROTECTED_SETTINGS are the settings keys that must be protected for only some +// eyes. +const PROTECTED_SETTINGS = { + 'premodLinksEnable': [VIEW_PROTECTED_SETTINGS], + 'autoCloseStream': [VIEW_PROTECTED_SETTINGS], + 'wordlist': [VIEW_PROTECTED_SETTINGS], + 'domains': [VIEW_PROTECTED_SETTINGS], +}; + +// decorate the fields on the settings resolver with a permission check. +decorateWithPermissionCheck(Settings, PROTECTED_SETTINGS); + module.exports = Settings; diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js index ef3c70865..cf3162679 100644 --- a/graph/resolvers/util.js +++ b/graph/resolvers/util.js @@ -13,6 +13,31 @@ const decorateWithTags = (typeResolver) => { }; }; +/** + * decorateWithPermissionCheck will decorate the field resolver with + * permission checks. + * + * @param {Object} typeResolver the type resolver + * @param {Object} protect the object with field -> Array of permissions + */ +const decorateWithPermissionCheck = (typeResolver, protect) => { + for (const [field, permissions] of Object.entries(protect)) { + let fieldResolver = (obj) => obj[field]; + if (field in typeResolver) { + fieldResolver = typeResolver[field]; + } + + typeResolver[field] = (obj, args, ctx, info) => { + if (!ctx.user || !ctx.user.can(...permissions)) { + return null; + } + + return fieldResolver(obj, args, ctx, info); + }; + } +}; + module.exports = { - decorateWithTags + decorateWithTags, + decorateWithPermissionCheck, }; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 9d8e25f12..24d814311 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -164,7 +164,19 @@ input AssetsQuery { # Limit the number of results to be returned limit: Int = 10 + + # open filters assets that are open/closed/all. Not providing this parameter + # will return all the assets, true will return assets that are open, and false + # will return assets that are closed. + open: Boolean + + # sortOrder specifies the order of the sort for the returned Assets. + sortOrder: SORT_ORDER = DESC + + # Skip results from the last created_at timestamp. + cursor: Cursor } + ################################################################################ ## Tags ################################################################################ @@ -572,27 +584,85 @@ enum MODERATION_MODE { POST } -# Site wide global settings. +# Wordlist describes all the available wordlists. +type Wordlist { + + # banned words will by default reject the comment if it is found. + banned: [String!]! + + # suspect words will simply flag the comment. + suspect: [String!]! +} + +# Domains describes all the available lists of domains. +type Domains { + + # whitelist is the list of domains that the embed is allowed to render on. + whitelist: [String!]! +} + +# Settings stores the global settings for a given installation. type Settings { - # Moderation mode for the site. + # moderation is the moderation mode for all Asset's on the site. moderation: MODERATION_MODE! # Enables a requirement for email confirmation before a user can login. requireEmailConfirmation: Boolean + # infoBoxEnable will enable the Info Box content visible above the question + # box. infoBoxEnable: Boolean + + # infoBoxContent is the content of the Info Box. infoBoxContent: String - premodLinksEnable: Boolean + + # questionBoxEnable will enable the Question Box's content to be visible above + # the comment box. questionBoxEnable: Boolean + + # questionBoxContent is the content of the Question Box. questionBoxContent: String + + # premodLinksEnable will put all comments that contain links into premod. + premodLinksEnable: Boolean + + # questionBoxIcon is the icon for the Question Box. questionBoxIcon: String - closeTimeout: Int + + # autoCloseStream when true will auto close the stream when the `closeTimeout` + # amount of seconds have been reached. + autoCloseStream: Boolean + + # customCssUrl is the URL of the custom CSS used to display on the frontend. + customCssUrl: String + + # closedTimeout is the amount of seconds from the created_at timestamp that a + # given asset will be considered closed. + closedTimeout: Int + + # closedMessage is the message shown to the user when the given Asset is + # closed. closedMessage: String + + # editCommentWindowLength is the length of time (in milliseconds) after a + # comment is posted that it can still be edited by the author. + editCommentWindowLength: Int + + # charCountEnable is true when the character count restriction is enabled. charCountEnable: Boolean + + # charCount is the maximum number of characters a comment may be. charCount: Int + # organizationName is the name of the organization. organizationName: String + + # wordlist will return a given list of words. + wordlist: Wordlist + + # domains will return a given list of domains. + domains: Domains } ################################################################################ @@ -648,6 +718,22 @@ type Asset { author: String } +# AssetConnection represents a paginable subset of a asset list. +type AssetConnection { + + # Indicates that there are more assets after this subset. + hasNextPage: Boolean! + + # Cursor of first asset in subset. + startCursor: Cursor + + # Cursor of last asset in subset. + endCursor: Cursor + + # Subset of assets. + nodes: [Asset!]! +} + ################################################################################ ## Errors ################################################################################ @@ -735,7 +821,7 @@ type RootQuery { comment(id: ID!): Comment # All assets. Requires the `ADMIN` role. - assets(query: AssetsQuery): [Asset] + assets(query: AssetsQuery): AssetConnection # Find or create an asset by url, or just find with the ID. asset(id: ID, url: String): Asset @@ -905,6 +991,56 @@ input RejectUsernameInput { message: String! } +# Configurable settings that can be overridden for the Asset. You must specify +# all fields that should be updated. +input AssetSettingsInput { + + # premodLinksEnable will put all comments that contain links into premod. + premodLinksEnable: Boolean + + # moderation is the moderation mode for the asset. + moderation: MODERATION_MODE + + # questionBoxEnable will enable the Question Boxs' content to be visable above + # the comment box. + questionBoxEnable: Boolean + + # questionBoxContent is the content of the Question Box. + questionBoxContent: String + + # questionBoxIcon is the icon for the Question Box. + questionBoxIcon: String +} + +# UpdateAssetStatusInput contains the input to change the status of a comment as +# it relates to being open/closed for commenting. +input UpdateAssetStatusInput { + + # closedAt is the time that the asset will be closed for commenting. If this + # is null or in the future, it will be open for commenting. + closedAt: Date + + # closedMessage is the message to be set on the asset when it is closed. If it + # is null, then the message will default to the globally set `closedMessage`. + closedMessage: String +} + +# UpdateAssetStatusResponse is the response returned with possibly some errors +# relating to the update status attempt. +type UpdateAssetStatusResponse implements Response { + + # An array of errors relating to the mutation that occurred. + errors: [UserError!] +} + +# UpdateAssetSettingsResponse is the response returned with possibly some errors +# relating to the update settings attempt. +type UpdateAssetSettingsResponse implements Response { + + # An array of errors relating to the mutation that occurred. + errors: [UserError!] +} + # DeleteActionResponse is the response returned with possibly some errors # relating to the delete action attempt. type DeleteActionResponse implements Response { @@ -998,6 +1134,91 @@ type EditCommentResponse implements Response { errors: [UserError!] } +# UpdateSettingsInput is the input used to input the global site settings. This +# will override the existing settings, so all fields must be included. +input UpdateSettingsInput { + + # moderation is the moderation mode for all Asset's on the site. + moderation: MODERATION_MODE + + # Enables a requirement for email confirmation before a user can login. + requireEmailConfirmation: Boolean + + # infoBoxEnable will enable the Info Box content visible above the question + # box. + infoBoxEnable: Boolean + + # infoBoxContent is the content of the Info Box. + infoBoxContent: String + + # questionBoxEnable will enable the Question Box's content to be visible above + # the comment box. + questionBoxEnable: Boolean + + # questionBoxContent is the content of the Question Box. + questionBoxContent: String + + # premodLinksEnable will put all comments that contain links into premod. + premodLinksEnable: Boolean + + # questionBoxIcon is the icon for the Question Box. + questionBoxIcon: String + + # autoCloseStream when true will auto close the stream when the `closeTimeout` + # amount of seconds have been reached. + autoCloseStream: Boolean + + # customCssUrl is the URL of the custom CSS used to display on the frontend. + customCssUrl: String + + # closedTimeout is the amount of seconds from the created_at timestamp that a + # given asset will be considered closed. + closedTimeout: Int + + # closedMessage is the message shown to the user when the given Asset is + # closed. + closedMessage: String + + # charCountEnable is true when the character count restriction is enabled. + charCountEnable: Boolean + + # charCount is the maximum number of characters a comment may be. + charCount: Int + + # organizationName is the name of the organization. + organizationName: String + + # editCommentWindowLength is the length of time (in milliseconds) after a + # comment is posted that it can still be edited by the author. + editCommentWindowLength: Int +} + +# UpdateSettingsResponse contains any errors that were rendered as a result +# of the mutation. +type UpdateSettingsResponse implements Response { + + # An array of errors relating to the mutation that occurred. + errors: [UserError!] +} + +# UpdateWordlistInput is the list of words that composes the Wordlist. +input UpdateWordlistInput { + + # banned words will by default reject the comment if it is found. + banned: [String!]! + + # suspect words will simply flag the comment. + suspect: [String!]! +} + +# UpdateWordlistResponse contains any errors that were rendered as a result +# of the mutation. +type UpdateWordlistResponse implements Response { + + # An array of errors relating to the mutation that occurred. + errors: [UserError!] +} + # CreateTokenInput contains the input to create the token. input CreateTokenInput { @@ -1033,31 +1254,35 @@ type RevokeTokenResponse implements Response { type RootMutation { # Creates a comment on the asset. - createComment(comment: CreateCommentInput!): CreateCommentResponse + createComment(comment: CreateCommentInput!): CreateCommentResponse! # Creates a flag on an entity. - createFlag(flag: CreateFlagInput!): CreateFlagResponse + createFlag(flag: CreateFlagInput!): CreateFlagResponse! # Creates a don't agree action on an entity. - createDontAgree(dontagree: CreateDontAgreeInput!): CreateDontAgreeResponse + createDontAgree(dontagree: CreateDontAgreeInput!): CreateDontAgreeResponse! # Delete an action based on the action id. deleteAction(id: ID!): DeleteActionResponse # Edit a comment - editComment(id: ID!, asset_id: ID!, edit: EditCommentInput): EditCommentResponse + editComment(id: ID!, asset_id: ID!, edit: EditCommentInput): EditCommentResponse! # Sets User status. Requires the `ADMIN` role. - setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse + # Mutation is restricted. + setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse! # Suspends a user. Requires the `ADMIN` role. - suspendUser(input: SuspendUserInput!): SuspendUserResponse + # Mutation is restricted. + suspendUser(input: SuspendUserInput!): SuspendUserResponse! # Reject a username. Requires the `ADMIN` role. - rejectUsername(input: RejectUsernameInput!): RejectUsernameResponse + # Mutation is restricted. + rejectUsername(input: RejectUsernameInput!): RejectUsernameResponse! # Sets Comment status. Requires the `ADMIN` role. - setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse + # Mutation is restricted. + setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse! # Add a tag. addTag(tag: ModifyTagInput!): ModifyTagResponse! @@ -1065,17 +1290,36 @@ type RootMutation { # Removes a tag. removeTag(tag: ModifyTagInput!): ModifyTagResponse! + # Updates settings on a given asset. + # Mutation is restricted. + updateAssetSettings(id: ID!, input: AssetSettingsInput!): UpdateAssetSettingsResponse! + + # Updates the status of an asset allowing you to close/reopen an asset for + # commenting. + # Mutation is restricted. + updateAssetStatus(id: ID!, input: UpdateAssetStatusInput!): UpdateAssetStatusResponse! + + # updateSettings will update the global settings. + # Mutation is restricted. + updateSettings(input: UpdateSettingsInput!): UpdateSettingsResponse! + + # updateWordlist will update the given Wordlist. + # Mutation is restricted. + updateWordlist(input: UpdateWordlistInput!): UpdateWordlistResponse! + # Ignore comments by another user ignoreUser(id: ID!): IgnoreUserResponse # CreateToken will create a token that is attached to the current user. + # Mutation is restricted. createToken(input: CreateTokenInput!): CreateTokenResponse! # RevokeToken will revoke an existing token. + # Mutation is restricted. revokeToken(input: RevokeTokenInput!): RevokeTokenResponse! - # Stop Ignoring comments by another user - stopIgnoringUser(id: ID!): StopIgnoringUserResponse + # Stop Ignoring comments by another user. + stopIgnoringUser(id: ID!): StopIgnoringUserResponse! } ################################################################################ diff --git a/locales/en.yml b/locales/en.yml index 791bc1121..4d215fd06 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -219,7 +219,6 @@ en: framework: banned_account_header: "Your account is currently banned." banned_account_body: "This means that you cannot Like, Report, or write comments." - because_you_ignored: "Because you ignored the following commenters, their comments are hidden." comment: comment comment_is_ignored: "This comment is hidden because you ignored this user." comments: comments @@ -230,13 +229,11 @@ en: error: "Usernames can contain letters numbers and _ only" label: "New Username" msg: "Your account is currently suspended because your username has been deemed inappropriate. To restore your account please enter a new username. Please contact us if you have any questions." - ignored_users: "Ignored users" my_comments: "My Comments" my_profile: "My profile" new_count: "View {0} new {1}" profile: Profile show_all_comments: "Show all comments" - stop_ignoring: "Stop ignoring" success_bio_update: "Your biography has been updated" success_name_update: "Your username has been updated" success_update_settings: "The changes you have made have been applied to the comment stream on this article" diff --git a/locales/es.yml b/locales/es.yml index 99f55d32b..f9b675a10 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -227,13 +227,11 @@ es: error: "Nombres de usuarios pueden solamente incluir letras, números y _" label: "Nuevo Nombre" msg: "Tu cuenta está suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario. Contáctanos si tienes alguna pregunta." - ignored_users: "Usuarios ignorados" my_comments: "Mis Comentarios" my_profile: "Mi perfil" new_count: "Ver {0} {1} nuevo" profile: Perfil show_all_comments: "Mostrar todos los comentarios" - stop_ignoring: "No ignorar más" success_bio_update: "Tu biografia fue actualizada" success_name_update: "Tu nombre de usuario ha sido actualizado" success_update_settings: "La configuración de este articulo fue actualizada" diff --git a/locales/fr.yml b/locales/fr.yml index 77231e17b..5ece19c67 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -188,7 +188,6 @@ fr: error: "Les noms d'utilisateur ne peuvent contenir que des chiffres, des lettres et \"_\"" label: "Nouveau nom d'utilisateur" msg: "Votre compte est actuellement suspendu car votre nom d'utilisateur a été jugé inapproprié. Pour restaurer votre compte, entrez un nouveau nom d'utilisateur. Contactez-nous si vous avez des questions." - ignored_users: "Utilisateurs ignorés" my_comments: "Mes commentaires" my_profile: "Mon profil" new_count: "Voir {0} nouveau {1}" diff --git a/locales/pt_BR.yml b/locales/pt_BR.yml index 0e606fd81..e2e93c531 100644 --- a/locales/pt_BR.yml +++ b/locales/pt_BR.yml @@ -214,7 +214,6 @@ pt_BR: framework: banned_account_header: "Sua conta está atualmente proibida." banned_account_body: "Isso significa que você não pode gostar, informar ou escrever comentários." - because_you_ignored: "Porque você ignorou os seguintes comentadores, seus comentários estão ocultos." comment: comentário comment_is_ignored: "Este comentário está oculto porque você ignorou esse usuário." comments: comentários @@ -225,13 +224,11 @@ pt_BR: error: "Nomes de usuários podem conter números de letras e _ somente" label: "Novo usuário" msg: "Sua conta está suspensa porque seu nome de usuário foi considerado inapropriado. Para restaurar sua conta, insira um novo nome de usuário. Entre em contato conosco se você tiver alguma dúvida." - ignored_users: "Usuários ignorados" my_comments: "Meus comentários" my_profile: "Meu perfil" new_count: "Ver {0} {1}" profile: Perfil show_all_comments: "Exibir todos os comentários" - stop_ignoring: "Pare de ignorar" success_bio_update: "Sua biografia foi atualizada" success_name_update: "Seu nome de usuário foi atualizado" success_update_settings: "As alterações que você fez foram aplicadas no hilo de comentários neste artigo" diff --git a/package.json b/package.json index a5127f6ac..8c45239e3 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "material-design-lite": "^1.2.1", "metascraper": "^1.0.7", "minimist": "^1.2.0", + "moment": "^2.18.1", "mongoose": "^4.11.7", "morgan": "^1.8.2", "ms": "^2.0.0", diff --git a/perms/constants.js b/perms/constants.js index 383f36daf..070fe80bf 100644 --- a/perms/constants.js +++ b/perms/constants.js @@ -16,6 +16,10 @@ module.exports = { UPDATE_CONFIG: 'UPDATE_CONFIG', CREATE_TOKEN: 'CREATE_TOKEN', REVOKE_TOKEN: 'REVOKE_TOKEN', + UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS', + UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS', + UPDATE_SETTINGS: 'UPDATE_SETTINGS', + UPDATE_WORDLIST: 'UPDATE_WORDLIST', // queries SEARCH_ASSETS: 'SEARCH_ASSETS', @@ -27,6 +31,7 @@ module.exports = { LIST_OWN_TOKENS: 'LIST_OWN_TOKENS', SEARCH_COMMENT_STATUS_HISTORY: 'SEARCH_COMMENT_STATUS_HISTORY', VIEW_SUSPENSION_INFO: 'VIEW_SUSPENSION_INFO', + VIEW_PROTECTED_SETTINGS: 'VIEW_PROTECTED_SETTINGS', // subscriptions SUBSCRIBE_COMMENT_ACCEPTED: 'SUBSCRIBE_COMMENT_ACCEPTED', diff --git a/perms/index.js b/perms/index.js index be6e7a2cd..a6b459efe 100644 --- a/perms/index.js +++ b/perms/index.js @@ -41,11 +41,8 @@ const findGrant = (user, perms) => { */ module.exports = (user, ...perms) => { - // make sure all the passed permissions are not typos - const missingPerms = perms.filter((perm) => { - return allPermissions.indexOf(perm) === -1; - }); - + // Make sure all the passed permissions are not typos. + const missingPerms = perms.filter((perm) => !allPermissions.includes(perm)); if (missingPerms.length > 0) { throw new Error(`${missingPerms.join(' ')} are not valid permissions.`); } diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js index ec97fe7a2..2e8718ed1 100644 --- a/perms/mutationReducer.js +++ b/perms/mutationReducer.js @@ -4,33 +4,26 @@ const types = require('./constants'); module.exports = (user, perm) => { switch (perm) { case types.CREATE_COMMENT: - return true; case types.CREATE_ACTION: - return true; case types.DELETE_ACTION: - return true; case types.EDIT_NAME: - return true; case types.EDIT_COMMENT: return true; - case types.UPDATE_USER_ROLES: - return check(user, ['ADMIN']); - case types.REJECT_USERNAME: - return check(user, ['ADMIN', 'MODERATOR']); - case types.SET_USER_STATUS: - return check(user, ['ADMIN', 'MODERATOR']); - case types.SUSPEND_USER: - return check(user, ['ADMIN', 'MODERATOR']); - case types.SET_COMMENT_STATUS: - return check(user, ['ADMIN', 'MODERATOR']); case types.ADD_COMMENT_TAG: - return check(user, ['ADMIN', 'MODERATOR', 'STAFF']); case types.REMOVE_COMMENT_TAG: return check(user, ['ADMIN', 'MODERATOR', 'STAFF']); + case types.UPDATE_USER_ROLES: + case types.REJECT_USERNAME: + case types.SET_USER_STATUS: + case types.SUSPEND_USER: + case types.SET_COMMENT_STATUS: case types.UPDATE_CONFIG: + case types.UPDATE_SETTINGS: + case types.UPDATE_WORDLIST: + case types.UPDATE_ASSET_SETTINGS: + case types.UPDATE_ASSET_STATUS: return check(user, ['ADMIN', 'MODERATOR']); case types.CREATE_TOKEN: - return check(user, ['ADMIN']); case types.REVOKE_TOKEN: return check(user, ['ADMIN']); default: diff --git a/perms/queryReducer.js b/perms/queryReducer.js index 0b76c225a..b71c3d964 100644 --- a/perms/queryReducer.js +++ b/perms/queryReducer.js @@ -21,6 +21,8 @@ module.exports = (user, perm) => { return check(user, ['ADMIN', 'MODERATOR']); case types.VIEW_SUSPENSION_INFO: return check(user, ['ADMIN', 'MODERATOR']); + case types.VIEW_PROTECTED_SETTINGS: + return check(user, ['ADMIN', 'MODERATOR']); default: break; } diff --git a/plugin-api/beta/client/actions/notification.js b/plugin-api/beta/client/actions/notification.js index c1a75cb7a..0fae2b983 100644 --- a/plugin-api/beta/client/actions/notification.js +++ b/plugin-api/beta/client/actions/notification.js @@ -1 +1 @@ -export {addNotification} from 'coral-framework/actions/notification'; +export {notify} from 'coral-framework/actions/notification'; diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js index 1bc47868e..15c8b3544 100644 --- a/plugin-api/beta/client/hocs/index.js +++ b/plugin-api/beta/client/hocs/index.js @@ -5,3 +5,7 @@ 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'; export {default as withEmit} from 'coral-framework/hocs/withEmit'; +export { + withIgnoreUser, + withStopIgnoringUser, +} from 'coral-framework/graphql/mutations'; diff --git a/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js index 6dd18f3a1..3fe742569 100644 --- a/plugin-api/beta/client/utils/index.js +++ b/plugin-api/beta/client/utils/index.js @@ -7,4 +7,5 @@ export { capitalize, getErrorMessages, getDefinitionName, + getShallowChanges, } from 'coral-framework/utils'; diff --git a/plugins.default.json b/plugins.default.json index 0fcc70681..950cec229 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -17,6 +17,9 @@ "talk-plugin-sort-newest", "talk-plugin-sort-oldest", "talk-plugin-sort-most-respected", - "talk-plugin-sort-most-replied" + "talk-plugin-sort-most-replied", + "talk-plugin-author-menu", + "talk-plugin-member-since", + "talk-plugin-ignore-user" ] } diff --git a/plugins/talk-plugin-author-menu/client/.babelrc b/plugins/talk-plugin-author-menu/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-author-menu/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-author-menu/client/.eslintrc.json b/plugins/talk-plugin-author-menu/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-author-menu/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-author-menu/client/actions.js b/plugins/talk-plugin-author-menu/client/actions.js new file mode 100644 index 000000000..960e66bb4 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/actions.js @@ -0,0 +1,19 @@ +import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT, OPEN_MENU, CLOSE_MENU} from './constants'; + +export const setContentSlot = (slot) => ({ + type: SET_CONTENT_SLOT, + slot, +}); + +export const resetContentSlot = () => ({ + type: RESET_CONTENT_SLOT, +}); + +export const openMenu = (id) => ({ + type: OPEN_MENU, + id, +}); + +export const closeMenu = () => ({ + type: CLOSE_MENU, +}); diff --git a/plugins/talk-plugin-author-menu/client/components/AuthorName.css b/plugins/talk-plugin-author-menu/client/components/AuthorName.css new file mode 100644 index 000000000..d9a8bdd71 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/components/AuthorName.css @@ -0,0 +1,16 @@ +.root { + display: inline-block; + position: relative; +} + +.button { + composes: buttonReset from "coral-framework/styles/reset.css"; + &:hover { + text-decoration: underline; + } +} + +.name { + font-weight: bold; +} + diff --git a/plugins/talk-plugin-author-menu/client/components/AuthorName.js b/plugins/talk-plugin-author-menu/client/components/AuthorName.js new file mode 100644 index 000000000..191f93490 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/components/AuthorName.js @@ -0,0 +1,31 @@ +import React from 'react'; +import Menu from './Menu'; +import styles from './AuthorName.css'; +import {ClickOutside} from 'plugin-api/beta/client/components'; +import cn from 'classnames'; + +export default ({data, root, asset, comment, contentSlot, menuVisible, toggleMenu, hideMenu}) => { + return ( + +
+ + {menuVisible && + + } +
+
+ ); +}; diff --git a/plugins/talk-plugin-author-menu/client/components/Menu.css b/plugins/talk-plugin-author-menu/client/components/Menu.css new file mode 100644 index 000000000..dd149ce18 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/components/Menu.css @@ -0,0 +1,45 @@ + +.menu { + background-color: white; + border: solid 1px #999; + border-radius: 3px; + padding: 10px; + position: absolute; + -webkit-box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2); + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2); + z-index: 9; + top: 26px; + left: 0px; + min-width: 20px; + text-align: left; +} + +.menu::before{ + content: ''; + border: 10px solid transparent; + border-top-color: #999; + position: absolute; + left: 6px; + top: -21px; + transform: rotate(180deg); +} + +.menu::after{ + content: ''; + border: 10px solid transparent; + border-top-color: white; + position: absolute; + left: 6px; + top: -20px; + transform: rotate(180deg); +} + +.actions { + display: flex; + justify-content: flex-end; + margin-top: 8px; + + &:empty { + display: none; + } +} diff --git a/plugins/talk-plugin-author-menu/client/components/Menu.js b/plugins/talk-plugin-author-menu/client/components/Menu.js new file mode 100644 index 000000000..0c4db07d9 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/components/Menu.js @@ -0,0 +1,35 @@ +import React from 'react'; +import styles from './Menu.css'; +import {Slot} from 'plugin-api/beta/client/components'; +import cn from 'classnames'; + +export default ({data, root, asset, comment, contentSlot}) => { + if (contentSlot) { + return ( +
+ +
+ ); + } + + return ( +
+ + +
+ ); +}; diff --git a/plugins/talk-plugin-author-menu/client/constants.js b/plugins/talk-plugin-author-menu/client/constants.js new file mode 100644 index 000000000..1d14e94d4 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/constants.js @@ -0,0 +1,7 @@ +const prefix = 'TALK_AUTHOR_MENU'; + +export const SET_CONTENT_SLOT = `${prefix}_SET_CONTENT_SLOT`; +export const RESET_CONTENT_SLOT = `${prefix}_RESET_CONTENT_SLOT`; +export const OPEN_MENU = `${prefix}_OPEN_MENU`; +export const CLOSE_MENU = `${prefix}_CLOSE_MENU`; + diff --git a/plugins/talk-plugin-author-menu/client/containers/AuthorName.js b/plugins/talk-plugin-author-menu/client/containers/AuthorName.js new file mode 100644 index 000000000..78663b603 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/containers/AuthorName.js @@ -0,0 +1,102 @@ +import React from 'react'; +import {connect, withFragments} from 'plugin-api/beta/client/hocs'; +import {bindActionCreators} from 'redux'; +import AuthorName from '../components/AuthorName'; +import {setContentSlot, resetContentSlot, openMenu, closeMenu} from '../actions'; +import {compose, gql} from 'react-apollo'; +import {getSlotFragmentSpreads, getShallowChanges} from 'plugin-api/beta/client/utils'; + +class AuthorNameContainer extends React.Component { + + shouldComponentUpdate(nextProps) { + + // Specifically handle `showMenuForComment` if it is the only change. + const changes = getShallowChanges(this.props, nextProps); + if (changes.length === 1 && changes[0] === 'showMenuForComment') { + const commentId = this.props.comment.id; + if ( + commentId !== this.props.showMenuForComment && + commentId !== nextProps.showMenuForComment + ) { + return false; + } + } + + // Prevent Slot from rerendering when no props has shallowly changed. + return changes.length !== 0; + } + + toggleMenu = () => { + if (this.props.showMenuForComment === this.props.comment.id) { + this.props.closeMenu(); + } else { + this.props.openMenu(this.props.comment.id); + } + } + + hideMenu = () => { + if (this.props.showMenuForComment === this.props.comment.id) { + this.props.closeMenu(); + } + } + + render() { + return ; + } +} + +const slots = [ + 'authorMenuInfos', + 'authorMenuActions', +]; + +const mapStateToProps = ({talkPluginAuthorMenu: state}) => ({ + contentSlot: state.contentSlot, + showMenuForComment: state.showMenuForComment, +}); + +const mapDispatchToProps = (dispatch) => + bindActionCreators({ + setContentSlot, + resetContentSlot, + openMenu, + closeMenu, + }, dispatch); + +const withAuthorNameFragments = withFragments({ + root: gql` + fragment TalkAuthorMenu_AuthorName_root on RootQuery { + __typename + ${getSlotFragmentSpreads(slots, 'root')} + }`, + asset: gql` + fragment TalkAuthorMenu_AuthorName_asset on Asset { + __typename + ${getSlotFragmentSpreads(slots, 'asset')} + }`, + comment: gql` + fragment TalkAuthorMenu_AuthorName_comment on Comment { + __typename + id + user { + username + } + ${getSlotFragmentSpreads(slots, 'comment')} + }`, +}); + +const enhance = compose( + connect(mapStateToProps, mapDispatchToProps), + withAuthorNameFragments, +); + +export default enhance(AuthorNameContainer); diff --git a/plugins/talk-plugin-author-menu/client/index.js b/plugins/talk-plugin-author-menu/client/index.js new file mode 100644 index 000000000..a98a15822 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/index.js @@ -0,0 +1,11 @@ +import AuthorName from './containers/AuthorName'; +import reducer from './reducer'; +import translations from './translations.yml'; + +export default { + reducer, + slots: { + commentAuthorName: [AuthorName] + }, + translations +}; diff --git a/plugins/talk-plugin-author-menu/client/reducer.js b/plugins/talk-plugin-author-menu/client/reducer.js new file mode 100644 index 000000000..822c2a6a8 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/reducer.js @@ -0,0 +1,34 @@ +import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT, OPEN_MENU, CLOSE_MENU} from './constants'; + +const initialState = { + contentSlot: null, + showMenuForComment: null, +}; + +export default function reducer(state = initialState, action) { + switch (action.type) { + case SET_CONTENT_SLOT: + return { + ...state, + contentSlot: action.slot, + }; + case RESET_CONTENT_SLOT: + return { + ...state, + contentSlot: initialState.contentSlot, + }; + case OPEN_MENU: + return { + ...state, + showMenuForComment: action.id, + }; + case CLOSE_MENU: + return { + ...state, + showMenuForComment: null, + contentSlot: null, + }; + default : + return state; + } +} diff --git a/plugins/talk-plugin-author-menu/client/translations.yml b/plugins/talk-plugin-author-menu/client/translations.yml new file mode 100644 index 000000000..adac91ad4 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/translations.yml @@ -0,0 +1,4 @@ +en: + talk-plugin-author-menu: +es: + talk-plugin-author-menu: diff --git a/plugins/talk-plugin-author-menu/index.js b/plugins/talk-plugin-author-menu/index.js new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/plugins/talk-plugin-author-menu/index.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/plugins/talk-plugin-ignore-user/client/.babelrc b/plugins/talk-plugin-ignore-user/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-ignore-user/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-ignore-user/client/.eslintrc.json b/plugins/talk-plugin-ignore-user/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-ignore-user/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-ignore-user/client/components/IgnoreUserAction.css b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.css new file mode 100644 index 000000000..ac818278c --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.css @@ -0,0 +1,17 @@ +.root { + white-space: nowrap; +} + +.button { + composes: buttonReset from "coral-framework/styles/reset.css"; + border: 1px solid rgba(1, 1, 1, 0.8); + border-radius: 1px; + padding: 3px 6px; + font-size: 12px; + transition: color 100ms, background 100ms; + + &:hover { + background-color: rgba(1, 1, 1, 0.8); + color: white; + } +} diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.js b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.js new file mode 100644 index 000000000..19319ec9c --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.js @@ -0,0 +1,12 @@ +import React from 'react'; +import styles from './IgnoreUserAction.css'; +import {t} from 'plugin-api/beta/client/services'; +import cn from 'classnames'; + +export default ({ignoreUser}) => ( + +); diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css new file mode 100644 index 000000000..4f3d8e9da --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css @@ -0,0 +1,45 @@ +.root { + width: 200px; +} + +.actions { + display: flex; + justify-content: flex-end; + margin-top: 8px; +} + +.message { + font-size: 13px; +} + +.title { + padding: 0; + margin: 0 0 8px 0; + font-size: 15px; +} + +.button { + composes: buttonReset from "coral-framework/styles/reset.css"; + border: 1px solid rgba(1, 1, 1, 0.8); + border-radius: 1px; + padding: 3px 6px; + font-size: 12px; + transition: color 100ms, background 100ms; + + &:hover { + background-color: rgba(1, 1, 1, 0.8); + color: white; + } +} + +.cancel { + composes: buttonReset from "coral-framework/styles/reset.css"; + padding: 3px 6px; + font-size: 12px; + transition: color 100ms; + margin-right: 4px; + + &:hover { + color: rgba(1, 1, 1, 0.75); + } +} diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js new file mode 100644 index 000000000..8da879f62 --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js @@ -0,0 +1,23 @@ +import React from 'react'; +import styles from './IgnoreUserConfirmation.css'; +import {t} from 'plugin-api/beta/client/services'; +import cn from 'classnames'; + +export default ({ignoreUser, cancel, username}) => ( + +); diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.css b/plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.css new file mode 100644 index 000000000..b279101ea --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.css @@ -0,0 +1,22 @@ +.list { + display: table; + list-style-type: none; + margin-left: 0; + padding-left: 0; +} + +.listItem { + display: table-row; +} + +.username { + display: table-cell; +} + +.button { + composes: buttonReset from "coral-framework/styles/reset.css"; + + margin-left: 16px; + color: #D0011B; + text-decoration: underline; +} diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.js b/plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.js new file mode 100644 index 000000000..3946626cd --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.js @@ -0,0 +1,23 @@ +import React from 'react'; +import styles from './IgnoredUserSection.css'; +import {t} from 'plugin-api/beta/client/services'; + +export default ({ignoredUsers, stopIgnoringUser}) => ( +
+

{t('talk-plugin-ignore-user.section_title')}

+

{t('talk-plugin-ignore-user.section_info')}

+
    + {ignoredUsers.map(({username, id}) => ( +
  • + {username} + +
  • + ))} +
+
+); diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js new file mode 100644 index 000000000..d7e9d38b5 --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js @@ -0,0 +1,53 @@ +import React from 'react'; +import IgnoreUserAction from '../components/IgnoreUserAction'; +import {compose, gql} from 'react-apollo'; +import {connect, withFragments, excludeIf} from 'plugin-api/beta/client/hocs'; +import {bindActionCreators} from 'redux'; +import {setContentSlot} from 'plugins/talk-plugin-author-menu/client/actions'; +import IgnoreUserConfirmation from './IgnoreUserConfirmation'; +import {getDefinitionName} from 'plugin-api/beta/client/utils'; + +class IgnoreUserActionContainer extends React.Component { + + ignoreUser = () => { + this.props.setContentSlot('ignoreUserConfirmation'); + }; + + render() { + return ; + } +} + +const mapDispatchToProps = (dispatch) => + bindActionCreators({ + setContentSlot, + }, dispatch); + +const withIgnoreUserActionFragments = withFragments({ + root: gql` + fragment TalkIgnoreUser_IgnoreUserAction_root on RootQuery { + me { + id + } + } + `, + comment: gql` + fragment TalkIgnoreUser_IgnoreUserAction_comment on Comment { + user { + id + } + ...${getDefinitionName(IgnoreUserConfirmation.fragments.comment)} + } + ${IgnoreUserConfirmation.fragments.comment} + `, +}); + +const enhance = compose( + connect(null, mapDispatchToProps), + withIgnoreUserActionFragments, + excludeIf(({root: {me}, comment}) => !me || me.id === comment.user.id), +); + +export default enhance(IgnoreUserActionContainer); diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js new file mode 100644 index 000000000..fdf6d1986 --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js @@ -0,0 +1,60 @@ +import React from 'react'; +import IgnoreUserConfirmation from '../components/IgnoreUserConfirmation'; +import {compose, gql} from 'react-apollo'; +import {connect, withFragments, withIgnoreUser} from 'plugin-api/beta/client/hocs'; +import {bindActionCreators} from 'redux'; +import {closeMenu} from 'plugins/talk-plugin-author-menu/client/actions'; +import {notify} from 'plugin-api/beta/client/actions/notification'; +import {t} from 'plugin-api/beta/client/services'; +import {getErrorMessages} from 'plugin-api/beta/client/utils'; + +class IgnoreUserConfirmationContainer extends React.Component { + + ignoreUser = () => { + const {ignoreUser, notify, comment, closeMenu} = this.props; + ignoreUser(comment.user.id) + .then(() => { + notify('success', t('talk-plugin-ignore-user.notify_success', comment.user.username)); + }) + .catch((err) => { + notify('error', getErrorMessages(err)); + }); + closeMenu(); + }; + + cancel = () => { + this.props.closeMenu(); + } + + render() { + return ; + } +} + +const mapDispatchToProps = (dispatch) => + bindActionCreators({ + closeMenu, + notify, + }, dispatch); + +const withIgnoreUserConfirmationFragments = withFragments({ + comment: gql` + fragment TalkIgnoreUser_IgnoreUserConfirmation_comment on Comment { + user { + id + username + } + }`, +}); + +const enhance = compose( + connect(null, mapDispatchToProps), + withIgnoreUserConfirmationFragments, + withIgnoreUser, +); + +export default enhance(IgnoreUserConfirmationContainer); diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoredUserSection.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoredUserSection.js new file mode 100644 index 000000000..e420f180b --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoredUserSection.js @@ -0,0 +1,36 @@ +import React from 'react'; +import IgnoredUserSection from '../components/IgnoredUserSection'; +import {compose, gql} from 'react-apollo'; +import {withFragments, excludeIf, withStopIgnoringUser} from 'plugin-api/beta/client/hocs'; + +class IgnoredUserSectionContainer extends React.Component { + + render() { + return ; + } +} + +const withIgnoredUserSectionFragments = withFragments({ + root: gql` + fragment TalkIgnoreUser_IgnoredUserSection_root on RootQuery { + me { + id + ignoredUsers { + id, + username, + } + } + } + `, +}); + +const enhance = compose( + withIgnoredUserSectionFragments, + withStopIgnoringUser, + excludeIf(({root: {me}}) => me.ignoredUsers.length === 0), +); + +export default enhance(IgnoredUserSectionContainer); diff --git a/plugins/talk-plugin-ignore-user/client/index.js b/plugins/talk-plugin-ignore-user/client/index.js new file mode 100644 index 000000000..6583f68e6 --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/index.js @@ -0,0 +1,43 @@ +import IgnoreUserAction from './containers/IgnoreUserAction'; +import IgnoreUserConfirmation from './containers/IgnoreUserConfirmation'; +import IgnoredUserSection from './containers/IgnoredUserSection'; +import translations from './translations.yml'; +import update from 'immutability-helper'; + +export default { + slots: { + authorMenuActions: [IgnoreUserAction], + ignoreUserConfirmation: [IgnoreUserConfirmation], + profileSections: [IgnoredUserSection], + }, + translations, + mutations: { + IgnoreUser: ({variables}) => ({ + updateQueries: { + CoralEmbedStream_Embed: (previousData) => { + const ignoredUserId = variables.id; + const updated = update(previousData, {me: {ignoredUsers: {$push: [{ + id: ignoredUserId, + __typename: 'User', + }]}}}); + return updated; + } + } + }), + StopIgnoringUser: ({variables}) => ({ + updateQueries: { + CoralEmbedStream_Profile: (previousData) => { + const noLongerIgnoredUserId = variables.id; + + // remove noLongerIgnoredUserId from ignoredUsers + const updated = update(previousData, {me: {ignoredUsers: { + $apply: (ignoredUsers) => { + return ignoredUsers.filter((u) => u.id !== noLongerIgnoredUserId); + } + }}}); + return updated; + } + } + }), + }, +}; diff --git a/plugins/talk-plugin-ignore-user/client/translations.yml b/plugins/talk-plugin-ignore-user/client/translations.yml new file mode 100644 index 000000000..a6c051ada --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/translations.yml @@ -0,0 +1,25 @@ +en: + talk-plugin-ignore-user: + section_title: Ignored users + section_info: Because you ignored the following commenters, their comments are hidden. + stop_ignoring: Stop ignoring + ignore_user: Ignore User + cancel: Cancel + confirmation: | + When you ignore a user, all comments they wrote on the site will be hidden from you. You can + undo this later from My Profile. + notify_success: | + You are now ignoring {0}. You can undo this action from My Profile. + confirmation_title: Ignore {0}? +es: + talk-plugin-ignore-user: + section_title: "Usuarios ignorados" + stop_ignoring: "No ignorar más" +fr: + talk-plugin-ignore-user: + section_title: "Utilisateurs ignorés" +pt_Br: + talk-plugin-ignore-user: + section_title: "Usuários ignorados" + section_info: "Porque você ignorou os seguintes comentadores, seus comentários estão ocultos." + stop_ignoring: "Pare de ignorar" diff --git a/plugins/talk-plugin-ignore-user/index.js b/plugins/talk-plugin-ignore-user/index.js new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/plugins/talk-plugin-ignore-user/index.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/plugins/talk-plugin-like/client/index.js b/plugins/talk-plugin-like/client/index.js index 68b7a2c46..aaea3aaf4 100644 --- a/plugins/talk-plugin-like/client/index.js +++ b/plugins/talk-plugin-like/client/index.js @@ -5,5 +5,5 @@ export default { translations, slots: { commentReactions: [LikeButton] - } + }, }; diff --git a/plugins/talk-plugin-member-since/client/.babelrc b/plugins/talk-plugin-member-since/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-member-since/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-member-since/client/.eslintrc.json b/plugins/talk-plugin-member-since/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-member-since/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-member-since/client/components/MemberSinceInfo.css b/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.css new file mode 100644 index 000000000..3cf2fb75d --- /dev/null +++ b/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.css @@ -0,0 +1,19 @@ +.root { + white-space: nowrap; +} + +.root:not(:last-child) { + margin-bottom: 4px; +} + +.memberSince { + margin-left: 4px; + letter-spacing: 0.2px; + font-size: 13px; + vertical-align: middle; +} + +.icon { + font-size: 18px; + vertical-align: middle; +} diff --git a/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.js b/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.js new file mode 100644 index 000000000..639f733d6 --- /dev/null +++ b/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.js @@ -0,0 +1,15 @@ +import React from 'react'; +import styles from './MemberSinceInfo.css'; +import {t} from 'plugin-api/beta/client/services'; +import {Icon} from 'plugin-api/beta/client/components/ui'; +import cn from 'classnames'; +import moment from 'moment'; + +export default ({memberSinceDate}) => ( +
+ + + {t('talk-plugin-member-since.member_since')}: {moment(new Date(memberSinceDate)).format('MMM DD, YYYY')} + +
+); diff --git a/plugins/talk-plugin-member-since/client/containers/MemberSinceInfo.js b/plugins/talk-plugin-member-since/client/containers/MemberSinceInfo.js new file mode 100644 index 000000000..186e9c8c1 --- /dev/null +++ b/plugins/talk-plugin-member-since/client/containers/MemberSinceInfo.js @@ -0,0 +1,28 @@ +import React from 'react'; +import MemberSinceInfo from '../components/MemberSinceInfo'; +import {compose, gql} from 'react-apollo'; +import {withFragments} from 'plugin-api/beta/client/hocs'; + +class MemberSinceInfoContainer extends React.Component { + render() { + return ; + } +} + +const withMemberSinceInfoFragments = withFragments({ + comment: gql` + fragment TalkAuthorMenu_MemberSinceInfo_comment on Comment { + user { + username + created_at + } + }`, +}); + +const enhance = compose( + withMemberSinceInfoFragments, +); + +export default enhance(MemberSinceInfoContainer); diff --git a/plugins/talk-plugin-member-since/client/index.js b/plugins/talk-plugin-member-since/client/index.js new file mode 100644 index 000000000..cfb60d867 --- /dev/null +++ b/plugins/talk-plugin-member-since/client/index.js @@ -0,0 +1,9 @@ +import MemberSinceInfo from './containers/MemberSinceInfo'; +import translations from './translations.yml'; + +export default { + slots: { + authorMenuInfos: [MemberSinceInfo] + }, + translations +}; diff --git a/plugins/talk-plugin-member-since/client/translations.yml b/plugins/talk-plugin-member-since/client/translations.yml new file mode 100644 index 000000000..1329974d7 --- /dev/null +++ b/plugins/talk-plugin-member-since/client/translations.yml @@ -0,0 +1,6 @@ +en: + talk-plugin-member-since: + member_since: "Member Since" +es: + talk-plugin-member-since: + member_since: "Miembro desde" diff --git a/plugins/talk-plugin-member-since/index.js b/plugins/talk-plugin-member-since/index.js new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/plugins/talk-plugin-member-since/index.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.js b/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.js index 7b59d87d6..7f4ec3503 100644 --- a/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.js +++ b/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.js @@ -29,12 +29,13 @@ export default class OffTopicCheckbox extends React.Component { } render() { + const checked = this.props.tags.indexOf(this.label) >= 0; return (
{ !this.props.isReply ? ( ) : null diff --git a/routes/api/assets/index.js b/routes/api/assets/index.js index a306b36b4..87014b86e 100644 --- a/routes/api/assets/index.js +++ b/routes/api/assets/index.js @@ -1,7 +1,6 @@ const express = require('express'); const router = express.Router(); -const scraper = require('../../../services/scraper'); const errors = require('../../../errors'); const AssetsService = require('../../../services/assets'); @@ -88,25 +87,6 @@ router.get('/:asset_id', async (req, res, next) => { } }); -// Adds the asset id to the queue to be scraped. -router.post('/:asset_id/scrape', async (req, res, next) => { - try { - - // Send back the asset. - let asset = await AssetsService.findById(req.params.asset_id); - if (!asset) { - return next(errors.ErrNotFound); - } - - let job = await scraper.create(asset); - - // Send the job back for monitoring. - res.status(201).json(job); - } catch (e) { - return next(e); - } -}); - router.put('/:asset_id/settings', async (req, res, next) => { try { await AssetsService.overrideSettings(req.params.asset_id, req.body); diff --git a/services/assets.js b/services/assets.js index 74bc1aa3a..34c8ff465 100644 --- a/services/assets.js +++ b/services/assets.js @@ -108,19 +108,57 @@ module.exports = class AssetsService { * @param {String} value string to search by. * @return {Promise} */ - static search({value, skip, limit} = {}) { - if (!value) { - return AssetsService.all(skip, limit); - } else { - return AssetModel - .find({ - $text: { - $search: value - } - }) - .skip(skip) - .limit(limit); + static search({value, limit, open, sortOrder, cursor} = {}) { + let assets = AssetModel.find({}); + + if (value) { + assets.merge({ + $text: { + $search: value + } + }); } + + if (open != null) { + if (open) { + assets.merge({ + $or: [ + { + closedAt: null + }, + { + closedAt: { + $gt: Date.now() + } + } + ] + }); + } else { + assets.merge({ + closedAt: { + $lt: Date.now() + } + }); + } + } + + if (cursor) { + if (sortOrder === 'DESC') { + assets.merge({ + created_at: { + $lt: cursor, + }, + }); + } else { + assets.merge({ + created_at: { + $gt: cursor, + }, + }); + } + } + + return assets.sort({created_at: sortOrder === 'DESC' ? -1 : 1}).limit(limit); } /** @@ -185,10 +223,9 @@ module.exports = class AssetsService { // That's it! } - static all(skip = null, limit = null) { + static all(limit = undefined) { return AssetModel .find({}) - .skip(skip) .limit(limit); } }; diff --git a/services/redis.js b/services/redis.js index 027d5bc20..5fd1b78d3 100644 --- a/services/redis.js +++ b/services/redis.js @@ -30,7 +30,6 @@ const attachMonitors = (client) => { }; const connectionOptions = { - url: REDIS_URL, retry_strategy: function(options) { if (options.error && options.error.code !== 'ECONNREFUSED') { @@ -65,7 +64,7 @@ const connectionOptions = { }; const createClient = () => { - let client = new Redis(connectionOptions); + let client = new Redis(REDIS_URL, connectionOptions); // Attach the monitors that will print debug messages to the console. attachMonitors(client); diff --git a/services/settings.js b/services/settings.js index 2bb15319d..3033701d8 100644 --- a/services/settings.js +++ b/services/settings.js @@ -42,6 +42,19 @@ module.exports = class SettingsService { }); } + /** + * updateWordlist will update the wordlists. + * + * @param {Object} wordlist the Wordlist object + */ + static updateWordlist(wordlist) { + return SettingModel.findOneAndUpdate(selector, { + $set: { + wordlist, + }, + }); + } + /** * This is run once when the app starts to ensure settings are populated. */ diff --git a/test/server/graph/mutations/updateAssetSettings.js b/test/server/graph/mutations/updateAssetSettings.js new file mode 100644 index 000000000..3fda0bd6c --- /dev/null +++ b/test/server/graph/mutations/updateAssetSettings.js @@ -0,0 +1,70 @@ +const {graphql} = require('graphql'); + +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const UserModel = require('../../../../models/user'); +const SettingsService = require('../../../../services/settings'); +const AssetModel = require('../../../../models/asset'); + +const {expect} = require('chai'); + +describe('graph.mutations.updateAssetSettings', () => { + let asset; + beforeEach(async () => { + await SettingsService.init(); + asset = await AssetModel.create({url: 'http://new.test.com/'}); + }); + + const QUERY = ` + mutation UpdateAssetStatus($id: ID!, $settings: AssetSettingsInput!) { + updateAssetSettings(id: $id, input: $settings) { + errors { + translation_key + } + } + } +`; + + describe('context with different user roles', () => { + + [ + {error: 'NOT_AUTHORIZED'}, + {roles: ['ADMIN', 'MODERATOR']}, + {roles: ['MODERATOR']}, + ].forEach(({roles, error}) => { + it(roles ? roles.join(', ') : '', async () => { + const user = new UserModel({roles}); + const ctx = new Context({user}); + + const settings = { + premodLinksEnable: false, + moderation: 'POST', + questionBoxEnable: true, + questionBoxContent: 'Question?', + questionBoxIcon: '', + }; + + const res = await graphql(schema, QUERY, {}, ctx, { + id: asset.id, + settings, + }); + if (res.errors) { + console.error(res.errors); + } + expect(res.errors).to.be.empty; + + if (error) { + expect(res.data.updateAssetSettings.errors).to.not.be.empty; + expect(res.data.updateAssetSettings.errors[0]).to.have.property('translation_key', error); + } else { + expect(res.data.updateAssetSettings.errors).to.be.null; + + const retrievedAsset = await AssetModel.findOne({id: asset.id}); + Object.keys(settings).forEach((key) => { + expect(retrievedAsset.settings).to.have.property(key, settings[key]); + }); + } + }); + }); + }); +}); diff --git a/test/server/graph/mutations/updateAssetStatus.js b/test/server/graph/mutations/updateAssetStatus.js new file mode 100644 index 000000000..b1eb8874c --- /dev/null +++ b/test/server/graph/mutations/updateAssetStatus.js @@ -0,0 +1,67 @@ +const {graphql} = require('graphql'); + +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const UserModel = require('../../../../models/user'); +const SettingsService = require('../../../../services/settings'); +const AssetModel = require('../../../../models/asset'); + +const {expect} = require('chai'); + +describe('graph.mutations.updateAssetStatus', () => { + let asset; + beforeEach(async () => { + await SettingsService.init(); + asset = await AssetModel.create({url: 'http://new.test.com/'}); + }); + + const QUERY = ` + mutation UpdateAssetStatus($id: ID!, $status: UpdateAssetStatusInput!) { + updateAssetStatus(id: $id, input: $status) { + errors { + translation_key + } + } + } + `; + + describe('context with different user roles', () => { + + [ + {error: 'NOT_AUTHORIZED'}, + {roles: ['ADMIN', 'MODERATOR']}, + {roles: ['MODERATOR']}, + ].forEach(({roles, error}) => { + it(roles ? roles.join(', ') : '', async () => { + const user = new UserModel({roles}); + const ctx = new Context({user}); + + const closedAt = (new Date()).toISOString(); + const closedMessage = 'my closed message!'; + + const res = await graphql(schema, QUERY, {}, ctx, { + id: asset.id, + status: { + closedAt, + closedMessage, + }, + }); + if (res.errors) { + console.error(res.errors); + } + expect(res.errors).to.be.empty; + + if (error) { + expect(res.data.updateAssetStatus.errors).to.not.be.empty; + expect(res.data.updateAssetStatus.errors[0]).to.have.property('translation_key', error); + } else { + expect(res.data.updateAssetStatus.errors).to.be.null; + + const retrievedAsset = await AssetModel.findOne({id: asset.id}); + expect(retrievedAsset.closedAt).to.not.be.null; + expect(retrievedAsset).to.have.property('closedMessage', closedMessage); + } + }); + }); + }); +}); diff --git a/test/server/graph/mutations/updateSettings.js b/test/server/graph/mutations/updateSettings.js new file mode 100644 index 000000000..27269b70a --- /dev/null +++ b/test/server/graph/mutations/updateSettings.js @@ -0,0 +1,74 @@ +const {graphql} = require('graphql'); + +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const UserModel = require('../../../../models/user'); +const SettingsService = require('../../../../services/settings'); + +const {expect} = require('chai'); + +describe('graph.mutations.updateSettings', () => { + beforeEach(async () => { + await SettingsService.init(); + }); + + const QUERY = ` + mutation UpdateSettings($settings: UpdateSettingsInput!) { + updateSettings(input: $settings) { + errors { + translation_key + } + } + } +`; + + describe('context with different user roles', () => { + + [ + {error: 'NOT_AUTHORIZED'}, + {error: 'NOT_AUTHORIZED', roles: []}, + {roles: ['ADMIN']}, + {roles: ['ADMIN', 'MODERATOR']}, + {roles: ['MODERATOR']}, + ].forEach(({roles, error}) => { + it(roles ? roles.join(', ') : '', async () => { + let user; + if (roles != null) { + user = new UserModel({roles}); + } + const ctx = new Context({user}); + + const newSettings = { + premodLinksEnable: false, + moderation: 'POST', + questionBoxEnable: true, + questionBoxContent: 'Question?', + questionBoxIcon: '', + }; + + const res = await graphql(schema, QUERY, {}, ctx, { + settings: newSettings, + }); + if (res.errors) { + console.error(res.errors); + } + expect(res.errors).to.be.empty; + + if (error) { + expect(res.data.updateSettings.errors).to.not.be.empty; + expect(res.data.updateSettings.errors[0]).to.have.property('translation_key', error); + } else { + if (res.data.updateSettings.errors) { + console.error(res.data.updateSettings.errors); + } + expect(res.data.updateSettings.errors).to.be.null; + + const retrievedSettings = await SettingsService.retrieve(); + Object.keys(newSettings).forEach((key) => { + expect(retrievedSettings).to.have.property(key, newSettings[key]); + }); + } + }); + }); + }); +}); diff --git a/test/server/graph/mutations/updateWordlist.js b/test/server/graph/mutations/updateWordlist.js new file mode 100644 index 000000000..b2f2e4abb --- /dev/null +++ b/test/server/graph/mutations/updateWordlist.js @@ -0,0 +1,82 @@ +const {graphql} = require('graphql'); + +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const UserModel = require('../../../../models/user'); +const SettingsService = require('../../../../services/settings'); + +const {expect} = require('chai'); + +describe('graph.mutations.updateWordlist', () => { + beforeEach(async () => { + await SettingsService.init(); + }); + + const QUERY = ` + mutation UpdateWordlist($wordlist: UpdateWordlistInput!) { + updateWordlist(input: $wordlist) { + errors { + translation_key + } + } + } +`; + + describe('context with different user roles', () => { + + [ + {error: 'NOT_AUTHORIZED'}, + {error: 'NOT_AUTHORIZED', roles: []}, + {roles: ['ADMIN']}, + {roles: ['ADMIN', 'MODERATOR']}, + {roles: ['MODERATOR']}, + ].forEach(({roles, error}) => { + it(roles && roles.length > 0 ? roles.join(', ') : '', async () => { + let user; + if (roles != null) { + user = new UserModel({roles}); + } + const ctx = new Context({user}); + + const wordlist = { + banned: [ + 'happy', + ], + suspect: [ + 'sad', + ], + }; + + const res = await graphql(schema, QUERY, {}, ctx, { + wordlist, + }); + if (res.errors) { + console.error(res.errors); + } + expect(res.errors).to.be.empty; + + if (error) { + expect(res.data.updateWordlist.errors).to.not.be.empty; + expect(res.data.updateWordlist.errors[0]).to.have.property('translation_key', error); + + const {wordlist: retrievedWordlist} = await SettingsService.retrieve(); + expect(retrievedWordlist).to.have.property('banned'); + expect(retrievedWordlist.banned).to.have.members([]); + expect(retrievedWordlist).to.have.property('suspect'); + expect(retrievedWordlist.suspect).to.have.members([]); + } else { + if (res.data.updateWordlist.errors) { + console.error(res.data.updateWordlist.errors); + } + expect(res.data.updateWordlist.errors).to.be.null; + + const {wordlist: retrievedWordlist} = await SettingsService.retrieve(); + expect(retrievedWordlist).to.have.property('banned'); + expect(retrievedWordlist.banned).to.have.members(wordlist.banned); + expect(retrievedWordlist).to.have.property('suspect'); + expect(retrievedWordlist.suspect).to.have.members(wordlist.suspect); + } + }); + }); + }); +}); diff --git a/test/server/graph/queries/asset.js b/test/server/graph/queries/asset.js index ba605dfd7..b49eb7429 100644 --- a/test/server/graph/queries/asset.js +++ b/test/server/graph/queries/asset.js @@ -74,12 +74,10 @@ describe('graph.queries.asset', () => { expect(asset.nodes).to.have.length(2); expect(asset.hasNextPage).to.be.false; - expect(asset.nodes[0]).to.have.property('id', comments[1].id); - expect(asset.nodes[1]).to.have.property('id', comments[0].id); + expect(asset.nodes.map(({id}) => id)).to.have.members(comments.slice(0, 2).map(({id}) => id)); expect(otherAsset.nodes).to.have.length(2); expect(otherAsset.hasNextPage).to.be.false; - expect(otherAsset.nodes[0]).to.have.property('id', comments[3].id); - expect(otherAsset.nodes[1]).to.have.property('id', comments[2].id); + expect(otherAsset.nodes.map(({id}) => id)).to.have.members(comments.slice(2, 4).map(({id}) => id)); for (let node of asset.nodes) { for (let otherNode of otherAsset.nodes) { diff --git a/test/server/graph/queries/settings.js b/test/server/graph/queries/settings.js new file mode 100644 index 000000000..338999145 --- /dev/null +++ b/test/server/graph/queries/settings.js @@ -0,0 +1,97 @@ +const {graphql} = require('graphql'); + +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const SettingsService = require('../../../../services/settings'); +const UserModel = require('../../../../models/user'); + +const {expect} = require('chai'); + +const defaultSettings = { + organizationName: 'The Coral Project' +}; + +describe('graph.queries.settings', () => { + let settings; + beforeEach(async () => { + settings = await SettingsService.init(defaultSettings); + }); + + const QUERY = ` + { + settings { + moderation + requireEmailConfirmation + infoBoxEnable + infoBoxContent + questionBoxEnable + questionBoxContent + premodLinksEnable + questionBoxIcon + autoCloseStream + customCssUrl + closedTimeout + closedMessage + charCountEnable + charCount + organizationName + wordlist { + banned + suspect + } + domains { + whitelist + } + } + } + `; + + describe('context with different user roles', () => { + + const BLACKLISTED_PROPERTIES = [ + 'premodLinksEnable', + 'autoCloseStream', + 'wordlist', + 'domains', + ]; + + [ + {bl: true}, + {bl: true, roles: []}, + {bl: false, roles: ['ADMIN']}, + {bl: false, roles: ['ADMIN', 'MODERATOR']}, + {bl: false, roles: ['MODERATOR']}, + ].forEach(({bl, roles}) => { + it(roles && roles.length > 0 ? roles.join(', ') : '', async () => { + let user; + if (roles != null) { + user = new UserModel({roles}); + } + + const ctx = new Context({user}); + + const res = await graphql(schema, QUERY, {}, ctx); + if (res.errors) { + console.error(res.errors); + } + + expect(res.errors).to.be.empty; + expect(res.data.settings).to.be.object; + Object.keys(res.data.settings).forEach((key) => { + if (bl && BLACKLISTED_PROPERTIES.includes(key)) { + expect(res.data.settings).to.have.property(key, null); + return; + } + + if (typeof settings[key] !== 'object') { + expect(res.data.settings).to.have.property(key, settings[key]); + } else { + expect(res.data.settings).to.have.property(key); + expect(res.data.settings[key]).to.not.be.null; + } + }); + }); + }); + }); + +}); diff --git a/yarn.lock b/yarn.lock index d55aa9745..1ca8acdbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,20 +63,13 @@ abbrev@1, abbrev@1.0.x: version "1.0.9" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" -accepts@^1.3.4: +accepts@^1.3.4, accepts@~1.3.3: version "1.3.4" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" dependencies: mime-types "~2.1.16" negotiator "0.6.1" -accepts@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" - dependencies: - mime-types "~2.1.11" - negotiator "0.6.1" - acorn-dynamic-import@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" @@ -356,18 +349,12 @@ async@2.1.4: dependencies: lodash "^4.14.0" -async@2.4.1: +async@2.4.1, async@^2.1.2, async@^2.1.4: version "2.4.1" resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7" dependencies: lodash "^4.14.0" -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: - lodash "^4.14.0" - async@~0.9.0: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" @@ -416,15 +403,7 @@ babel-cli@^6.26.0: optionalDependencies: chokidar "^1.6.1" -babel-code-frame@^6.11.0, babel-code-frame@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" - dependencies: - chalk "^1.1.0" - esutils "^2.0.2" - js-tokens "^3.0.0" - -babel-code-frame@^6.26.0: +babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" dependencies: @@ -465,20 +444,7 @@ babel-eslint@^7.2.1: babel-types "^6.23.0" babylon "^6.17.0" -babel-generator@^6.18.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.1.tgz#e715f486c58ded25649d888944d52aa07c5d9497" - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.2.0" - source-map "^0.5.0" - trim-right "^1.0.1" - -babel-generator@^6.26.0: +babel-generator@^6.18.0, babel-generator@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" dependencies: @@ -956,31 +922,14 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.6.1: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.10.0" - -babel-runtime@^6.26.0: +babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.6.1: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: core-js "^2.4.0" regenerator-runtime "^0.11.0" -babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.3.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.24.1.tgz#04ae514f1f93b3a2537f2a0f60a5a45fb8308333" - dependencies: - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - babylon "^6.11.0" - lodash "^4.2.0" - -babel-template@^6.26.0: +babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" dependencies: @@ -990,21 +939,7 @@ babel-template@^6.26.0: babylon "^6.18.0" lodash "^4.17.4" -babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.24.1.tgz#ab36673fd356f9a0948659e7b338d5feadb31695" - dependencies: - babel-code-frame "^6.22.0" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - babylon "^6.15.0" - debug "^2.2.0" - globals "^9.0.0" - invariant "^2.2.0" - lodash "^4.2.0" - -babel-traverse@^6.26.0: +babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" dependencies: @@ -1018,16 +953,7 @@ babel-traverse@^6.26.0: invariant "^2.2.2" lodash "^4.17.4" -babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.24.1.tgz#a136879dc15b3606bda0d90c1fc74304c2ff0975" - dependencies: - babel-runtime "^6.22.0" - esutils "^2.0.2" - lodash "^4.2.0" - to-fast-properties "^1.0.1" - -babel-types@^6.26.0: +babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" dependencies: @@ -1036,11 +962,7 @@ babel-types@^6.26.0: lodash "^4.17.4" to-fast-properties "^1.0.3" -babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0, babylon@^6.17.0: - version "6.17.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.0.tgz#37da948878488b9c4e3c4038893fa3314b3fc932" - -babylon@^6.18.0: +babylon@^6.13.0, babylon@^6.17.0, babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" @@ -1056,7 +978,7 @@ balanced-match@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.2.1.tgz#7bc658b4bed61eee424ad74f75f5c3e2c4df3cc7" -balanced-match@^0.4.1, balanced-match@^0.4.2: +balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" @@ -1112,7 +1034,7 @@ bluebird@2.9.24: version "2.9.24" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.9.24.tgz#14a2e75f0548323dc35aa440d92007ca154e967c" -bluebird@3.5.0, bluebird@^3.3.4: +bluebird@3.5.0, bluebird@^3.3.4, bluebird@^3.4.6: version "3.5.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" @@ -1120,30 +1042,11 @@ bluebird@^2.10.2: version "2.11.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" -bluebird@^3.4.6: - version "3.4.7" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" - bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.6" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" -body-parser@^1.12.2: - version "1.17.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.1.tgz#75b3bc98ddd6e7e0d8ffe750dfaca5c66993fa47" - dependencies: - bytes "2.4.0" - content-type "~1.0.2" - debug "2.6.1" - depd "~1.1.0" - http-errors "~1.6.1" - iconv-lite "0.4.15" - on-finished "~2.3.0" - qs "6.4.0" - raw-body "~2.2.0" - type-is "~1.6.14" - -body-parser@^1.17.2: +body-parser@^1.12.2, body-parser@^1.17.2: version "1.17.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee" dependencies: @@ -1172,13 +1075,6 @@ bowser@^1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.7.2.tgz#b94cc6925ba6b5e07c421a58e601ce4611264572" -brace-expansion@^1.0.0: - version "1.1.7" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59" - dependencies: - balanced-match "^0.4.1" - concat-map "0.0.1" - brace-expansion@^1.1.7: version "1.1.8" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" @@ -1394,7 +1290,7 @@ chalk@2.0.1: escape-string-regexp "^1.0.5" supports-color "^4.0.0" -chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -1867,11 +1763,7 @@ core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" -core-js@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" - -core-js@^2.5.0: +core-js@^2.4.0, core-js@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086" @@ -2131,9 +2023,9 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" -debug@*: - version "3.0.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.0.tgz#1d2feae53349047b08b264ec41906ba17a8516e4" +debug@*, debug@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.1.tgz#0564c612b521dc92d9f2988f0549e34f9c98db64" dependencies: ms "2.0.0" @@ -2143,12 +2035,6 @@ debug@2.6.0: dependencies: ms "0.7.2" -debug@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351" - dependencies: - ms "0.7.2" - debug@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" @@ -2161,12 +2047,6 @@ debug@2.6.8, debug@^2.2.0, debug@^2.6.8: dependencies: ms "2.0.0" -debug@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.1.tgz#0564c612b521dc92d9f2988f0549e34f9c98db64" - dependencies: - ms "2.0.0" - debug@~0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" @@ -3200,7 +3080,7 @@ glob@7.0.x: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1: +glob@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: @@ -3211,7 +3091,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@7.1.2, glob@^7.1.2: +glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -3242,10 +3122,6 @@ glob@^6.0.4: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^9.0.0: - version "9.17.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.17.0.tgz#0c0ca696d9b9bb694d2e5470bd37777caad50286" - globals@^9.17.0, globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -3372,15 +3248,7 @@ graphql-server-module-graphiql@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/graphql-server-module-graphiql/-/graphql-server-module-graphiql-0.6.0.tgz#e37634b05f000731981e8ed13103f9a5861e5da0" -graphql-subscriptions@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.4.4.tgz#39cff32d08dd3c990113864bab77154403727e9b" - dependencies: - "@types/graphql" "^0.9.1" - es6-promise "^4.0.5" - iterall "^1.1.1" - -graphql-subscriptions@^0.4.3: +graphql-subscriptions@^0.4.2, graphql-subscriptions@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.4.3.tgz#2aed6ba87551cc747742b793497ff24b22991867" dependencies: @@ -4240,22 +4108,18 @@ js-stringify@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" -js-tokens@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" - -js-tokens@^3.0.2: +js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.2, js-yaml@^3.7.0: +js-yaml@3.x, js-yaml@^3.5.2: version "3.8.3" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.3.tgz#33a05ec481c850c8875929166fe1beb61c728766" dependencies: argparse "^1.0.7" esprima "^3.1.1" -js-yaml@^3.9.1: +js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.9.1: version "3.9.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0" dependencies: @@ -4318,11 +4182,7 @@ jshint@^2.8.0: shelljs "0.3.x" strip-json-comments "1.0.x" -json-loader@^0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de" - -json-loader@^0.5.7: +json-loader@^0.5.4, json-loader@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" @@ -4954,15 +4814,11 @@ 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" +mime-types@^2.1.10, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7: + version "2.1.16" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" dependencies: - mime-db "~1.27.0" + mime-db "~1.29.0" mime-types@~2.0.3: version "2.0.14" @@ -4970,12 +4826,6 @@ mime-types@~2.0.3: dependencies: mime-db "~1.12.0" -mime-types@~2.1.16: - version "2.1.16" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" - dependencies: - mime-db "~1.29.0" - mime@1.3.4, mime@1.3.x, mime@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" @@ -4992,13 +4842,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.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: - brace-expansion "^1.0.0" - -minimatch@^3.0.4, minimatch@~3.0.2: +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -5043,7 +4887,7 @@ mocha@^3.1.2: mkdirp "0.5.1" supports-color "3.1.2" -moment@2.18.1, moment@2.x.x, moment@^2.10.3: +moment@2.18.1, moment@2.x.x, moment@^2.10.3, moment@^2.18.1: version "2.18.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" @@ -6429,7 +6273,7 @@ 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.5.0: +q@1.5.0, q@^1.1.2: version "1.5.0" resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" @@ -6441,10 +6285,6 @@ q@2.0.3: pop-iterate "^1.0.1" weak-map "^1.0.5" -q@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/q/-/q-1.1.2.tgz#6357e291206701d99f197ab84e57e8ad196f2a89" - qs@6.4.0, qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -6796,10 +6636,6 @@ regenerate@^1.2.1: version "1.3.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" -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.10.5: version "0.10.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" @@ -7127,7 +6963,7 @@ shelljs@0.3.x: version "0.3.0" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1" -shelljs@0.7.8: +shelljs@0.7.8, shelljs@^0.7.0: version "0.7.8" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" dependencies: @@ -7135,14 +6971,6 @@ shelljs@0.7.8: interpret "^1.0.0" rechoir "^0.6.2" -shelljs@^0.7.0: - version "0.7.7" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1" - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -7275,7 +7103,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.1, source-map@~0.5.3: +source-map@0.5.x, 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" @@ -7648,10 +7476,6 @@ to-capital-case@^1.0.0: dependencies: to-space-case "^1.0.0" -to-fast-properties@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" - to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" @@ -7753,7 +7577,7 @@ type-detect@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea" -type-is@~1.6.14, type-is@~1.6.15: +type-is@~1.6.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" dependencies: @@ -8051,14 +7875,10 @@ word-wrap@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.1.0.tgz#356153d61d10610d600785c5d701288e0ae764a6" -word-wrap@1.2.3: +word-wrap@1.2.3, word-wrap@^1.0.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" -word-wrap@^1.0.3: - version "1.2.1" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.1.tgz#248f459b465d179a17bc407c854d3151d07e45d8" - wordwrap@0.0.2, wordwrap@~0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"