Merge branch 'master' into staff-badge-bug

This commit is contained in:
Wyatt Johnson
2017-09-05 10:26:11 -06:00
committed by GitHub
88 changed files with 1883 additions and 568 deletions
+3
View File
@@ -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
+3
View File
@@ -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/*
@@ -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);
@@ -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 {
/>
<TransitionGroup component='div' className="embed__stream">
{view.map((comment) => {
return commentIsIgnored(comment)
? <IgnoredCommentTombstone key={comment.id} />
: <Comment
return (
<Comment
commentClassNames={commentClassNames}
data={data}
root={root}
@@ -173,8 +169,6 @@ class AllCommentsPane extends React.Component {
currentUser={currentUser}
postFlag={postFlag}
postDontAgree={postDontAgree}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
@@ -184,7 +178,8 @@ class AllCommentsPane extends React.Component {
maxCharCount={maxCharCount}
editComment={editComment}
emit={emit}
/>;
/>
);
})}
</TransitionGroup>
<LoadMore
@@ -66,17 +66,6 @@
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
.topRight .popover {
margin-top: 1em;
right: 0px;
}
.topRight .link.active,
.topRight .active .link {
padding-bottom: 0.125em;
border-bottom: 2px solid currentColor;
}
.editCommentForm {
margin-bottom: 10px;
}
@@ -175,4 +164,4 @@
.username {
margin-right: 5px;
}
}
@@ -16,7 +16,6 @@ import mapValues from 'lodash/mapValues';
import LoadMore from './LoadMore';
import {getEditableUntilDate} from './util';
import {findCommentWithId} from '../graphql/utils';
import {TopRightMenu} from './TopRightMenu';
import CommentContent from './CommentContent';
import Slot from 'coral-framework/components/Slot';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
@@ -184,12 +183,6 @@ export default class Comment extends React.Component {
})
}).isRequired,
// given a comment, return whether it should be rendered as ignored
commentIsIgnored: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: PropTypes.func,
// edit a comment, passed (id, asset_id, { body })
editComment: PropTypes.func,
@@ -216,9 +209,18 @@ export default class Comment extends React.Component {
}
}
commentIsIgnored(comment) {
const me = this.props.root.me;
return (
me &&
me.ignoredUsers &&
me.ignoredUsers.find((u) => 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 <IgnoredCommentTombstone />;
}
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 {
}
</span>
}
{ isActive && (currentUser && (comment.user.id !== currentUser.id)) &&
/* TopRightMenu allows currentUser to ignore other users' comments */
<span className={cn(styles.topRight, styles.topRightMenu)}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
notify={notify} />
</span>
}
{ !isActive &&
<InactiveCommentLabel status={comment.status}/>
}
@@ -581,9 +575,8 @@ export default class Comment extends React.Component {
<TransitionGroup>
{view.map((reply) => {
return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} />
: <CommentContainer
return (
<CommentContainer
data={this.props.data}
root={this.props.root}
setActiveReplyBox={setActiveReplyBox}
@@ -600,17 +593,16 @@ export default class Comment extends React.Component {
postFlag={postFlag}
deleteAction={deleteAction}
loadMore={loadMore}
ignoreUser={ignoreUser}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
showSignInDialog={showSignInDialog}
commentIsIgnored={commentIsIgnored}
liveUpdates={liveUpdates}
reactKey={reply.id}
key={reply.id}
comment={reply}
emit={emit}
/>;
/>
);
})}
</TransitionGroup>
<div className="talk-load-more-replies">
@@ -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
};
@@ -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 (
<Toggleable key={this.state.timesReset} className="talk-stream-comment-chevron">
<div style={{position: 'absolute', right: 0, zIndex: 1}}>
<IgnoreUserWizard
user={comment.user}
cancel={reset}
ignoreUser={ignoreUserAndCloseMenuAndNotifyOnError}
/>
</div>
</Toggleable>
);
}
}
@@ -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}
`
@@ -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);
@@ -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},
+1 -1
View File
@@ -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}`;
+1 -1
View File
@@ -293,7 +293,7 @@ export const withIgnoreUser = withMutation(
}
`, {
props: ({mutate}) => ({
ignoreUser: ({id}) => {
ignoreUser: (id) => {
return mutate({
variables: {
id,
@@ -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}));
},
};
}
@@ -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';
}
@@ -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 (
<div>
{
users.length
? <p>{t('framework.because_you_ignored')}</p>
: null
}
<dl className={styles.ignoredUserList}>
{
users.map(({username, id}) => (
<span className={styles.ignoredUser} key={id}>
<dt key={id}>{ username }</dt>
<dd className={styles.stopListening}>
<a
onClick={() => stopIgnoring({id})}
className={styles.link}>{t('framework.stop_ignoring')}</a>
</dd>
</span>
))
}
</dl>
</div>
);
}
}
export default IgnoredUsers;
@@ -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 {
<h2>{user.username}</h2>
{emailAddress ? <p>{emailAddress}</p> : null}
{me.ignoredUsers && me.ignoredUsers.length
? <div>
<h3>{t('framework.ignored_users')}</h3>
<IgnoredUsers
users={me.ignoredUsers}
stopIgnoring={stopIgnoringUser}
/>
</div>
: null}
<Slot
fill="profileSections"
data={data}
queryData={{root}}
/>
<hr />
@@ -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);
+28 -3
View File
@@ -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({}))
+2 -2
View File
@@ -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())
});
+55
View File
@@ -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;
};
+4
View File
@@ -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,
+33
View File
@@ -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;
};
+12
View File
@@ -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));
},
+1 -1
View File
@@ -11,7 +11,7 @@ const RootQuery = {
return null;
}
return Assets.search(query);
return Assets.getByQuery(query);
},
asset(_, query, {loaders: {Assets}}) {
if (query.id) {
+18
View File
@@ -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;
+26 -1
View File
@@ -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<String> 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,
};
+259 -15
View File
@@ -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!
}
################################################################################
-3
View File
@@ -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"
-2
View File
@@ -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"
-1
View File
@@ -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}"
-3
View File
@@ -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"
+1
View File
@@ -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",
+5
View File
@@ -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',
+2 -5
View File
@@ -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.`);
}
+9 -16
View File
@@ -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:
+2
View File
@@ -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;
}
@@ -1 +1 @@
export {addNotification} from 'coral-framework/actions/notification';
export {notify} from 'coral-framework/actions/notification';
+4
View File
@@ -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';
+1
View File
@@ -7,4 +7,5 @@ export {
capitalize,
getErrorMessages,
getDefinitionName,
getShallowChanges,
} from 'coral-framework/utils';
+4 -1
View File
@@ -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"
]
}
@@ -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"
]
}
@@ -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"] }]
}
}
@@ -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,
});
@@ -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;
}
@@ -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 (
<ClickOutside onClickOutside={hideMenu}>
<div className={cn(styles.root, 'talk-plugin-author-menu')}>
<button
className={cn(styles.button, 'talk-plugin-author-menu-button')}
onClick={toggleMenu}
>
<span className={styles.name}>
{comment.user.username}
</span>
</button>
{menuVisible &&
<Menu
data={data}
root={root}
asset={asset}
comment={comment}
contentSlot={contentSlot}
/>
}
</div>
</ClickOutside>
);
};
@@ -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;
}
}
@@ -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 (
<div className={cn(styles.menu, 'talk-plugin-author-menu-popup')}>
<Slot
fill={contentSlot}
data={data}
queryData={{asset, root, comment}}
/>
</div>
);
}
return (
<div className={cn(styles.menu, 'talk-plugin-author-menu-popup')}>
<Slot
className={cn('talk-plugin-author-menu-infos')}
fill={'authorMenuInfos'}
data={data}
queryData={{asset, root, comment}}
/>
<Slot
className={cn(styles.actions, 'talk-plugin-author-menu-actions')}
fill={'authorMenuActions'}
data={data}
queryData={{asset, root, comment}}
/>
</div>
);
};
@@ -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`;
@@ -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 <AuthorName
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
comment={this.props.comment}
contentSlot={this.props.contentSlot}
menuVisible={this.props.showMenuForComment === this.props.comment.id}
toggleMenu={this.toggleMenu}
hideMenu={this.hideMenu}
/>;
}
}
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);
@@ -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
};
@@ -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;
}
}
@@ -0,0 +1,4 @@
en:
talk-plugin-author-menu:
es:
talk-plugin-author-menu:
+1
View File
@@ -0,0 +1 @@
module.exports = {};
@@ -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"
]
}
@@ -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"] }]
}
}
@@ -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;
}
}
@@ -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}) => (
<button
className={cn(styles.button, 'talk-plugin-ignore-user-action')}
onClick={ignoreUser}>
{t('talk-plugin-ignore-user.ignore_user')}
</button>
);
@@ -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);
}
}
@@ -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}) => (
<aside className={cn(styles.root, 'talk-plugin-ignore-user-confirmation')}>
<div className={styles.message}>
<h1 className={styles.title}>
{t('talk-plugin-ignore-user.confirmation_title', username)}
</h1>
{t('talk-plugin-ignore-user.confirmation')}
</div>
<div className={cn(styles.actions, 'talk-plugin-ignore-user-confirmation-actions')}>
<button className={cn(styles.cancel, 'talk-plugin-ignore-user-confirmation-cancel')} onClick={cancel}>
{t('talk-plugin-ignore-user.cancel')}
</button>
<button className={cn(styles.button, 'talk-plugin-ignore-user-confirmation-button')} onClick={ignoreUser}>
{t('talk-plugin-ignore-user.ignore_user')}
</button>
</div>
</aside>
);
@@ -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;
}
@@ -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}) => (
<section className={'talk-plugin-ignore-user-section'}>
<h3>{t('talk-plugin-ignore-user.section_title')}</h3>
<p>{t('talk-plugin-ignore-user.section_info')}</p>
<ul className={styles.list}>
{ignoredUsers.map(({username, id}) => (
<li className={styles.listItem} key={id}>
<span className={styles.username}>{username}</span>
<button
onClick={() => stopIgnoringUser({id})}
className={styles.button}
>
{t('talk-plugin-ignore-user.stop_ignoring')}
</button>
</li>
))}
</ul>
</section>
);
@@ -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 <IgnoreUserAction
ignoreUser={this.ignoreUser}
/>;
}
}
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);
@@ -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 <IgnoreUserConfirmation
username={this.props.comment.user.username}
ignoreUser={this.ignoreUser}
cancel={this.cancel}
/>;
}
}
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);
@@ -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 <IgnoredUserSection
stopIgnoringUser={this.props.stopIgnoringUser}
ignoredUsers={this.props.root.me.ignoredUsers}
/>;
}
}
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);
@@ -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;
}
}
}),
},
};
@@ -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"
+1
View File
@@ -0,0 +1 @@
module.exports = {};
+1 -1
View File
@@ -5,5 +5,5 @@ export default {
translations,
slots: {
commentReactions: [LikeButton]
}
},
};
@@ -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"
]
}
@@ -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"] }]
}
}
@@ -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;
}
@@ -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}) => (
<div className={cn(styles.root, 'talk-plugin-member-since')}>
<Icon name="date_range" className={cn(styles.icon, 'talk-plugin-member-since-icon')} />
<span className={cn(styles.memberSince, 'talk-plugin-member-since-date')}>
{t('talk-plugin-member-since.member_since')}: {moment(new Date(memberSinceDate)).format('MMM DD, YYYY')}
</span>
</div>
);
@@ -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 <MemberSinceInfo
memberSinceDate={this.props.comment.user.created_at}
/>;
}
}
const withMemberSinceInfoFragments = withFragments({
comment: gql`
fragment TalkAuthorMenu_MemberSinceInfo_comment on Comment {
user {
username
created_at
}
}`,
});
const enhance = compose(
withMemberSinceInfoFragments,
);
export default enhance(MemberSinceInfoContainer);
@@ -0,0 +1,9 @@
import MemberSinceInfo from './containers/MemberSinceInfo';
import translations from './translations.yml';
export default {
slots: {
authorMenuInfos: [MemberSinceInfo]
},
translations
};
@@ -0,0 +1,6 @@
en:
talk-plugin-member-since:
member_since: "Member Since"
es:
talk-plugin-member-since:
member_since: "Miembro desde"
@@ -0,0 +1 @@
module.exports = {};
@@ -29,12 +29,13 @@ export default class OffTopicCheckbox extends React.Component {
}
render() {
const checked = this.props.tags.indexOf(this.label) >= 0;
return (
<div className={styles.offTopic}>
{
!this.props.isReply ? (
<label className={styles.offTopicLabel}>
<input type="checkbox" onChange={this.handleChange}/>
<input type="checkbox" onChange={this.handleChange} checked={checked}/>
{t('off_topic')}
</label>
) : null
-20
View File
@@ -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);
+51 -14
View File
@@ -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);
}
};
+1 -2
View File
@@ -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);
+13
View File
@@ -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.
*/
@@ -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(', ') : '<None>', async () => {
const user = new UserModel({roles});
const ctx = new Context({user});
const settings = {
premodLinksEnable: false,
moderation: 'POST',
questionBoxEnable: true,
questionBoxContent: 'Question?',
questionBoxIcon: '<Icon>',
};
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]);
});
}
});
});
});
});
@@ -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(', ') : '<None>', 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);
}
});
});
});
});
@@ -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(', ') : '<None>', 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: '<Icon>',
};
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]);
});
}
});
});
});
});
@@ -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(', ') : '<None>', 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);
}
});
});
});
});
+2 -4
View File
@@ -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) {
+97
View File
@@ -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(', ') : '<None>', 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;
}
});
});
});
});
});
+35 -215
View File
@@ -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"