Merge branch 'master' into pre-responsive-admin-cleanup

This commit is contained in:
Kim Gardner
2017-08-10 10:57:40 +01:00
committed by GitHub
30 changed files with 293 additions and 101 deletions
+5 -3
View File
@@ -61,9 +61,11 @@ class FlagBox extends Component {
<ul>
{actionList.map((action, j) =>
<li key={`${i}_${j}`} className={styles.subDetail}>
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
{action.user.username}
</a>
{action.user &&
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
{action.user.username}
</a>
}
{action.message}
</li>
)}
@@ -0,0 +1,21 @@
.loadMoreContainer {
display: flex;
justify-content: center;
width: 100%;
}
.loadMore {
width: 100%;
text-align: center;
color: #FFF;
max-width: 660px;
margin-bottom: 30px;
background-color: #2376D8;
cursor: pointer;
}
.loadMore:hover {
background-color: #4399FF;
}
@@ -1,9 +1,10 @@
import React, {PropTypes} from 'react';
import {Button} from 'coral-ui';
import styles from './styles.css';
import styles from './LoadMore.css';
import cn from 'classnames';
const LoadMore = ({loadMore, showLoadMore}) =>
<div className={styles.loadMoreContainer}>
const LoadMore = ({loadMore, showLoadMore, className, ...rest}) =>
<div {...rest} className={cn(className, styles.loadMoreContainer)}>
{
showLoadMore && <Button
className={styles.loadMore}
@@ -79,3 +79,12 @@
margin-left: -10px;
}
}
.loadMore > button {
background-color: #696969;
&:hover {
background-color: #404040;
color: white;
}
}
@@ -6,6 +6,7 @@ import {Slot} from 'coral-framework/components';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import {actionsMap} from '../utils/moderationQueueActionsMap';
import ClickOutside from 'coral-framework/components/ClickOutside';
import LoadMore from '../components/LoadMore';
export default class UserDetail extends React.Component {
@@ -59,7 +60,7 @@ export default class UserDetail extends React.Component {
user,
totalComments,
rejectedComments,
comments: {nodes}
comments: {nodes, hasNextPage}
},
activeTab,
selectedCommentIds,
@@ -70,6 +71,7 @@ export default class UserDetail extends React.Component {
bulkReject,
hideUserDetail,
viewUserDetail,
loadMore,
} = this.props;
const localProfile = user.profiles.find((p) => p.provider === 'local');
@@ -167,6 +169,11 @@ export default class UserDetail extends React.Component {
})
}
</div>
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={hasNextPage}
/>
</Drawer>
</ClickOutside>
);
@@ -8,6 +8,10 @@
min-height: 0;
}
.root:last-child {
border: 0;
}
.rootSelected {
background-color: #ecf4ff;
}
@@ -14,6 +14,7 @@ import {
} from 'coral-admin/src/actions/userDetail';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
import update from 'immutability-helper';
const commentConnectionFragment = gql`
fragment CoralAdmin_Moderation_CommentConnection on CommentConnection {
@@ -32,6 +33,7 @@ const slots = [
];
class UserDetailContainer extends React.Component {
isLoadingMore = false;
// status can be 'ACCEPTED' or 'REJECTED'
bulkSetCommentStatus = (status) => {
@@ -40,7 +42,6 @@ class UserDetailContainer extends React.Component {
});
Promise.all(changes).then(() => {
this.props.data.refetch(); // some comments may have moved out of this tab
this.props.clearUserDetailSelections(); // un-select everything
});
}
@@ -61,12 +62,53 @@ class UserDetailContainer extends React.Component {
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
}
loadMore = () => {
if (this.isLoadingMore) {
return;
}
this.isLoadingMore = true;
const variables = {
limit: 10,
cursor: this.props.root.comments.endCursor,
author_id: this.props.data.variables.author_id,
statuses: this.props.data.variables.statuses,
};
this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables,
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
return update(prev, {
comments: {
nodes: {$push: comments.nodes},
hasNextPage: {$set: comments.hasNextPage},
startCursor: {$set: comments.startCursor},
endCursor: {$set: comments.endCursor},
},
});
}
})
.then(() => {
this.isLoadingMore = false;
})
.catch((err) => {
this.isLoadingMore = false;
throw err;
});
};
componentWillReceiveProps(next) {
if (this.props.userId === null && next.userId) {
next.data.refetch();
}
}
render () {
if (!this.props.userId) {
return null;
}
const loading = !('user' in this.props.root) || this.props.root.user.id !== this.props.userId;
const loading = [1, 2, 4].indexOf(this.props.data.networkStatus) >= 0;
return <UserDetail
bulkReject={this.bulkReject}
@@ -76,10 +118,20 @@ class UserDetailContainer extends React.Component {
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
loadMore={this.loadMore}
{...this.props} />;
}
}
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
...CoralAdmin_Moderation_CommentConnection
}
}
${commentConnectionFragment}
`;
export const withUserDetailQuery = withQuery(gql`
query CoralAdmin_UserDetail($author_id: ID!, $statuses: [COMMENT_STATUS!]) {
user(id: $author_id) {
@@ -4,7 +4,7 @@ import Comment from '../containers/Comment';
import styles from './styles.css';
import EmptyCard from '../../../components/EmptyCard';
import {actionsMap} from '../../../utils/moderationQueueActionsMap';
import LoadMore from './LoadMore';
import LoadMore from '../../../components/LoadMore';
import t from 'coral-framework/services/i18n';
import {CSSTransitionGroup} from 'react-transition-group';
@@ -397,26 +397,6 @@ span {
}
}
.loadMoreContainer {
display: flex;
justify-content: center;
width: 100%;
};
.loadMore {
width: 100%;
text-align: center;
color: #FFF;
max-width: 660px;
margin-bottom: 30px;
background-color: #2376D8;
cursor: pointer;
}
.loadMore:hover {
background-color: #4399FF;
}
.tabIcon {
position: relative;
top: 3px;
@@ -93,6 +93,7 @@ class AllCommentsPane extends React.Component {
viewNewComments = () => {
this.setState(resetCursors);
this.props.emit('ui.AllCommentsPane.viewNewComments');
};
// getVisibileComments returns a list containing comments
@@ -142,6 +143,7 @@ class AllCommentsPane extends React.Component {
charCountEnable,
maxCharCount,
editComment,
emit,
} = this.props;
const {loadingState} = this.state;
@@ -181,6 +183,7 @@ class AllCommentsPane extends React.Component {
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
editComment={editComment}
emit={emit}
/>;
})}
</TransitionGroup>
@@ -224,6 +224,7 @@ export default class Comment extends React.Component {
return;
}
this.setState(resetCursors);
this.props.emit('ui.Comment.showMoreReplies');
};
showReplyBox = () => {
@@ -400,13 +401,13 @@ export default class Comment extends React.Component {
<div className={commentClassName}>
<Slot
className={styles.commentAvatar}
className={`${styles.commentAvatar} talk-stream-comment-avatar`}
fill="commentAvatar"
{...slotProps}
inline
/>
<div className={styles.commentContainer}>
<div className={`${styles.commentContainer} talk-stream-comment-container`}>
<div className={styles.header}>
<AuthorName author={comment.user} className={'talk-stream-comment-user-name'} />
@@ -292,6 +292,7 @@ class Stream extends React.Component {
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
emit={this.props.emit}
/>
</TabPane>
</TabContent>
@@ -1,10 +1,10 @@
import React from 'react';
import ClickOutside from 'coral-framework/components/ClickOutside';
import styles from './Toggleable.css';
import classnames from 'classnames';
import cn from 'classnames';
const upArrow = <span className={classnames(styles.chevron, styles.up)}></span>;
const downArrow = <span className={classnames(styles.chevron, styles.down)}></span>;
const upArrow = <span className={cn(styles.chevron, styles.up)}></span>;
const downArrow = <span className={cn(styles.chevron, styles.down)}></span>;
export default class Toggleable extends React.Component {
constructor(props) {
@@ -23,11 +23,11 @@ export default class Toggleable extends React.Component {
}
render() {
const {children} = this.props;
const {children, className, ...rest} = this.props;
const {isOpen} = this.state;
return (
<ClickOutside onClickOutside={this.close}>
<span className={styles.Toggleable}>
<span {...rest} className={cn(className, styles.Toggleable)} >
<button className={styles.toggler} onClick={this.toggle}>{isOpen ? upArrow : downArrow}</button>
{isOpen ? children : null}
</span>
@@ -45,7 +45,7 @@ export class TopRightMenu extends React.Component {
}
};
return (
<Toggleable key={this.state.timesReset}>
<Toggleable key={this.state.timesReset} className="talk-stream-comment-chevron">
<div style={{position: 'absolute', right: 0, zIndex: 1}}>
<IgnoreUserWizard
user={comment.user}
@@ -14,7 +14,7 @@ import {editName} from 'coral-framework/actions/user';
import {setActiveReplyBox, setActiveTab, viewAllComments} from '../actions/stream';
import Stream from '../components/Stream';
import Comment from './Comment';
import {withFragments} from 'coral-framework/hocs';
import {withFragments, withEmit} from 'coral-framework/hocs';
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
import {Spinner} from 'coral-ui';
import {
@@ -326,6 +326,7 @@ const mapDispatchToProps = (dispatch) =>
export default compose(
withFragments(fragments),
withEmit,
connect(mapStateToProps, mapDispatchToProps),
withPostComment,
withPostFlag,
@@ -73,6 +73,11 @@ const extension = {
created_at
status
replyCount
asset {
id
title
url
}
tags {
tag {
name
@@ -190,6 +195,15 @@ const extension = {
}
return insertCommentIntoEmbedQuery(prev, comment);
},
CoralEmbedStream_Profile: (prev, {mutationResult: {data: {createComment: {comment}}}}) => {
return update(prev, {
me: {
comments: {
nodes: {$unshift: [comment]},
},
},
});
},
}
}),
EditComment: () => ({
+2 -2
View File
@@ -10,7 +10,7 @@ import camelize from './camelize';
import plugins from 'pluginsConfig';
export function getSlotComponents(slot, reduxState, props = {}) {
const pluginConfig = reduxState.config.pluginConfig || {};
const pluginConfig = reduxState.config.plugin_config || {};
return flatten(plugins
// Filter out components that have slots and have been disabled in `plugin_config`
@@ -39,7 +39,7 @@ export function isSlotEmpty(slot, reduxState, props) {
* Returns React Elements for given slot.
*/
export function getSlotElements(slot, reduxState, props = {}) {
const pluginConfig = reduxState.config.pluginConfig || {};
const pluginConfig = reduxState.config.plugin_config || {};
return getSlotComponents(slot, reduxState, props)
.map((component, i) => React.createElement(component, {key: i, ...props, config: pluginConfig}));
}
@@ -1,7 +1,8 @@
import {connect} from 'react-redux';
import {compose, graphql, gql} from 'react-apollo';
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';
@@ -11,18 +12,12 @@ import IgnoredUsers from '../components/IgnoredUsers';
import {Spinner} from 'coral-ui';
import CommentHistory from 'talk-plugin-history/CommentHistory';
import {showSignInDialog, checkLogin} from 'coral-framework/actions/auth';
import {insertCommentsSorted} from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import t from 'coral-framework/services/i18n';
class ProfileContainer extends Component {
constructor() {
super();
this.state = {
activeTab: 0
};
}
componentWillReceiveProps(nextProps) {
if (!this.props.auth.loggedIn && nextProps.auth.loggedIn) {
@@ -31,21 +26,40 @@ class ProfileContainer extends Component {
}
}
handleTabChange = (tab) => {
this.setState({
activeTab: tab
loadMore = () => {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
limit: 5,
cursor: this.props.root.me.comments.endCursor,
},
updateQuery: (previous, {fetchMoreResult:{comments}}) => {
const updated = update(previous, {
me: {
comments: {
nodes: {
$apply: (nodes) => insertCommentsSorted(nodes, comments.nodes, 'REVERSE_CHRONOLOGICAL'),
},
hasNextPage: {$set: comments.hasNextPage},
endCursor: {$set: comments.endCursor},
},
}
});
return updated;
},
});
};
render() {
const {auth, asset, data, showSignInDialog, stopIgnoringUser} = this.props;
const {me} = this.props.data;
const {auth, asset, showSignInDialog, stopIgnoringUser} = this.props;
const {me} = this.props.root;
const loading = [1, 2, 4].indexOf(this.props.data.networkStatus) >= 0;
if (!auth.loggedIn) {
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
}
if (!me || data.loading) {
if (loading) {
return <Spinner />;
}
@@ -73,14 +87,40 @@ class ProfileContainer extends Component {
<h3>{t('framework.my_comments')}</h3>
{me.comments.nodes.length
? <CommentHistory comments={me.comments.nodes} asset={asset} link={link} />
? <CommentHistory comments={me.comments} asset={asset} link={link} loadMore={this.loadMore}/>
: <p>{t('user_no_comment')}</p>}
</div>
);
}
}
const withQuery = graphql(
const CommentFragment = gql`
fragment TalkSettings_CommentConnectionFragment on CommentConnection {
nodes {
id
body
asset {
id
title
url
}
created_at
}
endCursor
hasNextPage
}
`;
const LOAD_MORE_QUERY = gql`
query TalkSettings_LoadMoreComments($limit: Int, $cursor: Date) {
comments(query: {limit: $limit, cursor: $cursor}) {
...TalkSettings_CommentConnectionFragment
}
}
${CommentFragment}
`;
const withProfileQuery = withQuery(
gql`
query CoralEmbedStream_Profile {
me {
@@ -89,21 +129,13 @@ const withQuery = graphql(
id,
username,
}
comments {
nodes {
id
body
asset {
id
title
url
}
created_at
}
comments(query: {limit: 10}) {
...TalkSettings_CommentConnectionFragment
}
}
}`
);
}
${CommentFragment}
`);
const mapStateToProps = (state) => ({
user: state.user.toJS(),
@@ -117,5 +149,5 @@ const mapDispatchToProps = (dispatch) =>
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withStopIgnoringUser,
withQuery
withProfileQuery
)(ProfileContainer);
+1 -1
View File
@@ -3,7 +3,7 @@
min-width: 550px;
position: fixed;
top: 0;
right: -17px;
right: 0px;
bottom: 0;
background-color: white;
transition: transform 500ms ease-in-out;
+42 -15
View File
@@ -1,25 +1,52 @@
import React, {PropTypes} from 'react';
import Comment from './Comment';
import styles from './CommentHistory.css';
import LoadMore from './LoadMore';
import {forEachError} from 'plugin-api/beta/client/utils';
const CommentHistory = (props) => {
return (
<div className={`${styles.header} commentHistory`}>
<div className="commentHistory__list">
{props.comments.map((comment, i) => {
return <Comment
key={i}
comment={comment}
link={props.link}
asset={comment.asset} />;
})}
class CommentHistory extends React.Component {
state = {
loadingState: '',
};
loadMore = () => {
this.setState({loadingState: 'loading'});
this.props.loadMore()
.then(() => {
this.setState({loadingState: 'success'});
})
.catch((error) => {
this.setState({loadingState: 'error'});
forEachError(error, ({msg}) => {this.props.addNotification('error', msg);});
});
}
render() {
const {link, comments} = this.props;
return (
<div className={`${styles.header} commentHistory`}>
<div className="commentHistory__list">
{comments.nodes.map((comment, i) => {
return <Comment
key={i}
comment={comment}
link={link}
asset={comment.asset} />;
})}
</div>
{comments.hasNextPage &&
<LoadMore
loadMore={this.loadMore}
loadingState={this.state.loadingState}
/>
}
</div>
</div>
);
};
);
}
}
CommentHistory.propTypes = {
comments: PropTypes.array.isRequired
comments: PropTypes.object.isRequired
};
export default CommentHistory;
+30
View File
@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import t from 'coral-framework/services/i18n';
import cn from 'classnames';
class LoadMore extends React.Component {
render () {
const {loadingState, loadMore} = this.props;
const disabled = loadingState === 'loading';
return (
<div className='talk-load-more'>
<Button
onClick={loadMore}
className={cn('talk-load-more-button', {[`talk-load-more-button-${loadingState}`]: loadingState})}
disabled={disabled}
>
{t('framework.view_more_comments')}
</Button>
</div>
);
}
}
LoadMore.propTypes = {
loadMore: PropTypes.func.isRequired,
loadingState: PropTypes.oneOf(['', 'loading', 'success', 'error']),
};
export default LoadMore;
+1 -1
View File
@@ -1,6 +1,6 @@
---
title: Development Tooling
permalink: /docs/development/tools
permalink: /docs/development/tools/
---
## Debugging
+8 -5
View File
@@ -2,18 +2,21 @@ const errors = require('../../errors');
const {Error: {ValidationError}} = require('mongoose');
/**
* Wraps up a promise to return an object with the resolution of the promise
* Wraps up a promise or value to return an object with the resolution of the promise
* keyed at `key` or an error caught at `errors`.
*/
const wrapResponse = (key) => (promise) => {
return promise.then((value) => {
const wrapResponse = (key) => async (promise) => {
try {
let value = await promise;
let res = {};
if (key) {
res[key] = value;
}
return res;
}).catch((err) => {
} catch (err) {
if (err instanceof errors.APIError) {
return {
errors: [err]
@@ -25,7 +28,7 @@ const wrapResponse = (key) => (promise) => {
}
throw err;
});
}
};
module.exports = wrapResponse;
+2 -2
View File
@@ -29,12 +29,12 @@ const User = {
return null;
},
comments({id}, _, {loaders: {Comments}, user}) {
comments({id}, {query}, {loaders: {Comments}, user}) {
// If the user is not an admin, only return comment list for the owner of
// the comments.
if (user && (user.can(SEARCH_OTHERS_COMMENTS) || user.id === id)) {
return Comments.getByQuery({author_id: id, sort: 'REVERSE_CHRONOLOGICAL'});
return Comments.getByQuery(Object.assign({}, query, {author_id: id}));
}
return null;
+1
View File
@@ -3,3 +3,4 @@ export {default as withTags} from './withTags';
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';
@@ -271,6 +271,7 @@ export default (reaction) => (WrappedComponent) => {
alreadyReacted={alreadyReacted}
postReaction={this.postReaction}
deleteReaction={this.deleteReaction}
config={this.props.config}
/>;
}
}
+3 -2
View File
@@ -68,16 +68,17 @@ export default (tag) => (WrappedComponent) => {
}
render() {
const {comment} = this.props;
const {comment, user, config} = this.props;
const alreadyTagged = isTagged(comment.tags, TAG);
return <WrappedComponent
user={this.props.user}
user={user}
comment={comment}
alreadyTagged={alreadyTagged}
postTag={this.postTag}
deleteTag={this.deleteTag}
config={config}
/>;
}
}
@@ -6,7 +6,7 @@ import {showSignInDialog} from 'coral-framework/actions/auth';
import t from 'coral-framework/services/i18n';
const SignInButton = ({loggedIn, showSignInDialog}) => (
<div>
<div className="talk-stream-auth-sign-in-button">
{!loggedIn
? <Button id="coralSignInButton" onClick={showSignInDialog} full>
{t('sign_in.sign_in_to_comment')}
@@ -36,7 +36,7 @@ class TabPane extends React.Component {
{featuredComments.hasNextPage &&
<LoadMore
loadMore={this.loadMore}
loadingState={this.loadingState}
loadingState={this.state.loadingState}
/>
}
</div>
+2 -1
View File
@@ -173,7 +173,8 @@ module.exports = class ActionsService {
return ActionModel.aggregate([
{$match},
{$group},
{$project}
{$project},
{$sort: {action_type: 1, group_id: 1}},
]);
}