Merge pull request #567 from coralproject/query-mutation-api

Extendable GraphQL framework
This commit is contained in:
David Erwin
2017-05-12 09:43:50 -04:00
committed by GitHub
35 changed files with 907 additions and 498 deletions
@@ -65,7 +65,7 @@ class Comment extends React.Component {
parentId: PropTypes.string,
highlighted: PropTypes.string,
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
depth: PropTypes.number.isRequired,
asset: PropTypes.shape({
id: PropTypes.string,
@@ -145,7 +145,7 @@ class Comment extends React.Component {
// re-render
this.setState(this.state);
}, msLeftToEdit);
}, msLeftToEdit);
}
}
componentWillUnmount() {
@@ -161,7 +161,7 @@ class Comment extends React.Component {
currentUser,
asset,
depth,
postItem,
postComment,
addNotification,
showSignInDialog,
highlighted,
@@ -286,7 +286,7 @@ class Comment extends React.Component {
</span>
}
{
{
this.state.isEditing
? <EditableCommentContent
editComment={this.props.editComment.bind(null, comment.id, asset.id)}
@@ -370,7 +370,7 @@ class Comment extends React.Component {
parentId={parentId || comment.id}
addNotification={addNotification}
authorId={currentUser.id}
postItem={postItem}
postComment={postComment}
assetId={asset.id}
/>
: null}
@@ -386,7 +386,7 @@ class Comment extends React.Component {
activeReplyBox={activeReplyBox}
addNotification={addNotification}
parentId={comment.id}
postItem={postItem}
postComment={postComment}
editComment={this.props.editComment}
depth={depth + 1}
asset={asset}
@@ -430,7 +430,7 @@ function commentIsStillEditable (comment) {
if ( ! editing) {return false;}
const editableUntil = getEditableUntilDate(comment);
const editWindowExpired = (editableUntil - new Date) < 0;
return ! editWindowExpired;
return ! editWindowExpired;
}
// return number of milliseconds before edit window expires
@@ -26,7 +26,7 @@ class Stream extends React.Component {
render() {
const {
root: {asset, asset: {comments}, comment, myIgnoredUsers},
postItem,
postComment,
addNotification,
postFlag,
postDontAgree,
@@ -84,7 +84,7 @@ class Stream extends React.Component {
{user
? <CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
postComment={this.props.postComment}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
setCommentCountCache={this.props.setCommentCountCache}
@@ -122,7 +122,7 @@ class Stream extends React.Component {
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={this.props.postItem}
postComment={this.props.postComment}
asset={asset}
currentUser={user}
highlighted={comment.id}
@@ -161,7 +161,7 @@ class Stream extends React.Component {
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={postItem}
postComment={postComment}
asset={asset}
currentUser={user}
postFlag={postFlag}
@@ -198,7 +198,7 @@ class Stream extends React.Component {
Stream.propTypes = {
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
// dispatch action to add a tag to a comment
addCommentTag: PropTypes.func,
@@ -1,6 +1,6 @@
import {gql} from 'react-apollo';
import Comment from '../components/Comment';
import withFragments from 'coral-framework/hocs/withFragments';
import {withFragments} from 'coral-framework/hocs';
import {getSlotsFragments} from 'coral-framework/helpers/plugins';
const pluginFragments = getSlotsFragments([
@@ -1,21 +1,20 @@
import React from 'react';
import {compose, gql, graphql} from 'react-apollo';
import {compose, gql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import isEqual from 'lodash/isEqual';
import branch from 'recompose/branch';
import renderComponent from 'recompose/renderComponent';
import update from 'immutability-helper';
import {Spinner} from 'coral-ui';
import {authActions, assetActions, pym} from 'coral-framework';
import Stream from './Stream';
import {getDefinitionName} from 'coral-framework/utils';
import {withQuery} from 'coral-framework/hocs';
import Embed from '../components/Embed';
import Stream from './Stream';
import {setActiveTab} from '../actions/embed';
import {setCommentCountCache, viewAllComments} from '../actions/stream';
import {getDefinitionName, separateDataAndRoot} from 'coral-framework/utils';
const {logout, checkLogin} = authActions;
const {fetchAssetSuccess} = assetActions;
@@ -74,7 +73,7 @@ const EMBED_QUERY = gql`
${Stream.fragments.root}
`;
export const withQuery = graphql(EMBED_QUERY, {
export const withEmbedQuery = withQuery(EMBED_QUERY, {
options: ({auth, commentId, assetId, assetUrl}) => ({
variables: {
assetId,
@@ -83,11 +82,7 @@ export const withQuery = graphql(EMBED_QUERY, {
hasComment: commentId !== '',
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
},
reducer: (previousResult, action, variables) => {
return reduceEditCommentActionsToUpdateStreamQuery(previousResult, action, variables);
},
}),
props: ({data}) => separateDataAndRoot(data)
});
const mapStateToProps = state => ({
@@ -116,88 +111,6 @@ const mapDispatchToProps = dispatch =>
export default compose(
connect(mapStateToProps, mapDispatchToProps),
branch(props => !props.auth.checkedInitialLogin && props.config, renderComponent(Spinner)),
withQuery
withEmbedQuery,
)(EmbedContainer);
/**
* Reduce editComment mutation actions
* producing a new queryStream result where asset.comments reflects the edit
*/
function reduceEditCommentActionsToUpdateStreamQuery(previousResult, action) {
if ( ! (action.type === 'APOLLO_MUTATION_RESULT' && action.operationName === 'editComment')) {
return previousResult;
}
const resultHasErrors = (result) => {
try {
return result.data.editComment.errors.length > 0;
} catch (error) {
// expected if no errors;
return false;
}
};
if (resultHasErrors(action.result)) {
return previousResult;
}
const {variables: {id, edit}, result: {data: {editComment: {comment: {status}}}}} = action;
const updateCommentWithEdit = (comment, edit) => {
const {body} = edit;
const editedComment = update(comment, {
$merge: {
body
},
editing: {$merge:{edited:true}}
});
return editedComment;
};
const commentIsStillVisible = (comment) => {
return ! ((id === comment.id) && (['PREMOD', 'REJECTED'].includes(status)));
};
const resultReflectingEdit = update(previousResult, {
asset: {
comments: {
$apply: comments => {
return comments.filter(commentIsStillVisible).map(comment => {
let replyWasEditedToBeHidden = false;
if (comment.id === id) {
return updateCommentWithEdit(comment, edit);
}
const commentWithUpdatedReplies = update(comment, {
replies: {
$apply: (comments) => {
return comments
.filter(c => {
if (commentIsStillVisible(c)) {
return true;
}
replyWasEditedToBeHidden = true;
return false;
})
.map(comment => {
if (comment.id === id) {
return updateCommentWithEdit(comment, edit);
}
return comment;
});
}
},
});
// If a reply was edited to be hdiden, then this parent needs its replyCount to be decremented.
if (replyWasEditedToBeHidden) {
return update(commentWithUpdatedReplies, {
replyCount: {
$apply: (replyCount) => {
return replyCount - 1;
}
}
});
}
return commentWithUpdatedReplies;
});
}
}
}
});
return resultReflectingEdit;
}
@@ -6,13 +6,17 @@ import uniqBy from 'lodash/uniqBy';
import sortBy from 'lodash/sortBy';
import isNil from 'lodash/isNil';
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from '../constants/stream';
import {postComment, postFlag, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser, editComment} from 'coral-framework/graphql/mutations';
import {
withPostComment, withPostFlag, withPostDontAgree, withDeleteAction,
withAddCommentTag, withRemoveCommentTag, withIgnoreUser, withEditComment,
} from 'coral-framework/graphql/mutations';
import {notificationActions, authActions} from 'coral-framework';
import {editName} from 'coral-framework/actions/user';
import {setCommentCountCache, setActiveReplyBox} from '../actions/stream';
import Stream from '../components/Stream';
import Comment from './Comment';
import withFragments from 'coral-framework/hocs/withFragments';
import {withFragments} from 'coral-framework/hocs';
import {getDefinitionName} from 'coral-framework/utils';
const {showSignInDialog} = authActions;
@@ -236,13 +240,13 @@ const mapDispatchToProps = dispatch =>
export default compose(
withFragments(fragments),
connect(mapStateToProps, mapDispatchToProps),
postComment,
postFlag,
postDontAgree,
addCommentTag,
removeCommentTag,
ignoreUser,
deleteAction,
editComment,
withPostComment,
withPostFlag,
withPostDontAgree,
withAddCommentTag,
withRemoveCommentTag,
withIgnoreUser,
withDeleteAction,
withEditComment,
)(StreamContainer);
@@ -0,0 +1,270 @@
import {gql} from 'react-apollo';
import {add} from 'coral-framework/services/graphqlRegistry';
import update from 'immutability-helper';
const extension = {
fragments: {
EditCommentResponse: gql`
fragment CoralEmbedStream_EditCommentResponse on EditCommentResponse {
comment {
status
}
errors {
translation_key
}
}
`,
StopIgnoringUserResponse: gql`
fragment CoralEmbedStream_StopIgnoringUserResponse on StopIgnoringUserResponse {
errors {
translation_key
}
}
`,
IgnoreUserResponse: gql`
fragment CoralEmbedStream_IgnoreUserResponse on IgnoreUserResponse {
errors {
translation_key
}
}
`,
RemoveCommentTagResponse: gql`
fragment CoralEmbedStream_RemoveCommentTagResponse on RemoveCommentTagResponse {
comment {
id
tags {
name
}
}
errors {
translation_key
}
}
`,
AddCommentTagResponse: gql`
fragment CoralEmbedStream_AddCommentTagResponse on AddCommentTagResponse {
comment {
id
tags {
name
}
}
errors {
translation_key
}
}
`,
DeleteActionResponse: gql`
fragment CoralEmbedStream_DeleteActionResponse on DeleteActionResponse {
errors {
translation_key
}
}
`,
CreateFlagResponse: gql`
fragment CoralEmbedStream_CreateFlagResponse on CreateFlagResponse {
flag {
id
}
errors {
translation_key
}
}
`,
CreateDontAgreeResponse : gql`
fragment CoralEmbedStream_CreateDontAgreeResponse on CreateDontAgreeResponse {
dontagree {
id
}
errors {
translation_key
}
}
`,
CreateCommentResponse: gql`
fragment CoralEmbedStream_CreateCommentResponse on CreateCommentResponse {
comment {
...CoralEmbedStream_CreateCommentResponse_Comment
replies {
...CoralEmbedStream_CreateCommentResponse_Comment
}
}
errors {
translation_key
}
}
fragment CoralEmbedStream_CreateCommentResponse_Comment on Comment {
id
body
created_at
status
replyCount
tags {
name
}
user {
id
name: username
}
action_summaries {
count
current_user {
id
created_at
}
}
editing {
edited
editableUntil
}
}
`,
},
mutations: {
IgnoreUser: () => ({
// TODO: don't rely on refetching.
refetchQueries: [
'EmbedQuery', 'myIgnoredUsers',
],
}),
StopIgnoringUser: () => ({
// TODO: don't rely on refetching.
refetchQueries: [
'EmbedQuery', 'myIgnoredUsers',
],
}),
PostComment: ({
variables: {comment: {asset_id, body, parent_id, tags = []}},
state: {auth},
}) => ({
optimisticResponse: {
createComment: {
comment: {
user: {
id: auth.toJS().user.id,
name: auth.toJS().user.username
},
created_at: new Date().toISOString(),
body,
parent_id,
asset_id,
action_summaries: [],
tags,
status: null,
id: 'pending'
}
}
},
updateQueries: {
EmbedQuery: (previousData, {mutationResult: {data: {createComment: {comment}}}}) => {
if (previousData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
return previousData;
}
let updatedAsset;
// If posting a reply
if (parent_id) {
updatedAsset = {
...previousData,
asset: {
...previousData.asset,
comments: previousData.asset.comments.map((oldComment) => {
return oldComment.id === parent_id
? {...oldComment, replies: [...oldComment.replies, comment], replyCount: oldComment.replyCount + 1}
: oldComment;
})
}
};
} else {
// If posting a top-level comment
updatedAsset = {
...previousData,
asset: {
...previousData.asset,
commentCount: previousData.asset.commentCount + 1,
comments: [comment, ...previousData.asset.comments]
}
};
}
return updatedAsset;
}
}
}),
EditComment: ({
variables: {id, edit},
}) => ({
updateQueries: {
EmbedQuery: (previousData, {mutationResult: {data: {editComment: {comment: {status}}}}}) => {
const updateCommentWithEdit = (comment, edit) => {
const {body} = edit;
const editedComment = update(comment, {
$merge: {
body
},
editing: {$merge:{edited:true}}
});
return editedComment;
};
const commentIsStillVisible = (comment) => {
return ! ((id === comment.id) && (['PREMOD', 'REJECTED'].includes(status)));
};
const resultReflectingEdit = update(previousData, {
asset: {
comments: {
$apply: comments => {
return comments.filter(commentIsStillVisible).map(comment => {
let replyWasEditedToBeHidden = false;
if (comment.id === id) {
return updateCommentWithEdit(comment, edit);
}
const commentWithUpdatedReplies = update(comment, {
replies: {
$apply: (comments) => {
return comments
.filter(c => {
if (commentIsStillVisible(c)) {
return true;
}
replyWasEditedToBeHidden = true;
return false;
})
.map(comment => {
if (comment.id === id) {
return updateCommentWithEdit(comment, edit);
}
return comment;
});
}
},
});
// If a reply was edited to be hdiden, then this parent needs its replyCount to be decremented.
if (replyWasEditedToBeHidden) {
return update(commentWithUpdatedReplies, {
replyCount: {
$apply: (replyCount) => {
return replyCount - 1;
}
}
});
}
return commentWithUpdatedReplies;
});
}
}
}
});
return resultReflectingEdit;
},
},
}),
},
};
add(extension);
+1
View File
@@ -4,6 +4,7 @@ import {ApolloProvider} from 'react-apollo';
import {client} from 'coral-framework/services/client';
import {checkLogin} from 'coral-framework/actions/auth';
import './graphql';
import {addExternalConfig} from 'coral-embed-stream/src/actions/config';
import reducers from './reducers';
@@ -0,0 +1,2 @@
// fragments defined here are automatically registered.
export default {};
@@ -1,8 +0,0 @@
fragment actionSummaryView on ActionSummary {
__typename
count
current_user {
id
created_at
}
}
@@ -1,22 +0,0 @@
#import "../fragments/actionSummaryView.graphql"
fragment commentView on Comment {
id
body
created_at
status
tags {
name
}
user {
id
name: username
}
action_summaries {
...actionSummaryView
}
editing {
edited
editableUntil
}
}
+171
View File
@@ -0,0 +1,171 @@
import {gql} from 'react-apollo';
import withMutation from '../hocs/withMutation';
export const withPostComment = withMutation(
gql`
mutation PostComment($comment: CreateCommentInput!) {
createComment(comment: $comment) {
...CreateCommentResponse
}
}
`, {
props: ({mutate}) => ({
postComment: comment => {
return mutate({
variables: {
comment
},
});
}
}),
});
export const withEditComment = withMutation(
gql`
mutation EditComment($id: ID!, $asset_id: ID!, $edit: EditCommentInput) {
editComment(id:$id, asset_id:$asset_id, edit:$edit) {
...EditCommentResponse
}
}
`, {
props: ({mutate}) => ({
editComment: (id, asset_id, edit) => {
return mutate({
variables: {
id,
asset_id,
edit,
},
});
}
}),
});
export const withPostFlag = withMutation(
gql`
mutation PostFlag($flag: CreateFlagInput!) {
createFlag(flag: $flag) {
...CreateFlagResponse
}
}
`, {
props: ({mutate}) => ({
postFlag: (flag) => {
return mutate({
variables: {
flag
}
});
}}),
});
export const withPostDontAgree = withMutation(
gql`
mutation CreateDontAgree($dontagree: CreateDontAgreeInput!) {
createDontAgree(dontagree: $dontagree) {
...CreateDontAgreeResponse
}
}
`, {
props: ({mutate}) => ({
postDontAgree: (dontagree) => {
return mutate({
variables: {
dontagree
}
});
}}),
});
export const withDeleteAction = withMutation(
gql`
mutation DeleteAction($id: ID!) {
deleteAction(id:$id) {
...DeleteActionResponse
}
}
`, {
props: ({mutate}) => ({
deleteAction: (id) => {
return mutate({
variables: {
id
}
});
}}),
});
export const withAddCommentTag = withMutation(
gql`
mutation AddCommentTag($id: ID!, $tag: String!) {
addCommentTag(id:$id, tag:$tag) {
...AddCommentTagResponse
}
}
`, {
props: ({mutate}) => ({
addCommentTag: ({id, tag}) => {
return mutate({
variables: {
id,
tag
}
});
}}),
});
export const withRemoveCommentTag = withMutation(
gql`
mutation RemoveCommentTag($id: ID!, $tag: String!) {
removeCommentTag(id:$id, tag:$tag) {
...RemoveCommentTagResponse
}
}
`, {
props: ({mutate}) => ({
removeCommentTag: ({id, tag}) => {
return mutate({
variables: {
id,
tag
}
});
}}),
});
export const withIgnoreUser = withMutation(
gql`
mutation IgnoreUser($id: ID!) {
ignoreUser(id:$id) {
...IgnoreUserResponse
}
}
`, {
props: ({mutate}) => ({
ignoreUser: ({id}) => {
return mutate({
variables: {
id,
},
});
}}),
});
export const withStopIgnoringUser = withMutation(
gql`
mutation StopIgnoringUser($id: ID!) {
stopIgnoringUser(id:$id) {
...StopIgnoringUserResponse
}
}
`, {
props: ({mutate}) => ({
stopIgnoringUser: ({id}) => {
return mutate({
variables: {
id,
},
});
}}),
});
@@ -1,13 +0,0 @@
mutation AddCommentTag ($id: ID!, $tag: String!) {
addCommentTag(id:$id, tag:$tag) {
comment {
id
tags {
name
}
}
errors {
translation_key
}
}
}
@@ -1,7 +0,0 @@
mutation deleteAction ($id: ID!) {
deleteAction(id:$id) {
errors {
translation_key
}
}
}
@@ -1,10 +0,0 @@
mutation editComment ($id: ID!, $asset_id: ID!, $edit: EditCommentInput) {
editComment(id:$id, asset_id:$asset_id, edit:$edit) {
comment {
status
}
errors {
translation_key
}
}
}
@@ -1,7 +0,0 @@
mutation ignoreUser ($id: ID!) {
ignoreUser(id:$id) {
errors {
translation_key
}
}
}
@@ -1,191 +0,0 @@
import {graphql} from 'react-apollo';
import POST_COMMENT from './postComment.graphql';
import POST_FLAG from './postFlag.graphql';
import POST_DONT_AGREE from './postDontAgree.graphql';
import DELETE_ACTION from './deleteAction.graphql';
import ADD_COMMENT_TAG from './addCommentTag.graphql';
import REMOVE_COMMENT_TAG from './removeCommentTag.graphql';
import IGNORE_USER from './ignoreUser.graphql';
import STOP_IGNORING_USER from './stopIgnoringUser.graphql';
import EDIT_COMMENT from './editComment.graphql';
import commentView from '../fragments/commentView.graphql';
export const postComment = graphql(POST_COMMENT, {
options: () => ({
fragments: commentView
}),
props: ({ownProps, mutate}) => ({
postItem: comment => {
const {asset_id, body, parent_id, tags = []} = comment;
return mutate({
variables: {
comment
},
optimisticResponse: {
createComment: {
comment: {
user: {
id: ownProps.auth.user.id,
name: ownProps.auth.user.username
},
created_at: new Date().toISOString(),
body,
parent_id,
asset_id,
action_summaries: [],
tags,
status: null,
id: 'pending'
}
}
},
updateQueries: {
EmbedQuery: (oldData, {mutationResult: {data: {createComment: {comment}}}}) => {
if (oldData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
return oldData;
}
let updatedAsset;
// If posting a reply
if (parent_id) {
updatedAsset = {
...oldData,
asset: {
...oldData.asset,
comments: oldData.asset.comments.map((oldComment) => {
return oldComment.id === parent_id
? {...oldComment, replies: [...oldComment.replies, comment], replyCount: oldComment.replyCount + 1}
: oldComment;
})
}
};
} else {
// If posting a top-level comment
updatedAsset = {
...oldData,
asset: {
...oldData.asset,
commentCount: oldData.asset.commentCount + 1,
comments: [comment, ...oldData.asset.comments]
}
};
}
return updatedAsset;
}
}
});
}
}),
});
export const postFlag = graphql(POST_FLAG, {
props: ({mutate}) => ({
postFlag: (flag) => {
return mutate({
variables: {
flag
}
});
}}),
});
export const postDontAgree = graphql(POST_DONT_AGREE, {
props: ({mutate}) => ({
postDontAgree: (dontagree) => {
return mutate({
variables: {
dontagree
}
});
}}),
});
export const deleteAction = graphql(DELETE_ACTION, {
props: ({mutate}) => ({
deleteAction: (id) => {
return mutate({
variables: {
id
}
});
}}),
});
export const addCommentTag = graphql(ADD_COMMENT_TAG, {
props: ({mutate}) => ({
addCommentTag: ({id, tag}) => {
return mutate({
variables: {
id,
tag
}
});
}}),
});
export const removeCommentTag = graphql(REMOVE_COMMENT_TAG, {
props: ({mutate}) => ({
removeCommentTag: ({id, tag}) => {
return mutate({
variables: {
id,
tag
}
});
}}),
});
// TODO: don't rely on refetching.
export const ignoreUser = graphql(IGNORE_USER, {
props: ({mutate}) => ({
ignoreUser: ({id}) => {
return mutate({
variables: {
id,
},
refetchQueries: [
'EmbedQuery', 'myIgnoredUsers',
]
});
}}),
});
// TODO: don't rely on refetching.
export const stopIgnoringUser = graphql(STOP_IGNORING_USER, {
props: ({mutate}) => {
return {
stopIgnoringUser: ({id}) => {
return mutate({
variables: {
id,
},
refetchQueries: [
'EmbedQuery', 'myIgnoredUsers',
]
});
}
};
}
});
export const editComment = graphql(EDIT_COMMENT, {
props: ({mutate}) => {
return {
editComment: (id, asset_id, edit) => {
return mutate({
variables: {
id,
asset_id,
edit,
},
refetchQueries: []
});
}
};
}
});
@@ -1,16 +0,0 @@
#import "../fragments/commentView.graphql"
mutation CreateComment ($comment: CreateCommentInput!) {
createComment(comment: $comment) {
comment {
...commentView
replyCount
replies {
...commentView
}
}
errors {
translation_key
}
}
}
@@ -1,10 +0,0 @@
mutation CreateDontAgree($dontagree: CreateDontAgreeInput!) {
createDontAgree(dontagree:$dontagree) {
dontagree {
id
}
errors {
translation_key
}
}
}
@@ -1,10 +0,0 @@
mutation CreateFlag($flag: CreateFlagInput!) {
createFlag(flag:$flag) {
flag {
id
}
errors {
translation_key
}
}
}
@@ -1,13 +0,0 @@
mutation RemoveCommentTag ($id: ID!, $tag: String!) {
removeCommentTag(id:$id, tag:$tag) {
comment {
id
tags {
name
}
}
errors {
translation_key
}
}
}
@@ -1,7 +0,0 @@
mutation stopIgnoringUser ($id: ID!) {
stopIgnoringUser(id:$id) {
errors {
translation_key
}
}
}
@@ -1,13 +0,0 @@
import {graphql} from 'react-apollo';
import MY_COMMENT_HISTORY from './myCommentHistory.graphql';
import MY_IGNORED_USERS from './myIgnoredUsers.graphql';
export const myCommentHistory = graphql(MY_COMMENT_HISTORY, {});
export const myIgnoredUsers = graphql(MY_IGNORED_USERS, {
props: ({data}) => {
return ({
myIgnoredUsersData: data
});
}
});
@@ -1,14 +0,0 @@
query myCommentHistory {
me {
comments {
id
body
asset {
id
title
url
}
created_at
}
}
}
@@ -1,6 +0,0 @@
query myIgnoredUsers {
myIgnoredUsers {
id,
username,
}
}
+21 -6
View File
@@ -3,9 +3,9 @@ import merge from 'lodash/merge';
import flatten from 'lodash/flatten';
import flattenDeep from 'lodash/flattenDeep';
import uniq from 'lodash/uniq';
import pick from 'lodash/pick';
import plugins from 'pluginsConfig';
import {gql} from 'react-apollo';
import {getDefinitionName} from 'coral-framework/utils';
import {getDefinitionName, mergeDocuments} from 'coral-framework/utils';
export const pluginReducers = merge(
...plugins
@@ -25,19 +25,28 @@ export function getSlotElements(slot, props = {}) {
}
function getComponentFragments(components) {
return components
const res = components
.map(c => c.fragments)
.filter(fragments => fragments)
.reduce((res, fragments) => {
Object.keys(fragments).forEach(key => {
if (!(key in res)) {
res[key] = {spreads: '', definitions: ''};
res[key] = {spreads: [], definitions: []};
}
res[key].spreads += `...${getDefinitionName(fragments[key])}\n`;
res[key].definitions = gql`${res[key].definitions}${fragments[key]}`;
res[key].spreads.push(getDefinitionName(fragments[key]));
res[key].definitions.push(fragments[key]);
});
return res;
}, {});
Object.keys(res).forEach(key => {
// Assemble arguments for `gql` to call it directly without using template literals.
res[key].spreads = `...${res[key].spreads.join('\n...')}\n`;
res[key].definitions = mergeDocuments(res[key].definitions);
});
return res;
}
/**
@@ -73,3 +82,9 @@ export function getSlotsFragments(slots) {
};
}
export function getGraphQLExtensions() {
return plugins
.map(o => pick(o.module, ['mutations', 'queries', 'fragments']))
.filter(o => o);
}
+4 -6
View File
@@ -1,7 +1,5 @@
import withFragments from './withFragments';
import withReaction from './withReaction';
export {default as withFragments} from './withFragments';
export {default as withMutation} from './withMutation';
export {default as withQuery} from './withQuery';
export {default as withReaction} from './withReaction';
export default {
withFragments,
withReaction
};
@@ -0,0 +1,96 @@
import * as React from 'react';
import {graphql} from 'react-apollo';
import merge from 'lodash/merge';
import uniq from 'lodash/uniq';
import flatten from 'lodash/flatten';
import isEmpty from 'lodash/isEmpty';
import {getMutationOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry';
import {store} from 'coral-framework/services/store';
import {getDefinitionName} from '../utils';
/**
* Exports a HOC with the same signature as `graphql`, that will
* apply mutation options registered in the graphRegistry.
*/
export default (document, config) => WrappedComponent => {
config = {
...config,
options: config.options || {},
props: config.props || (data => ({mutate: data.mutate()})),
};
const wrappedProps = (data) => {
const name = getDefinitionName(document);
const callbacks = getMutationOptions(name);
const mutate = (base) => {
const variables = base.variables || config.options.variables;
const configs = callbacks.map(cb => cb({variables, state: store.getState()}));
const optimisticResponse = merge(
base.optimisticResponse || config.options.optimisticResponse,
...configs.map(cfg => cfg.optimisticResponse),
);
const refetchQueries = flatten(uniq([
base.refetchQueries || config.options.refetchQueries,
...configs.map(cfg => cfg.refetchQueries),
].filter(i => i)));
const updateCallbacks =
[base.update || config.options.update]
.concat(...configs.map(cfg => cfg.update))
.filter(i => i);
const update = (proxy, result) => {
updateCallbacks.forEach(cb => cb(proxy, result));
};
const updateQueries =
[
base.updateQueries || config.options.updateQueries,
...configs.map(cfg => cfg.updateQueries)
]
.filter(i => i)
.reduce((res, map) => {
Object.keys(map).forEach(key => {
if (!(key in res)) {
res[key] = map[key];
} else {
const existing = res[key];
res[key] = (prev, result) => {
const next = existing(prev, result);
return map[key](next, result);
};
}
});
return res;
}, {});
const wrappedConfig = {
variables,
optimisticResponse,
refetchQueries,
updateQueries,
update,
};
if (isEmpty(wrappedConfig.optimisticResponse)) {
delete wrappedConfig.optimisticResponse;
}
return data.mutate(wrappedConfig);
};
return config.props({...data, mutate});
};
// Lazily resolve fragments from graphRegistry to support circular dependencies.
let memoized = null;
const getWrapped = () => {
if (!memoized) {
memoized = graphql(resolveFragments(document), {...config, props: wrappedProps})(WrappedComponent);
}
return memoized;
};
return (props) => {
const Wrapped = getWrapped();
return <Wrapped {...props} />;
};
};
+49
View File
@@ -0,0 +1,49 @@
import * as React from 'react';
import {graphql} from 'react-apollo';
import {getQueryOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry';
import {getDefinitionName, separateDataAndRoot} from '../utils';
/**
* Exports a HOC with the same signature as `graphql`, that will
* apply query options registered in the graphRegistry.
*/
export default (document, config) => WrappedComponent => {
config = {
...config,
options: config.options || {},
props: config.props || (({data}) => separateDataAndRoot(data)),
};
const wrappedOptions = (data) => {
const base = (typeof config.options === 'function') ? config.options(data) : config.options;
const name = getDefinitionName(document);
const configs = getQueryOptions(name);
const reducerCallbacks =
[base.reducer || (i => i)]
.concat(...configs.map(cfg => cfg.reducer))
.filter(i => i);
const reducer = reducerCallbacks.reduce(
(a, b) => (prev, ...rest) =>
b(a(prev, ...rest), ...rest),
);
return {
...base,
reducer,
};
};
let memoized = null;
const getWrapped = () => {
if (!memoized) {
memoized = graphql(resolveFragments(document), {...config, options: wrappedOptions})(WrappedComponent);
}
return memoized;
};
return (props) => {
const Wrapped = getWrapped();
return <Wrapped {...props} />;
};
};
-2
View File
@@ -1,7 +1,6 @@
import pym from './services/PymConnection';
import I18n from './modules/i18n/i18n';
import actions from './actions';
import hocs from './hocs';
// TODO (bc): Deprecate old actions. Spreading actions is now needed.
@@ -9,6 +8,5 @@ export default {
pym,
I18n,
actions,
...hocs,
...actions
};
@@ -0,0 +1,198 @@
import {getDefinitionName, mergeDocuments} from 'coral-framework/utils';
import {getGraphQLExtensions} from 'coral-framework/helpers/plugins';
import globalFragments from 'coral-framework/graphql/fragments';
import uniq from 'lodash/uniq';
const fragments = {};
const mutationOptions = {};
const queryOptions = {};
const getTypeName = (ast) => ast.definitions[0].typeCondition.name.value;
/**
* Add fragment
*
* Example:
* addFragment('MyFragment', gql`
* fragment Plugin_MyFragment on Comment {
* body
* }
* `);
*/
export function addFragment(key, document) {
const type = getTypeName(document);
const name = getDefinitionName(document);
if (!(key in fragments)) {
fragments[key] = {type, names: [name], documents: [document]};
} else {
if (type !== fragments[key].type) {
console.error(`Type mismatch ${type} !== ${fragments[key].type}`);
}
fragments[key].names.push(name);
fragments[key].documents.push(document);
}
}
/**
* Add mutation options.
*
* Example:
* // state is the current redux state, which is sometimes
* // necessary to fill the optimistic response.
* addMutationOptions('PostComment', ({variables, state}) => ({
* optimisticResponse: {
* CreateComment: {
* extra: '',
* },
* },
* refetchQueries: [],
* updateQueries: {
* EmbedQuery: (previous, data) => {
* return previous;
* },
* },
* update: (proxy, result) => {
* },
* })
*/
export function addMutationOptions(key, config) {
if (!(key in mutationOptions)) {
mutationOptions[key] = [config];
} else {
mutationOptions[key].push(config);
}
}
/**
* Add query options.
*
* Example:
* addQueryOptions('EmbedQuery', {
* reducer: (previousResult, action, variables) => previousResult,
* });
*/
export function addQueryOptions(key, config) {
if (!(key in queryOptions)) {
queryOptions[key] = [config];
} else {
queryOptions[key].push(config);
}
}
/**
* Add all fragments, mutation options, and query options defined in the object.
*
* Example:
* add({
* fragments: {
* CreateCommentResponse: gql`
* fragment CoralRandomEmoji_CreateCommentResponse on CreateCommentResponse {
* [...]
* }`,
* },
* mutations: {
* // state is the current redux state, which is sometimes
* // necessary to fill the optimistic response.
* PostComment: ({variables, state}) => ({
* optimisticResponse: {
* [...]
* },
* refetchQueries: [],
* updateQueries: {
* EmbedQuery: (previous, data) => {
* return previous;
* },
* },
* update: (proxy, result) => {
* },
* })
* },
* queries: {
* EmbedQuery: {
* reducer: (previousResult, action, variables) => {
* return previousResult;
* },
* },
* },
* });
*/
export function add(extension) {
Object.keys(extension.fragments || []).forEach(key => addFragment(key, extension.fragments[key]));
Object.keys(extension.mutations || []).forEach(key => addMutationOptions(key, extension.mutations[key]));
Object.keys(extension.queries || []).forEach(key => addQueryOptions(key, extension.queries[key]));
}
/**
* Get a list of mutation options.
*/
export function getMutationOptions(key) {
init();
return mutationOptions[key] || [];
}
/**
* Get a list of query options.
*/
export function getQueryOptions(key) {
init();
return queryOptions[key] || [];
}
/**
* Get a document with a fragment named `key`, which contains
* all fragments added under this key.
*/
export function getFragmentDocument(key) {
init();
if (!(key in fragments)) {
return '';
}
let documents = fragments[key] ? fragments[key].documents : [];
let fields = fragments[key] ? `...${fragments[key].names.join('\n...')}\n` : ' __typename';
// Assemble arguments for `gql` to call it directly without using template literals.
const main = `
fragment ${key} on ${fragments[key].type} {
${fields}
}
`;
return mergeDocuments([main, ...documents]);
}
// The fragments and configs are lazily loaded to allow circular dependencies to work.
// TODO: We might want to change this to an explicit add after we have lazy Queries and Mutations.
let initialized = false;
function init() {
if (initialized) { return; }
initialized = true;
// Add fragments from framework.
[globalFragments].forEach(map =>
Object.keys(map).forEach(key => addFragment(key, map[key]))
);
// Add configs from plugins.
getGraphQLExtensions().forEach(ext => add(ext));
}
export function resolveFragments(document) {
if (document.loc.source) {
// resolve fragments from registry
const matchedSubFragments = document.loc.source.body.match(/\.\.\.(.*)/g) || [];
const subFragments =
uniq(matchedSubFragments.map(f => f.replace('...', '')))
.map(key => getFragmentDocument(key))
.filter(i => i);
if (subFragments.length > 0) {
return mergeDocuments([document, ...subFragments]);
}
} else {
console.warn('Can only resolve fragments from documents definied using the gql tag.');
}
return document;
}
+10
View File
@@ -1,3 +1,5 @@
import {gql} from 'react-apollo';
export const getTotalActionCount = (type, comment) => {
return comment.action_summaries
.filter(s => s.__typename === type)
@@ -61,3 +63,11 @@ export function separateDataAndRoot(
root,
};
}
export function mergeDocuments(documents) {
const main = typeof documents[0] === 'string' ? documents[0] : documents[0].loc.source.body;
const substitutions = documents.slice(1);
const literals = [main, ...substitutions.map(() => '\n')];
return gql.apply(null, [literals, ...substitutions]);
}
+3 -3
View File
@@ -43,7 +43,7 @@ class CommentBox extends React.Component {
postComment = ({body}) => {
const {
commentPostedHandler,
postItem,
postComment,
setCommentCountCache,
commentCountCache,
isReply,
@@ -64,7 +64,7 @@ class CommentBox extends React.Component {
// Execute preSubmit Hooks
this.state.hooks.preSubmit.forEach(hook => hook());
postItem(comment, 'comments')
postComment(comment, 'comments')
.then(({data}) => {
const postedComment = data.createComment.comment;
@@ -181,7 +181,7 @@ CommentBox.propTypes = {
charCountEnable: PropTypes.bool.isRequired,
maxCharCount: PropTypes.number,
commentPostedHandler: PropTypes.func,
postItem: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
cancelButtonClicked: PropTypes.func,
assetId: PropTypes.string.isRequired,
parentId: PropTypes.string,
+3 -3
View File
@@ -12,7 +12,7 @@ class ReplyBox extends Component {
render() {
const {
styles,
postItem,
postComment,
assetId,
authorId,
addNotification,
@@ -32,7 +32,7 @@ class ReplyBox extends Component {
addNotification={addNotification}
authorId={authorId}
assetId={assetId}
postItem={postItem}
postComment={postComment}
isReply={true} />
</div>;
}
@@ -46,7 +46,7 @@ ReplyBox.propTypes = {
parentId: PropTypes.string,
addNotification: PropTypes.func.isRequired,
authorId: PropTypes.string.isRequired,
postItem: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
assetId: PropTypes.string.isRequired
};
@@ -1,11 +1,10 @@
import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import {compose, graphql, gql} from 'react-apollo';
import React, {Component} from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import {bindActionCreators} from 'redux';
import {myCommentHistory, myIgnoredUsers} from 'coral-framework/graphql/queries';
import {stopIgnoringUser} from 'coral-framework/graphql/mutations';
import {withStopIgnoringUser} from 'coral-framework/graphql/mutations';
import {link} from 'coral-framework/services/PymConnection';
import NotLoggedIn from '../components/NotLoggedIn';
@@ -88,6 +87,38 @@ class ProfileContainer extends Component {
}
}
// TODO: These currently relies on refetching (see ignoreUser and stopIgnoringUser mutations).
//
const withMyIgnoredUsersQuery = graphql(gql`
query myIgnoredUsers {
myIgnoredUsers {
id,
username,
}
}`, {
props: ({data}) => {
return ({
myIgnoredUsersData: data
});
}
});
const withMyCommentHistoryQuery = graphql(gql`
query myCommentHistory {
me {
comments {
id
body
asset {
id
title
url
}
created_at
}
}
}`);
const mapStateToProps = state => ({
user: state.user.toJS(),
asset: state.asset.toJS(),
@@ -99,7 +130,7 @@ const mapDispatchToProps = dispatch =>
export default compose(
connect(mapStateToProps, mapDispatchToProps),
myCommentHistory,
myIgnoredUsers,
stopIgnoringUser,
withMyCommentHistoryQuery,
withMyIgnoredUsersQuery,
withStopIgnoringUser,
)(ProfileContainer);
@@ -1,15 +1,15 @@
import {compose, gql, graphql} from 'react-apollo';
import {compose, gql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import get from 'lodash/get';
import withFragments from 'coral-framework/hocs/withFragments';
import {withFragments, withMutation} from 'coral-framework/hocs';
import {showSignInDialog} from 'coral-framework/actions/auth';
import RespectButton from '../components/RespectButton';
const isRespectAction = (a) => a.__typename === 'RespectActionSummary';
const COMMENT_FRAGMENT = gql`
fragment RespectButton_updateFragment on Comment {
fragment CoralRespect_UpdateFragment on Comment {
action_summaries {
... on RespectActionSummary {
count
@@ -21,8 +21,8 @@ const COMMENT_FRAGMENT = gql`
}
`;
const withDeleteAction = graphql(gql`
mutation deleteAction($id: ID!) {
const withDeleteAction = withMutation(gql`
mutation CoralRespect_DeleteAction($id: ID!) {
deleteAction(id:$id) {
errors {
translation_key
@@ -66,8 +66,8 @@ const withDeleteAction = graphql(gql`
}),
});
const withPostRespect = graphql(gql`
mutation createRespect($respect: CreateRespectInput!) {
const withPostRespect = withMutation(gql`
mutation CoralRespect_CreateRespect($respect: CreateRespectInput!) {
createRespect(respect: $respect) {
respect {
id
@@ -137,14 +137,14 @@ const mapDispatchToProps = dispatch =>
const enhance = compose(
withFragments({
root: gql`
fragment RespectButton_root on RootQuery {
fragment CoralRespect_RespectButton_root on RootQuery {
me {
status
}
}
`,
comment: gql`
fragment RespectButton_comment on Comment {
fragment CoralRespect_RespectButton_comment on Comment {
action_summaries {
... on RespectActionSummary {
count