Merge branch 'master' into feature/talk-plugin-toxic-comments

This commit is contained in:
Kim Gardner
2017-08-15 11:28:00 +01:00
committed by GitHub
36 changed files with 496 additions and 435 deletions
+32 -6
View File
@@ -94,7 +94,7 @@ const performSetup = async () => {
name: 'requireEmailConfirmation',
default: settings.requireEmailConfirmation,
message: 'Should emails always be confirmed'
}
},
]);
// Update the settings that were changed.
@@ -104,6 +104,32 @@ const performSetup = async () => {
}
});
answers = await inquirer.prompt([
{
type: 'confirm',
name: 'inputWhitelistedDomains',
default: true,
message: 'Would you like to specify a whitelisted domain'
},
{
type: 'input',
name: 'whitelistedDomain',
message: 'Whitelisted Domain',
when: ({inputWhitelistedDomains}) => inputWhitelistedDomains,
validate: (input) => {
if (input && input.length > 0) {
return true;
}
return 'Whitelisted Domain cannot be empty.';
}
}
]);
if (answers.inputWhitelistedDomains) {
settings.domains.whitelist = [answers.whitelistedDomain];
}
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
let user = await inquirer.prompt([
@@ -147,7 +173,11 @@ const performSetup = async () => {
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
filter: (confirmPassword) => {
filter: (confirmPassword, {password}) => {
if (password !== confirmPassword) {
return Promise.reject(new Error('Passwords do not match'));
}
return UsersService
.isValidPassword(confirmPassword)
.catch((err) => {
@@ -157,10 +187,6 @@ const performSetup = async () => {
},
]);
if (user.password !== user.confirmPassword) {
return Promise.reject(new Error('Passwords do not match'));
}
let {user: newUser} = await SetupService.setup({
settings: settings.toObject(),
user: {
+4 -20
View File
@@ -37,27 +37,11 @@ const routes = (
<Route path='moderate' component={ModerationLayout}>
<IndexRoute components={Moderation} />
<Route path='all' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='new' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='approved' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='premod' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='rejected' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='reported' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path=':id' components={Moderation} />
<Route path=':tabOrId' components={Moderation} />
<Route path=':tab' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
</Route>
</Route>
</div>
@@ -3,11 +3,13 @@
color: white;
background: grey;
box-sizing: border-box;
padding: 2px 8px;
padding: 0px 5px;
border-radius: 2px;
font-size: 12px;
height: 28px;
height: 24px;
letter-spacing: 0.4px;
margin-bottom: 1px;
> i {
font-size: 14px;
vertical-align: text-top;
@@ -1,6 +1,6 @@
.flagBox {
border-top: 1px solid rgba(66, 66, 66, 0.12);
margin-top: 10px;
.container {
padding: 0 14px;
}
@@ -186,7 +186,6 @@
.actionButton {
transform: scale(.8);
margin: 0;
width: 140px;
}
.minimal {
@@ -10,16 +10,18 @@
.logo span {
display: inline-block;
margin-left: 10px;
font-size: 18px;
font-size: 26px;
vertical-align: middle;
font-weight: 500;
color: white;
}
.logo {
background: #E5E5E5;
background: #696969;
height: 100%;
width: 128px;
z-index: 10;
border-right: 1px #757575 solid;
}
.base {
@@ -17,11 +17,11 @@ const ActivityWidget = ({assets}) => {
? assets.map((asset) => {
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{asset.commentCount}</p>
<Link className={styles.linkToAsset} to={`${asset.url}#coralStreamEmbed_iframe`} target="_blank">
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</Link>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
@@ -24,11 +24,11 @@ const FlagWidget = ({assets}) => {
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
<Link className={styles.linkToModerate} to={`/admin/moderate/reported/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
<Link className={styles.linkToAsset} to={`${asset.url}#coralStreamEmbed_iframe`} target="_blank">
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</Link>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
@@ -19,11 +19,11 @@ const LikeWidget = ({assets}) => {
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
return (
<div className={styles.rowLinkify} key={asset.id}>
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
<Link className={styles.linkToAsset} to={`${asset.url}#coralStreamEmbed_iframe`} target="_blank">
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
<p className={styles.assetTitle}>{asset.title}</p>
</Link>
</a>
<p className={styles.lede}>{asset.author} Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</div>
);
@@ -1,16 +1,16 @@
.count {
display: inline-block;
background: #989797;
background: #616161;
margin: 2px;
vertical-align: middle;
padding: 1px 7px;
padding: 1px 5px;
border-radius: 2px;
margin-left: 2px;
line-height: 20px;
line-height: 18px;
box-sizing: border-box;
height: 21px;
height: 18px;
right: 0;
margin-top: -2px;
margin-top: 0px;
font-size: 12px;
color: white;
}
@@ -101,33 +101,19 @@ export default class Moderation extends Component {
}
render () {
const {root, data, moderation, settings, viewUserDetail, hideUserDetail, activeTab, getModPath, premodEnabled, ...props} = this.props;
const assetId = this.props.params.id;
const {root, data, moderation, settings, viewUserDetail, hideUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props;
const {asset} = root;
const assetId = asset && asset.id;
const comments = root[activeTab];
let activeTabCount;
switch(activeTab) {
case 'all':
activeTabCount = root.allCount;
break;
case 'new':
activeTabCount = root.newCount;
break;
case 'approved':
activeTabCount = root.approvedCount;
break;
case 'premod':
activeTabCount = root.premodCount;
break;
case 'reported':
activeTabCount = root.reportedCount;
break;
case 'rejected':
activeTabCount = root.rejectedCount;
break;
}
const activeTabCount = root[`${activeTab}Count`];
const menuItems = Object.keys(queueConfig).map((queue) => ({
key: queue,
name: queueConfig[queue].name,
icon: queueConfig[queue].icon,
count: root[`${queue}Count`]
}));
return (
<div>
@@ -139,16 +125,10 @@ export default class Moderation extends Component {
/>
<ModerationMenu
asset={asset}
allCount={root.allCount}
newCount={root.newCount}
getModPath={getModPath}
approvedCount={root.approvedCount}
premodCount={root.premodCount}
rejectedCount={root.rejectedCount}
reportedCount={root.reportedCount}
items={menuItems}
selectSort={this.props.setSortOrder}
sort={this.props.moderation.sortOrder}
premodEnabled={premodEnabled}
activeTab={activeTab}
/>
<ModerationQueue
@@ -191,6 +171,7 @@ export default class Moderation extends Component {
root={root}
assset={asset}
activeTab={activeTab}
handleCommentChange={handleCommentChange}
fill='adminModeration'
/>
</div>
@@ -9,16 +9,10 @@ import cn from 'classnames';
import t from 'coral-framework/services/i18n';
const ModerationMenu = ({
asset = {},
allCount,
approvedCount,
premodCount,
newCount,
rejectedCount,
reportedCount,
asset = {},
items,
selectSort,
sort,
premodEnabled,
getModPath,
activeTab
}) => {
@@ -27,49 +21,15 @@ const ModerationMenu = ({
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<div className={styles.tabBarPadding} />
<div>
{
premodEnabled ? (
<Link
to={getModPath('premod', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'premod'})}
activeClassName={styles.active}>
<Icon name='access_time' className={styles.tabIcon} /> {t('modqueue.premod')} <CommentCount count={premodCount} />
</Link>
) : (
<Link
to={getModPath('new', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'new'})}
activeClassName={styles.active}>
<Icon name='question_answer' className={styles.tabIcon} /> {t('modqueue.new')} <CommentCount count={newCount} />
</Link>
)
}
<Link
to={getModPath('reported', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'reported'})}
activeClassName={styles.active}>
<Icon name='flag' className={styles.tabIcon} /> {t('modqueue.reported')} <CommentCount count={reportedCount} />
</Link>
<Link
to={getModPath('approved', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'approved'})}
activeClassName={styles.active}>
<Icon name='check' className={styles.tabIcon} /> {t('modqueue.approved')} <CommentCount count={approvedCount} />
</Link>
<Link
to={getModPath('rejected', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'rejected'})}
activeClassName={styles.active}>
<Icon name='close' className={styles.tabIcon} /> {t('modqueue.rejected')} <CommentCount count={rejectedCount} />
</Link>
<Link
to={getModPath('all', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'all'})}
activeClassName={styles.active}>
<Icon name='question_answer' className={styles.tabIcon} /> {t('modqueue.all')} <CommentCount count={allCount} />
</Link>
{items.map((queue) =>
<Link
key={queue.key}
to={getModPath(queue.key, asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === queue.key})}
activeClassName={styles.active}>
<Icon name={queue.icon} className={styles.tabIcon} /> {queue.name} <CommentCount count={queue.count} />
</Link>
)}
</div>
<SelectField
className={styles.selectField}
@@ -85,10 +45,7 @@ const ModerationMenu = ({
};
ModerationMenu.propTypes = {
allCount: PropTypes.number.isRequired,
premodCount: PropTypes.number.isRequired,
rejectedCount: PropTypes.number.isRequired,
reportedCount: PropTypes.number.isRequired,
items: PropTypes.array.isRequired,
asset: PropTypes.shape({
id: PropTypes.string
})
@@ -18,7 +18,7 @@
.tab {
flex: 1;
color: #C0C0C0;
color: #BDBDBD;
text-transform: capitalize;
font-weight: 100;
font-size: 14px;
@@ -29,7 +29,7 @@
margin-right: 20px;
&:hover {
color: white;
border-bottom: solid 2px #F36451;
/*border-bottom: solid 2px #F36451;*/
box-sizing: border-box;
}
}
@@ -111,7 +111,7 @@ span {
color: white;
text-transform: capitalize;
font-weight: 400;
font-size: 15px;
font-size: 20px;
letter-spacing: 1px;
transition: background-color 200ms;
opacity: 1;
@@ -173,7 +173,7 @@ span {
border-bottom: 1px solid #e0e0e0;
font-size: 18px;
width: 100%;
max-width: 700px;
max-width: 650px;
min-width: 400px;
margin: 0 auto;
position: relative;
@@ -470,7 +470,7 @@ span {
.searchTrigger {
position: relative;
top: .3em;
top: .2em;
}
.adminCommentInfoBar {
@@ -11,7 +11,7 @@ import NotFoundAsset from '../components/NotFoundAsset';
import {isPremod, getModPath} from '../../../utils';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {handleCommentChange} from '../../../graphql/utils';
import {handleCommentChange} from '../graphql';
import {fetchSettings} from 'actions/settings';
import {showBanUserDialog} from 'actions/banUserDialog';
@@ -30,23 +30,44 @@ import {
import {Spinner} from 'coral-ui';
import Moderation from '../components/Moderation';
import Comment from './Comment';
import queueConfig from '../queueConfig';
function prepareNotificationText(text) {
return truncate(text, {length: 50}).replace('\n', ' ');
}
function getAssetId(props) {
if (props.params.tabOrId && !(props.params.tabOrId in queueConfig)) {
return props.params.tabOrId;
}
return props.params.id || null;
}
function getTab(props) {
if (props.params.tabOrId && props.params.tabOrId in queueConfig) {
return props.params.tabOrId;
}
return props.params.tab || null;
}
class ModerationContainer extends Component {
subscriptions = [];
handleCommentChange = (root, comment, notify) => {
return handleCommentChange(root, comment, this.props.data.variables.sort, notify, queueConfig, this.activeTab);
};
get activeTab() {
const {root: {asset, settings}, router, route} = this.props;
const {root: {asset, settings}} = this.props;
const id = getAssetId(this.props);
const tab = getTab(this.props);
// Grab premod from asset or from settings
const premod = !router.params.id ? settings.moderation : asset.settings.moderation;
const premod = !id ? settings.moderation : asset.settings.moderation;
const queue = isPremod(premod) ? 'premod' : 'new';
const activeTab = route.path && route.path !== ':id' ? route.path : queue;
const activeTab = tab ? tab : queue;
return activeTab;
}
@@ -57,15 +78,10 @@ class ModerationContainer extends Component {
variables,
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const sort = this.props.moderation.sortOrder;
const notify = this.props.auth.user.id === user.id
? {}
: {
activeQueue: this.activeTab,
text: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body)),
anyQueue: false,
};
return handleCommentChange(prev, comment, sort, notify);
? ''
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notify);
},
});
@@ -74,15 +90,10 @@ class ModerationContainer extends Component {
variables,
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const sort = this.props.moderation.sortOrder;
const notify = this.props.auth.user.id === user.id
? {}
: {
activeQueue: this.activeTab,
text: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body)),
anyQueue: false,
};
return handleCommentChange(prev, comment, sort, notify);
? ''
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notify);
},
});
@@ -90,13 +101,8 @@ class ModerationContainer extends Component {
document: COMMENT_EDITED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
const sort = this.props.moderation.sortOrder;
const notify = {
activeQueue: this.activeTab,
text: t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body)),
anyQueue: false,
};
return handleCommentChange(prev, comment, sort, notify);
const notify = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notify);
},
});
@@ -105,13 +111,8 @@ class ModerationContainer extends Component {
variables,
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
const user = comment.actions[comment.actions.length - 1].user;
const sort = this.props.moderation.sortOrder;
const notify = {
activeQueue: this.activeTab,
text: t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body)),
anyQueue: true,
};
return handleCommentChange(prev, comment, sort, notify);
const notify = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notify);
},
});
@@ -160,28 +161,9 @@ class ModerationContainer extends Component {
cursor: this.props.root[tab].endCursor,
sort: this.props.data.variables.sort,
asset_id: this.props.data.variables.asset_id,
statuses: queueConfig[tab].statuses,
action_type: queueConfig[tab].action_type,
};
switch(tab) {
case 'all':
variables.statuses = null;
break;
case 'new':
variables.statuses = ['NONE', 'PREMOD'];
break;
case 'approved':
variables.statuses = ['ACCEPTED'];
break;
case 'premod':
variables.statuses = ['PREMOD'];
break;
case 'reported':
variables.statuses = ['NONE', 'PREMOD'];
variables.action_type = 'FLAG';
break;
case 'rejected':
variables.statuses = ['REJECTED'];
break;
}
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables,
@@ -199,7 +181,8 @@ class ModerationContainer extends Component {
};
render () {
const {root, root: {asset, settings}, data, params: {id: assetId}} = this.props;
const {root, root: {asset, settings}, data} = this.props;
const assetId = getAssetId(this.props);
if (data.error) {
return <div>Error</div>;
@@ -222,6 +205,14 @@ class ModerationContainer extends Component {
return <Spinner />;
}
const premodEnabled = assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation);
const currentQueueConfig = Object.assign({}, queueConfig);
if (premodEnabled) {
delete currentQueueConfig.new;
} else {
delete currentQueueConfig.premod;
}
return <Moderation
{...this.props}
getModPath={getModPath}
@@ -229,7 +220,8 @@ class ModerationContainer extends Component {
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
activeTab={this.activeTab}
premodEnabled={assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation)}
queueConfig={currentQueueConfig}
handleCommentChange={this.handleCommentChange}
/>;
}
}
@@ -314,49 +306,25 @@ const commentConnectionFragment = gql`
const withModQueueQuery = withQuery(gql`
query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER, $allAssets: Boolean!) {
all: comments(query: {
statuses: [NONE, PREMOD, ACCEPTED, REJECTED],
asset_id: $asset_id,
sort: $sort
}) {
...CoralAdmin_Moderation_CommentConnection
}
new: comments(query: {
statuses: [NONE, PREMOD],
asset_id: $asset_id,
sort: $sort
}) {
...CoralAdmin_Moderation_CommentConnection
}
approved: comments(query: {
statuses: [ACCEPTED],
asset_id: $asset_id,
sort: $sort
}) {
...CoralAdmin_Moderation_CommentConnection
}
premod: comments(query: {
statuses: [PREMOD],
${Object.keys(queueConfig).map((queue) => `
${queue}: comments(query: {
${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''}
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
asset_id: $asset_id,
sort: $sort
}) {
...CoralAdmin_Moderation_CommentConnection
}
reported: comments(query: {
action_type: FLAG,
}) {
...CoralAdmin_Moderation_CommentConnection
}
`)}
${Object.keys(queueConfig).map((queue) => `
${queue}Count: commentCount(query: {
${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''}
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
asset_id: $asset_id,
statuses: [NONE, PREMOD],
sort: $sort
}) {
...CoralAdmin_Moderation_CommentConnection
}
rejected: comments(query: {
statuses: [REJECTED],
asset_id: $asset_id,
sort: $sort
}) {
...CoralAdmin_Moderation_CommentConnection
}
})
`)}
asset(id: $asset_id) @skip(if: $allAssets) {
id
title
@@ -365,30 +333,6 @@ const withModQueueQuery = withQuery(gql`
moderation
}
}
allCount: commentCount(query: {
asset_id: $asset_id
})
newCount: commentCount(query: {
statuses: [NONE, PREMOD],
asset_id: $asset_id
})
approvedCount: commentCount(query: {
statuses: [ACCEPTED],
asset_id: $asset_id
})
premodCount: commentCount(query: {
statuses: [PREMOD],
asset_id: $asset_id
})
rejectedCount: commentCount(query: {
statuses: [REJECTED],
asset_id: $asset_id
})
reportedCount: commentCount(query: {
action_type: FLAG,
asset_id: $asset_id,
statuses: [NONE, PREMOD]
})
settings {
organizationName
moderation
@@ -396,11 +340,12 @@ const withModQueueQuery = withQuery(gql`
}
${commentConnectionFragment}
`, {
options: ({params: {id = null}, moderation: {sortOrder}}) => {
options: (props) => {
const id = getAssetId(props);
return {
variables: {
asset_id: id,
sort: sortOrder,
sort: props.moderation.sortOrder,
allAssets: id === null
}
};
@@ -409,33 +354,18 @@ const withModQueueQuery = withQuery(gql`
const withQueueCountPolling = withQuery(gql`
query CoralAdmin_ModerationCountPoll($asset_id: ID) {
allCount: commentCount(query: {
asset_id: $asset_id
})
newCount: commentCount(query: {
statuses: [NONE, PREMOD],
asset_id: $asset_id
})
approvedCount: commentCount(query: {
statuses: [ACCEPTED],
asset_id: $asset_id
})
premodCount: commentCount(query: {
statuses: [PREMOD],
asset_id: $asset_id
})
rejectedCount: commentCount(query: {
statuses: [REJECTED],
asset_id: $asset_id
})
reportedCount: commentCount(query: {
action_type: FLAG,
asset_id: $asset_id,
statuses: [NONE, PREMOD]
})
${Object.keys(queueConfig).map((queue) => `
${queue}Count: commentCount(query: {
${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''}
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
asset_id: $asset_id,
})
`)}
}
`, {
options: ({params: {id = null}}) => {
options: (props) => {
const id = getAssetId(props);
return {
pollInterval: 5000,
variables: {
@@ -1,7 +1,6 @@
import update from 'immutability-helper';
import * as notification from 'coral-admin/src/services/notification';
const queues = ['all', 'premod', 'reported', 'approved', 'rejected', 'new'];
const limit = 10;
const ascending = (a, b) => {
@@ -67,32 +66,24 @@ function addCommentToQueue(root, queue, comment, sort) {
/**
* getCommentQueues determines in which queues a comment should be placed.
*/
function getCommentQueues(comment) {
const queues = ['all'];
const isFlagged = comment.actions && comment.actions.some((a) => a.__typename === 'FlagAction');
switch(comment.status) {
case 'ACCEPTED':
queues.push('approved');
break;
case 'REJECTED':
queues.push('rejected');
break;
case 'PREMOD':
queues.push('premod');
queues.push('new');
if (isFlagged) {
queues.push('reported');
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;
}
break;
case 'NONE':
queues.push('new');
if (isFlagged) {
queues.push('reported');
if (tags && comment.tags && comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0)) {
addToQueues = true;
}
break;
}
if (action_type && comment.actions && comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type}action`)) {
addToQueues = true;
}
if (addToQueues) {
queues.push(key);
}
});
return queues;
}
@@ -106,42 +97,42 @@ function getCommentQueues(comment) {
* @param {string} notify.text notification text to show
* @param {bool} notify.anyQueue if true show the notification when the comment is shown
* in the current active queue besides the 'all' queue.
* @param {Object} queueConfig queue configuration
* @return {Object} next state of the store
*/
export function handleCommentChange(root, comment, sort, notify) {
export function handleCommentChange(root, comment, sort, notify, queueConfig, activeQueue) {
let next = root;
const nextQueues = getCommentQueues(comment);
const nextQueues = getCommentQueues(comment, queueConfig);
let notificationShown = false;
const showNotificationOnce = () => {
if (notificationShown) {
return;
}
notification.info(notify.text);
notification.info(notify);
notificationShown = true;
};
queues.forEach((queue) => {
Object.keys(queueConfig).forEach((queue) => {
if (nextQueues.indexOf(queue) >= 0) {
if (!queueHasComment(next, queue, comment.id)) {
next = addCommentToQueue(next, queue, comment, sort);
if (notify && notify.activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sort)) {
if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sort)) {
showNotificationOnce(comment);
}
}
} else if(queueHasComment(next, queue, comment.id)){
next = removeCommentFromQueue(next, queue, comment.id);
if (notify && notify.activeQueue === queue) {
if (notify && activeQueue === queue) {
showNotificationOnce(comment);
}
}
if (
notify
&& (queue === 'all' || notify.anyQueue)
&& queueHasComment(next, queue, comment.id)
&& notify.activeQueue === queue
&& activeQueue === queue
) {
showNotificationOnce(comment);
}
@@ -0,0 +1,37 @@
import t from 'coral-framework/services/i18n';
import {getModQueueConfigs} from 'coral-framework/helpers/plugins';
export default {
premod: {
statuses: ['PREMOD'],
icon: 'access_time',
name: t('modqueue.premod'),
},
new: {
statuses: ['NONE', 'PREMOD'],
icon: 'question_answer',
name: t('modqueue.new'),
},
reported: {
action_type: 'FLAG',
statuses: ['NONE', 'PREMOD'],
icon: 'flag',
name: t('modqueue.reported'),
},
approved: {
statuses: ['ACCEPTED'],
icon: 'check',
name: t('modqueue.approved'),
},
rejected: {
statuses: ['REJECTED'],
icon: 'close',
name: t('modqueue.rejected'),
},
all: {
statuses: ['NONE', 'PREMOD', 'ACCEPTED', 'REJECTED'],
icon: 'question_answer',
name: t('modqueue.all'),
},
...getModQueueConfigs(),
};
+1 -1
View File
@@ -13,7 +13,7 @@ body {
width: 100%;
font-size: 14px;
margin: 0px;
padding: 0px 0px 50px 0px;
padding: 0px 0px 100px 0px;
height: auto !important;
}
+8 -1
View File
@@ -3,6 +3,7 @@ import uniq from 'lodash/uniq';
import pick from 'lodash/pick';
import merge from 'lodash/merge';
import flattenDeep from 'lodash/flattenDeep';
import isEmpty from 'lodash/isEmpty';
import flatten from 'lodash/flatten';
import {loadTranslations} from 'coral-framework/services/i18n';
import {injectReducers} from 'coral-framework/services/store';
@@ -64,7 +65,13 @@ export function getSlotFragments(slot, part) {
export function getGraphQLExtensions() {
return plugins
.map((o) => pick(o.module, ['mutations', 'queries', 'fragments']))
.filter((o) => o);
.filter((o) => !isEmpty(o));
}
export function getModQueueConfigs() {
return merge(...plugins
.map((o) => o.module.modQueues)
.filter((o) => o));
}
function getTranslations() {
+2 -2
View File
@@ -92,13 +92,13 @@ export default (document, config = {}) => (WrappedComponent) => {
// Do not run updates when we have mutation errors.
return prev;
}
return map[key](prev, result);
return map[key](prev, result) || prev;
};
} else {
const existing = res[key];
res[key] = (prev, result) => {
const next = existing(prev, result);
return map[key](next, result);
return map[key](next, result) || next;
};
}
});
+4 -2
View File
@@ -128,8 +128,10 @@ export default (document, config = {}) => (WrappedComponent) => {
const reducer = withSkipOnErrors(
reducerCallbacks.reduce(
(a, b) => (prev, ...rest) =>
b(a(prev, ...rest), ...rest),
(a, b) => (prev, ...rest) => {
const next = a(prev, ...rest);
return b(next, ...rest) || next;
}
));
return {
+4 -3
View File
@@ -27,9 +27,10 @@
}
.icon {
margin-right: 13px;
margin-right: 5px;
font-size: 18px;
vertical-align: middle;
margin-top: -3px;
}
.type--black {
@@ -143,7 +144,7 @@
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 128px;
width: 129px;
&:hover {
box-shadow: none;
@@ -166,7 +167,7 @@
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 128px;
width: 129px;
&:hover {
color: white;
+19
View File
@@ -7,6 +7,8 @@
// entrypoint for the entire applications configuration.
require('env-rewrite').rewrite();
const uniq = require('lodash/uniq');
//==============================================================================
// CONFIG INITIALIZATION
//==============================================================================
@@ -31,6 +33,13 @@ const CONFIG = {
// token.
JWT_COOKIE_NAME: process.env.TALK_JWT_COOKIE_NAME || 'authorization',
// JWT_SIGNING_COOKIE_NAME will be the cookie set when cookies are issued.
// This defaults to the TALK_JWT_COOKIE_NAME value.
JWT_SIGNING_COOKIE_NAME: process.env.TALK_JWT_SIGNING_COOKIE_NAME || process.env.TALK_JWT_COOKIE_NAME || 'authorization',
// JWT_COOKIE_NAMES declares the many cookie names used for verification.
JWT_COOKIE_NAMES: process.env.TALK_JWT_COOKIE_NAMES || null,
// JWT_CLEAR_COOKIE_LOGOUT specifies whether the named cookie should be
// cleared when the user is logged out.
JWT_CLEAR_COOKIE_LOGOUT: process.env.TALK_JWT_CLEAR_COOKIE_LOGOUT ? process.env.TALK_JWT_CLEAR_COOKIE_LOGOUT !== 'FALSE' : true,
@@ -165,6 +174,16 @@ if (CONFIG.JWT_DISABLE_ISSUER) {
CONFIG.JWT_ISSUER = undefined;
}
// Parse and handle cookie names.
if (CONFIG.JWT_COOKIE_NAMES) {
CONFIG.JWT_COOKIE_NAMES = CONFIG.JWT_COOKIE_NAMES.split(',');
} else {
CONFIG.JWT_COOKIE_NAMES = [];
}
// Add in the default cookie names and strip duplicates.
CONFIG.JWT_COOKIE_NAMES = uniq(CONFIG.JWT_COOKIE_NAMES.concat([CONFIG.JWT_COOKIE_NAME, CONFIG.JWT_SIGNING_COOKIE_NAME]));
//------------------------------------------------------------------------------
// External database url's
//------------------------------------------------------------------------------
+18 -2
View File
@@ -87,8 +87,16 @@ on the contents of those variables.**
These are advanced settings for fine tuning the auth integration, and
is not needed in most situations.
- `TALK_JWT_COOKIE_NAME` (_optional_) - the name of the cookie to extract the
JWT from (Default `authorization`)
- `TALK_JWT_COOKIE_NAME` (_optional_) - the default cookie name to check for a
valid JWT token to use for verifying a user. (Default `authorization`)
- `TALK_JWT_SIGNING_COOKIE_NAME` (_optional_) - the default cookie name that is
use to set a cookie containing a JWT that was issued by Talk.
(Default `process.env.TALK_JWT_COOKIE_NAME`)
- `TALK_JWT_COOKIE_NAMES` (_optional_) - the different cookie names to check for
a JWT token in, seperated by `,`. By default, we always use the
`process.env.TALK_JWT_COOKIE_NAME` and `process.env.TALK_JWT_SIGNING_COOKIE_NAME`
for this value. Any additional cookie names specified here will be appended to
the list of cookie names to inspect.
- `TALK_JWT_CLEAR_COOKIE_LOGOUT` (_optional_) - when `FALSE`, Talk will not
clear the cookie with name `TALK_JWT_COOKIE_NAME` when logging out (Default
`TRUE`)
@@ -115,6 +123,14 @@ will be used:
}
```
When our passport middleware checks for JWT tokens, it searches in the following
order:
1. Custom cookies named from the list in `TALK_JWT_COOKIE_NAMES`.
2. Default cookies named `TALK_JWT_COOKIE_NAME` then `TALK_JWT_SIGNING_COOKIE_NAME`.
3. Query parameter `?access_token={TOKEN}`.
4. Header: `Authorization: Bearer {TOKEN}`.
### Email
- `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "talk",
"version": "3.0.0",
"version": "3.1.0",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"scripts": {
@@ -93,7 +93,7 @@
"graphql-tools": "^0.10.1",
"helmet": "^3.5.0",
"immutability-helper": "^2.2.0",
"inquirer": "^3.0.6",
"inquirer": "^3.2.1",
"joi": "^10.4.1",
"jsonwebtoken": "^7.3.0",
"jwt-decode": "^2.2.0",
@@ -32,7 +32,7 @@ const CreateUsernameDialog = ({
className={styles.fakeComment}
username={formData.username}
created_at={Date.now()}
comment={{body:t('createdisplay.fake_comment_body')}}
body={t('createdisplay.fake_comment_body')}
/>
<p className={styles.ifyoudont}>
{t('createdisplay.if_you_dont_change_your_name')}
@@ -0,0 +1,37 @@
.root {
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin-bottom: 10px;
padding: 8px 0px 10px 0px;
position: relative;
}
.body {
}
.footer {
display: flex;
justify-content: space-between;
}
.button {
color: #2a2a2a;
margin: 5px 10px 5px 0px;
background: none;
padding: 0px;
border: none;
font-size: inherit;
vertical-align: middle;
&:hover {
color: #767676;
cursor: pointer;
}
}
.icon {
font-size: 12px;
padding: 0 2px 0 5px;
vertical-align: middle;
}
@@ -2,65 +2,43 @@ import React from 'react';
import t from 'coral-framework/services/i18n';
import {ReplyButton} from 'talk-plugin-replies';
import PubDate from 'talk-plugin-pubdate/PubDate';
import Slot from 'coral-framework/components/Slot';
import AuthorName from 'talk-plugin-author-name/AuthorName';
import styles from 'coral-embed-stream/src/components/Comment.css';
import styles from './FakeComment.css';
import {Icon} from 'plugin-api/beta/client/components/ui';
export const FakeComment = ({username, created_at, comment}) => (
<div className={`comment ${styles.Comment}`} style={{marginLeft: 0 * 30}}>
<hr aria-hidden={true} />
<AuthorName author={{name: username}} />
export const FakeComment = ({username, created_at, body}) => (
<div className={styles.root}>
<AuthorName author={{username}} />
<PubDate created_at={created_at} />
<Slot comment={comment} />
<div className="commentActionsLeft comment__action-container">
<div className={`${'talk-plugin-likes'}-container`}>
<button className={'talk-plugin-likes-button'}>
<span className={'talk-plugin-likes-button-text'}>
<div className={styles.body}>
{body}
</div>
<div className={styles.footer}>
<div>
<button className={styles.button}>
<span className={styles.label}>
{t('like')}
</span>
<i
className={`${'talk-plugin-likes'}-icon material-icons`}
aria-hidden={true}
>
thumb_up
</i>
<Icon name="thumb_up" className={styles.icon} />
</button>
<ReplyButton
onClick={() => {}}
parentCommentId={'commentID'}
currentUserId={{}}
/>
</div>
<ReplyButton
onClick={() => {}}
parentCommentId={'commentID'}
currentUserId={{}}
/>
</div>
<div className="commentActionsRight comment__action-container">
<div className="talk-plugin-permalinks-container">
<button className="talk-plugin-permalinks-button">
<span
className={`comment__action-button comment__action-button--nowrap ${'talk-plugin-flags'}-button-text`}
>
<div>
<button className={styles.button}>
<span className={styles.label}>
{t('permalink')}
</span>
<i
className="talk-plugin-permalinks-icon material-icons"
aria-hidden={true}
>
link
</i>
<Icon name="link" className={styles.icon} />
</button>
</div>
<div className={`${'talk-plugin-flags'}-container`}>
<button className={`${'talk-plugin-flags'}-button`}>
<span
className={`comment__action-button comment__action-button--nowrap ${'talk-plugin-flags'}-button-text`}
>
<button className={styles.button}>
<span className={styles.label}>
{t('report')}
</span>
<i
className={`${'talk-plugin-flags'}-icon material-icons`}
aria-hidden={true}
>
flag
</i>
<Icon name="flag" className={styles.icon} />
</button>
</div>
</div>
@@ -4,13 +4,14 @@
color: #696969;
background-color: white;
box-sizing: border-box;
padding: 2px 8px;
padding: 0px 5px;
border-radius: 2px;
font-size: 12px;
height: 28px;
height: 24px;
transition: background-color .2s cubic-bezier(.4,0,.2,1), color .2s cubic-bezier(.4,0,.2,1), border-color .2s cubic-bezier(.4,0,.2,1);
margin: 2px 0px;
letter-spacing: 0.4px;
}
.tag:hover {
@@ -39,4 +40,3 @@
font-size: 15px;
vertical-align: text-bottom;
}
@@ -2,7 +2,6 @@ import React from 'react';
import {gql} from 'react-apollo';
import {connect} from 'react-redux';
import Comment from 'coral-admin/src/routes/Moderation/containers/Comment';
import {handleCommentChange} from 'coral-admin/src/graphql/utils';
import {getDefinitionName} from 'coral-framework/utils';
import truncate from 'lodash/truncate';
import t from 'coral-framework/services/i18n';
@@ -22,20 +21,14 @@ class ModSubscription extends React.Component {
assetId: this.props.data.variables.asset_id,
},
updateQuery: (prev, {subscriptionData: {data: {commentFeatured: {user, comment}}}}) => {
const sort = this.props.data.variables.sort;
const text = this.props.user.id === user.id
? {}
const notify = this.props.user.id === user.id
? ''
: t(
'talk-plugin-featured-comments.notify_featured',
user.username,
prepareNotificationText(comment.body),
);
const notify = {
activeQueue: this.props.activeTab,
text,
anyQueue: true,
};
return handleCommentChange(prev, comment, sort, notify);
return this.props.handleCommentChange(prev, comment, notify);
},
},
{
@@ -44,20 +37,14 @@ class ModSubscription extends React.Component {
assetId: this.props.data.variables.asset_id,
},
updateQuery: (prev, {subscriptionData: {data: {commentUnfeatured: {user, comment}}}}) => {
const sort = this.props.data.variables.sort;
const text = this.props.user.id === user.id
? {}
const notify = this.props.user.id === user.id
? ''
: t(
'talk-plugin-featured-comments.notify_unfeatured',
user.username,
prepareNotificationText(comment.body),
);
const notify = {
activeQueue: this.props.activeTab,
text,
anyQueue: true,
};
return handleCommentChange(prev, comment, sort, notify);
return this.props.handleCommentChange(prev, comment, notify);
}
},
];
@@ -26,6 +26,9 @@ export default {
IgnoreUser: ({variables}) => ({
updateQueries: {
CoralEmbedStream_Embed: (previous) => {
if (!previous.asset.featuredComments) {
return previous;
}
const ignoredUserId = variables.id;
const newNodes = previous.asset.featuredComments.nodes.filter((n) => n.user.id !== ignoredUserId);
const removedCount = previous.asset.featuredComments.nodes.length - newNodes.length;
@@ -16,7 +16,6 @@ export default class OffTopicFilter extends React.Component {
this.props.removeCommentClassName(idx);
this.props.toggleCheckbox();
}
this.props.closeViewingOptions();
}
render() {
@@ -3,9 +3,6 @@ import {bindActionCreators} from 'redux';
import {toggleCheckbox} from '../actions';
import {commentClassNamesSelector} from 'plugin-api/alpha/client/selectors';
import OffTopicFilter from '../components/OffTopicFilter';
import {
closeViewingOptions
} from 'plugins/talk-plugin-viewing-options/client/actions';
import {
addCommentClassName,
removeCommentClassName
@@ -20,7 +17,6 @@ const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
toggleCheckbox,
closeViewingOptions,
addCommentClassName,
removeCommentClassName
},
+15 -8
View File
@@ -23,7 +23,8 @@ const {
JWT_ALG,
RECAPTCHA_SECRET,
RECAPTCHA_ENABLED,
JWT_COOKIE_NAME,
JWT_SIGNING_COOKIE_NAME,
JWT_COOKIE_NAMES,
JWT_CLEAR_COOKIE_LOGOUT,
JWT_USER_ID_CLAIM,
} = require('../config');
@@ -53,7 +54,7 @@ const GenerateToken = (user) => {
const SetTokenForSafari = (req, res, token) => {
const browser = bowser._detect(req.headers['user-agent']);
if (browser.ios || browser.safari) {
res.cookie(JWT_COOKIE_NAME, token, {
res.cookie(JWT_SIGNING_COOKIE_NAME, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
expires: new Date(Date.now() + ms(JWT_EXPIRY))
@@ -169,7 +170,7 @@ const HandleLogout = (req, res, next) => {
// Only clear the cookie on logout if enabled.
if (JWT_CLEAR_COOKIE_LOGOUT) {
res.clearCookie(JWT_COOKIE_NAME);
res.clearCookie(JWT_SIGNING_COOKIE_NAME);
}
res.status(204).end();
@@ -209,14 +210,20 @@ const CheckBlacklisted = async (jwt) => {
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
let cookieExtractor = function(req) {
let token = null;
let cookieExtractor = (req) => {
if (req && req.cookies) {
token = req.cookies[JWT_COOKIE_NAME];
// Walk over all the cookie names in JWT_COOKIE_NAMES.
for (const cookieName of JWT_COOKIE_NAMES) {
// Check to see if that cookie is set.
if (cookieName in req.cookies && req.cookies[cookieName] !== null && req.cookies[cookieName].length > 0) {
return req.cookies[cookieName];
}
}
}
return token;
return null;
};
// Override the JwtVerifier method on the JwtStrategy so we can pack the
+2 -2
View File
@@ -205,8 +205,8 @@ class TagsService {
return updateModel(item_type, query, {
$pull: {
tags: {
name: link.tag.name
}
'tag.name': link.tag.name,
},
}
});
}
+40 -2
View File
@@ -27,7 +27,7 @@ describe('services.TagsService', () => {
const id = comment.id;
const name = 'BEST';
const assigned_by = user.id;
await TagsService.add(id, 'COMMENTS', {
tag: {
name
@@ -45,7 +45,7 @@ describe('services.TagsService', () => {
const id = comment.id;
const name = 'BEST';
const assigned_by = user.id;
await TagsService.add(id, 'COMMENTS', {
tag: {
name
@@ -103,5 +103,43 @@ describe('services.TagsService', () => {
expect(tags.length).to.equal(0);
}
});
it('removes a tag out of 2', async () => {
const id = comment.id;
const name = 'BEST';
const assigned_by = user.id;
await TagsService.add(id, 'COMMENTS', {
tag: {
name: 'ANOTHER'
},
assigned_by
});
await TagsService.add(id, 'COMMENTS', {
tag: {
name
},
assigned_by
});
{
const {tags} = await CommentsService.findById(id);
expect(tags.length).to.equal(2);
}
// ok now to remove it
await TagsService.remove(id, 'COMMENTS', {
tag: {
name
},
assigned_by
});
{
const {tags} = await CommentsService.findById(id);
expect(tags.length).to.equal(1);
expect(tags[0].tag.name).to.equal('ANOTHER');
}
});
});
});
+79 -17
View File
@@ -162,6 +162,10 @@ ansi-escapes@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
ansi-escapes@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b"
ansi-regex@^1.0.0, ansi-regex@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-1.1.1.tgz#41c847194646375e6a1a5d10c3ca054ef9fc980d"
@@ -170,10 +174,20 @@ ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
ansi-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
ansi-styles@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
dependencies:
color-convert "^1.9.0"
any-promise@^0.1.0, any-promise@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-0.1.0.tgz#830b680aa7e56f33451d4b049f3bd8044498ee27"
@@ -1498,6 +1512,14 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
chalk@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e"
dependencies:
ansi-styles "^3.1.0"
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
change-emitter@^0.1.2:
version "0.1.6"
resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515"
@@ -1727,7 +1749,7 @@ codemirror@*:
version "5.25.2"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.25.2.tgz#8c77677ca9c9248d757d3a07ed1e89a8404850b7"
color-convert@^1.3.0:
color-convert@^1.3.0, color-convert@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
dependencies:
@@ -3053,10 +3075,12 @@ extend@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extend/-/extend-1.3.0.tgz#d1516fb0ff5624d2ebf9123ea1dac5a1994004f8"
external-editor@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.1.tgz#4c597c6c88fa6410e41dbbaa7b1be2336aa31095"
external-editor@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.4.tgz#1ed9199da9cbfe2ef2f7a31b2fde8b0d12368972"
dependencies:
iconv-lite "^0.4.17"
jschardet "^1.4.2"
tmp "^0.0.31"
extglob@^0.3.1:
@@ -3757,6 +3781,10 @@ has-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
has-flag@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
has-unicode@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
@@ -3972,6 +4000,10 @@ iconv-lite@0.4.15, iconv-lite@^0.4.5, iconv-lite@~0.4.13:
version "0.4.15"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
iconv-lite@^0.4.17:
version "0.4.18"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
icss-replace-symbols@1.0.2, icss-replace-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.0.2.tgz#cb0b6054eb3af6edc9ab1d62d01933e2d4c8bfa5"
@@ -4103,22 +4135,23 @@ inquirer@0.8.2:
rx "^2.4.3"
through "^2.3.6"
inquirer@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.0.6.tgz#e04aaa9d05b7a3cb9b0f407d04375f0447190347"
inquirer@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.1.tgz#06ceb0f540f45ca548c17d6840959878265fa175"
dependencies:
ansi-escapes "^1.1.0"
chalk "^1.0.0"
ansi-escapes "^2.0.0"
chalk "^2.0.0"
cli-cursor "^2.1.0"
cli-width "^2.0.0"
external-editor "^2.0.1"
external-editor "^2.0.4"
figures "^2.0.0"
lodash "^4.3.0"
mute-stream "0.0.7"
run-async "^2.2.0"
rx "^4.1.0"
string-width "^2.0.0"
strip-ansi "^3.0.0"
rx-lite "^4.0.8"
rx-lite-aggregates "^4.0.8"
string-width "^2.1.0"
strip-ansi "^4.0.0"
through "^2.3.6"
interpret@^1.0.0:
@@ -4560,6 +4593,10 @@ jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
jschardet@^1.4.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.1.tgz#c519f629f86b3a5bedba58a88d311309eec097f9"
jsdom@^7.0.2:
version "7.2.2"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-7.2.2.tgz#40b402770c2bda23469096bee91ab675e3b1fc6e"
@@ -7434,6 +7471,16 @@ run-async@^2.2.0:
dependencies:
is-promise "^2.1.0"
rx-lite-aggregates@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
dependencies:
rx-lite "*"
rx-lite@*, rx-lite@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
rx-lite@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
@@ -7442,10 +7489,6 @@ rx@^2.4.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/rx/-/rx-2.5.3.tgz#21adc7d80f02002af50dae97fd9dbf248755f566"
rx@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
safe-buffer@^5.0.1, safe-buffer@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
@@ -7833,6 +7876,13 @@ string-width@^2.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^3.0.0"
string-width@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
dependencies:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
string.prototype.codepointat@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78"
@@ -7871,6 +7921,12 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1:
dependencies:
ansi-regex "^2.0.0"
strip-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
dependencies:
ansi-regex "^3.0.0"
strip-bom@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
@@ -7961,6 +8017,12 @@ supports-color@^3.1.2, supports-color@^3.2.3:
dependencies:
has-flag "^1.0.0"
supports-color@^4.0.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836"
dependencies:
has-flag "^2.0.0"
svgo@^0.7.0:
version "0.7.2"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"