Merge branch 'master' into i18n-refactor

This commit is contained in:
gaba
2017-05-15 14:02:34 -07:00
251 changed files with 4984 additions and 1874 deletions
+8 -8
View File
@@ -6,14 +6,14 @@ import I18n from 'coral-i18n/modules/i18n/i18n';
const lang = new I18n();
export const fetchAssetRequest = () => ({type: actions.FETCH_ASSET_REQUEST});
export const fetchAssetSuccess = asset => ({type: actions.FETCH_ASSET_SUCCESS, asset});
export const fetchAssetFailure = error => ({type: actions.FETCH_ASSET_FAILURE, error});
export const fetchAssetSuccess = (asset) => ({type: actions.FETCH_ASSET_SUCCESS, asset});
export const fetchAssetFailure = (error) => ({type: actions.FETCH_ASSET_FAILURE, error});
const updateAssetSettingsRequest = () => ({type: actions.UPDATE_ASSET_SETTINGS_REQUEST});
const updateAssetSettingsSuccess = settings => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings});
const updateAssetSettingsSuccess = (settings) => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings});
const updateAssetSettingsFailure = () => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE});
export const updateConfiguration = newConfig => (dispatch, getState) => {
export const updateConfiguration = (newConfig) => (dispatch, getState) => {
const assetId = getState().asset.toJS().id;
dispatch(updateAssetSettingsRequest());
coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig})
@@ -21,10 +21,10 @@ export const updateConfiguration = newConfig => (dispatch, getState) => {
dispatch(addNotification('success', lang.t('framework.success_update_settings')));
dispatch(updateAssetSettingsSuccess(newConfig));
})
.catch(error => dispatch(updateAssetSettingsFailure(error)));
.catch((error) => dispatch(updateAssetSettingsFailure(error)));
};
export const updateOpenStream = closedBody => (dispatch, getState) => {
export const updateOpenStream = (closedBody) => (dispatch, getState) => {
const assetId = getState().asset.toJS().id;
dispatch(fetchAssetRequest());
coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody})
@@ -32,13 +32,13 @@ export const updateOpenStream = closedBody => (dispatch, getState) => {
dispatch(addNotification('success', lang.t('framework.success_update_settings')));
dispatch(fetchAssetSuccess(closedBody));
})
.catch(error => dispatch(fetchAssetFailure(error)));
.catch((error) => dispatch(fetchAssetFailure(error)));
};
const openStream = () => ({type: actions.OPEN_COMMENTS});
const closeStream = () => ({type: actions.CLOSE_COMMENTS});
export const updateOpenStatus = status => dispatch => {
export const updateOpenStatus = (status) => (dispatch) => {
if (status === 'open') {
dispatch(openStream());
dispatch(updateOpenStream({closedAt: null}));
+168 -103
View File
@@ -1,37 +1,15 @@
import {gql} from 'react-apollo';
import client from 'coral-framework/services/client';
import I18n from '../../coral-i18n/modules/i18n/i18n';
const lang = new I18n();
import {pym} from 'coral-framework';
import * as Storage from '../helpers/storage';
import * as actions from '../constants/auth';
import coralApi, {base} from '../helpers/response';
import {pym} from 'coral-framework';
import jwtDecode from 'jwt-decode';
const ME_QUERY = gql`
query Me {
me {
status
comments {
id
body
asset {
id
title
url
}
created_at
}
}
}
`;
function fetchMe() {
return client.query({
fetchPolicy: 'network-only',
query: ME_QUERY});
}
const lang = new I18n(translations);
import translations from './../translations';
import I18n from '../../coral-framework/modules/i18n/i18n';
// Dialog Actions
export const showSignInDialog = () => dispatch => {
export const showSignInDialog = () => (dispatch) => {
const signInPopUp = window.open(
'/embed/stream/login',
'Login',
@@ -49,27 +27,41 @@ export const showSignInDialog = () => dispatch => {
signInPopUp.onunload = () => {
if (loaded) {
dispatch(checkLogin());
fetchMe();
}
};
dispatch({type: actions.SHOW_SIGNIN_DIALOG});
};
export const hideSignInDialog = () => dispatch => {
export const hideSignInDialog = () => (dispatch) => {
dispatch({type: actions.HIDE_SIGNIN_DIALOG});
window.close();
};
export const createUsernameRequest = () => ({type: actions.CREATE_USERNAME_REQUEST});
export const showCreateUsernameDialog = () => ({type: actions.SHOW_CREATEUSERNAME_DIALOG});
export const hideCreateUsernameDialog = () => ({type: actions.HIDE_CREATEUSERNAME_DIALOG});
export const createUsernameRequest = () => ({
type: actions.CREATE_USERNAME_REQUEST
});
export const showCreateUsernameDialog = () => ({
type: actions.SHOW_CREATEUSERNAME_DIALOG
});
export const hideCreateUsernameDialog = () => ({
type: actions.HIDE_CREATEUSERNAME_DIALOG
});
const createUsernameSuccess = () => ({type: actions.CREATE_USERNAME_SUCCESS});
const createUsernameFailure = error => ({type: actions.CREATE_USERNAME_FAILURE, error});
const createUsernameSuccess = () => ({
type: actions.CREATE_USERNAME_SUCCESS
});
export const updateUsername = ({username}) => ({type: actions.UPDATE_USERNAME, username});
const createUsernameFailure = (error) => ({
type: actions.CREATE_USERNAME_FAILURE,
error
});
export const createUsername = (userId, formData) => dispatch => {
export const updateUsername = ({username}) => ({
type: actions.UPDATE_USERNAME,
username
});
export const createUsername = (userId, formData) => (dispatch) => {
dispatch(createUsernameRequest());
coralApi('/account/username', {method: 'PUT', body: formData})
.then(() => {
@@ -77,18 +69,18 @@ export const createUsername = (userId, formData) => dispatch => {
dispatch(hideCreateUsernameDialog());
dispatch(updateUsername(formData));
})
.catch(error => {
.catch((error) => {
dispatch(createUsernameFailure(lang.t(`error.${error.translation_key}`)));
});
};
export const changeView = view => dispatch => {
export const changeView = (view) => (dispatch) => {
dispatch({
type: actions.CHANGE_VIEW,
view
});
switch(view) {
switch (view) {
case 'SIGNUP':
window.resizeTo(500, 800);
break;
@@ -100,26 +92,49 @@ export const changeView = view => dispatch => {
}
};
export const cleanState = () => ({type: actions.CLEAN_STATE});
export const cleanState = () => ({
type: actions.CLEAN_STATE
});
// Sign In Actions
const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST});
const signInRequest = () => ({
type: actions.FETCH_SIGNIN_REQUEST
});
// TODO: revisit login redux flow.
// const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin});
//
const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
const signInFailure = (error) => ({
type: actions.FETCH_SIGNIN_FAILURE,
error
});
//==============================================================================
// AUTH TOKEN
//==============================================================================
export const handleAuthToken = (token) => (dispatch) => {
Storage.setItem('exp', jwtDecode(token).exp);
Storage.setItem('token', token);
dispatch({type: 'HANDLE_AUTH_TOKEN'});
};
//==============================================================================
// SIGN IN
//==============================================================================
export const fetchSignIn = (formData) => (dispatch) => {
dispatch(signInRequest());
return coralApi('/auth/local', {method: 'POST', body: formData})
.then(() => dispatch(hideSignInDialog()))
.catch(error => {
.then(({token}) => {
dispatch(handleAuthToken(token));
dispatch(hideSignInDialog());
})
.catch((error) => {
if (error.metadata) {
// the user might not have a valid email. prompt the user user re-request the confirmation email
dispatch(signInFailure(lang.t('error.email_not_verified', error.metadata)));
dispatch(
signInFailure(lang.t('error.email_not_verified', error.metadata))
);
} else {
// invalid credentials
@@ -128,13 +143,23 @@ export const fetchSignIn = (formData) => (dispatch) => {
});
};
// Sign In - Facebook
//==============================================================================
// SIGN IN - FACEBOOK
//==============================================================================
const signInFacebookRequest = () => ({type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST});
const signInFacebookSuccess = user => ({type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS, user});
const signInFacebookFailure = error => ({type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE, error});
const signInFacebookRequest = () => ({
type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST
});
const signInFacebookSuccess = (user) => ({
type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS,
user
});
const signInFacebookFailure = (error) => ({
type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE,
error
});
export const fetchSignInFacebook = () => dispatch => {
export const fetchSignInFacebook = () => (dispatch) => {
dispatch(signInFacebookRequest());
window.open(
`${base}/auth/facebook`,
@@ -143,11 +168,15 @@ export const fetchSignInFacebook = () => dispatch => {
);
};
// Sign Up Facebook
//==============================================================================
// SIGN UP - FACEBOOK
//==============================================================================
const signUpFacebookRequest = () => ({type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST});
const signUpFacebookRequest = () => ({
type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST
});
export const fetchSignUpFacebook = () => dispatch => {
export const fetchSignUpFacebook = () => (dispatch) => {
dispatch(signUpFacebookRequest());
window.open(
`${base}/auth/facebook`,
@@ -156,7 +185,7 @@ export const fetchSignUpFacebook = () => dispatch => {
);
};
export const facebookCallback = (err, data) => dispatch => {
export const facebookCallback = (err, data) => (dispatch) => {
if (err) {
dispatch(signInFacebookFailure(err));
return;
@@ -173,20 +202,26 @@ export const facebookCallback = (err, data) => dispatch => {
}
};
// Sign Up Actions
//==============================================================================
// SIGN UP
//==============================================================================
const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST});
const signUpSuccess = user => ({type: actions.FETCH_SIGNUP_SUCCESS, user});
const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error});
const signUpSuccess = (user) => ({type: actions.FETCH_SIGNUP_SUCCESS, user});
const signUpFailure = (error) => ({type: actions.FETCH_SIGNUP_FAILURE, error});
export const fetchSignUp = (formData, redirectUri) => (dispatch) => {
dispatch(signUpRequest());
coralApi('/users', {method: 'POST', body: formData, headers: {'X-Pym-Url': redirectUri}})
coralApi('/users', {
method: 'POST',
body: formData,
headers: {'X-Pym-Url': redirectUri}
})
.then(({user}) => {
dispatch(signUpSuccess(user));
})
.catch(error => {
.catch((error) => {
let errorMessage = lang.t(`error.${error.message}`);
// if there is no translation defined, just show the error string
@@ -197,75 +232,105 @@ export const fetchSignUp = (formData, redirectUri) => (dispatch) => {
});
};
// Forgot Password Actions
//==============================================================================
// FORGOT PASSWORD
//==============================================================================
const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUEST});
const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS});
const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE});
const forgotPasswordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
});
export const fetchForgotPassword = email => (dispatch) => {
dispatch(forgotPassowordRequest(email));
const forgotPasswordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
});
const forgotPasswordFailure = () => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE
});
export const fetchForgotPassword = (email) => (dispatch) => {
dispatch(forgotPasswordRequest(email));
const redirectUri = pym.parentUrl || location.href;
coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
.then(() => dispatch(forgotPassowordSuccess()))
.catch(error => dispatch(forgotPassowordFailure(error)));
coralApi('/account/password/reset', {
method: 'POST',
body: {email, loc: redirectUri}
})
.then(() => dispatch(forgotPasswordSuccess()))
.catch((error) => dispatch(forgotPasswordFailure(error)));
};
// LogOut Actions
//==============================================================================
// LOGOUT
//==============================================================================
const logOutRequest = () => ({type: actions.LOGOUT_REQUEST});
const logOutSuccess = () => ({type: actions.LOGOUT_SUCCESS});
const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
export const logout = () => dispatch => {
dispatch(logOutRequest());
return coralApi('/auth', {method: 'DELETE'})
.then(() => {
dispatch(logOutSuccess());
fetchMe();
})
.catch(error => dispatch(logOutFailure(error)));
export const logout = () => (dispatch) => {
return coralApi('/auth', {method: 'DELETE'}).then(() => {
dispatch({type: actions.LOGOUT});
Storage.removeItem('token');
});
};
// LogOut Actions
export const validForm = () => ({type: actions.VALID_FORM});
export const invalidForm = error => ({type: actions.INVALID_FORM, error});
// Check Login
//==============================================================================
// CHECK LOGIN
//==============================================================================
const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST});
const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin});
const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
const checkLoginFailure = (error) => ({type: actions.CHECK_LOGIN_FAILURE, error});
export const checkLogin = () => dispatch => {
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin
});
export const checkLogin = () => (dispatch) => {
dispatch(checkLoginRequest());
coralApi('/auth')
.then((result) => {
if (!result.user) {
Storage.removeItem('token');
throw new Error('Not logged in');
}
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
const isAdmin = !!result.user.roles.filter((i) => i === 'ADMIN').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
})
.catch(error => {
.catch((error) => {
console.error(error);
dispatch(checkLoginFailure(`${error.translation_key}`));
});
};
const verifyEmailRequest = () => ({type: actions.VERIFY_EMAIL_REQUEST});
const verifyEmailSuccess = () => ({type: actions.VERIFY_EMAIL_SUCCESS});
const verifyEmailFailure = () => ({type: actions.VERIFY_EMAIL_FAILURE});
export const validForm = () => ({type: actions.VALID_FORM});
export const invalidForm = (error) => ({type: actions.INVALID_FORM, error});
export const requestConfirmEmail = (email, redirectUri) => dispatch => {
//==============================================================================
// VERIFY EMAIL
//==============================================================================
const verifyEmailRequest = () => ({
type: actions.VERIFY_EMAIL_REQUEST
});
const verifyEmailSuccess = () => ({
type: actions.VERIFY_EMAIL_SUCCESS
});
const verifyEmailFailure = () => ({
type: actions.VERIFY_EMAIL_FAILURE
});
export const requestConfirmEmail = (email, redirectUri) => (dispatch) => {
dispatch(verifyEmailRequest());
return coralApi('/users/resend-verify', {method: 'POST', body: {email}, headers: {'X-Pym-Url': redirectUri}})
return coralApi('/users/resend-verify', {
method: 'POST',
body: {email},
headers: {'X-Pym-Url': redirectUri}
})
.then(() => {
dispatch(verifyEmailSuccess());
})
.catch(err => {
.catch((err) => {
// email might have already been verifyed
dispatch(verifyEmailFailure(err));
+2 -2
View File
@@ -5,7 +5,7 @@ import * as actions from '../constants/auth';
import I18n from 'coral-i18n/modules/i18n/i18n';
const lang = new I18n();
const editUsernameFailure = error => ({type: actions.EDIT_USERNAME_FAILURE, error});
const editUsernameFailure = (error) => ({type: actions.EDIT_USERNAME_FAILURE, error});
const editUsernameSuccess = () => ({type: actions.EDIT_USERNAME_SUCCESS});
export const editName = (username) => (dispatch) => {
@@ -14,7 +14,7 @@ export const editName = (username) => (dispatch) => {
dispatch(editUsernameSuccess());
dispatch(addNotification('success', lang.t('framework.success_name_update')));
})
.catch(error => {
.catch((error) => {
dispatch(editUsernameFailure(lang.t(`error.${error.translation_key}`)));
});
};
@@ -1,3 +1,7 @@
.inline {
display: inline-block;
}
.debug {
background-color: coral;
}
+9 -3
View File
@@ -1,12 +1,13 @@
import React from 'react';
import cn from 'classnames';
import styles from './Slot.css';
import {connect} from 'react-redux';
import {getSlotElements} from 'coral-framework/helpers/plugins';
export default function Slot ({fill, inline = false, ...rest}) {
function Slot ({fill, inline = false, plugin_config: config, ...rest}) {
return (
<div className={cn({[styles.inline]: inline})}>
{getSlotElements(fill, rest)}
<div className={cn({[styles.inline]: inline, [styles.debug]: config.debug})}>
{getSlotElements(fill, {...rest, config})}
</div>
);
}
@@ -14,3 +15,8 @@ export default function Slot ({fill, inline = false, ...rest}) {
Slot.propTypes = {
fill: React.PropTypes.string
};
const mapStateToProps = ({config: {plugin_config = {}}}) => ({plugin_config});
export default connect(mapStateToProps, null)(Slot);
@@ -8,4 +8,3 @@ export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
+1 -5
View File
@@ -33,9 +33,7 @@ export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE';
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
export const LOGOUT = 'LOGOUT';
export const INVALID_FORM = 'INVALID_FORM';
export const VALID_FORM = 'VALID_FORM';
@@ -44,8 +42,6 @@ export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN';
export const VERIFY_EMAIL_REQUEST = 'VERIFY_EMAIL_REQUEST';
export const VERIFY_EMAIL_SUCCESS = 'VERIFY_EMAIL_SUCCESS';
export const VERIFY_EMAIL_FAILURE = 'VERIFY_EMAIL_FAILURE';
@@ -1,9 +0,0 @@
export const UPDATE_CONFIG_REQUEST = 'UPDATE_CONFIG_REQUEST';
export const UPDATE_CONFIG_SUCCESS = 'UPDATE_CONFIG_SUCCESS';
export const UPDATE_CONFIG_FAILURE = 'UPDATE_CONFIG_FAILURE';
export const UPDATE_CONFIG = 'UPDATE_CONFIG';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
export const ADD_ITEM = 'ADD_ITEM';
@@ -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,18 +0,0 @@
#import "../fragments/actionSummaryView.graphql"
fragment commentView on Comment {
id
body
created_at
status
tags {
name
}
user {
id
name: username
}
action_summaries {
...actionSummaryView
}
}
+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,7 +0,0 @@
mutation ignoreUser ($id: ID!) {
ignoreUser(id:$id) {
errors {
translation_key
}
}
}
@@ -1,173 +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 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',
]
});
}
};
}
});
@@ -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,
}
}
+3
View File
@@ -0,0 +1,3 @@
export function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
+32 -17
View File
@@ -3,14 +3,14 @@ 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
.filter(o => o.module.reducer)
.map(o => ({...o.module.reducer}))
.filter((o) => o.module.reducer)
.map((o) => ({...o.module.reducer}))
);
/**
@@ -18,26 +18,35 @@ export const pluginReducers = merge(
*/
export function getSlotElements(slot, props = {}) {
const components = flatten(plugins
.filter(o => o.module.slots[slot])
.map(o => o.module.slots[slot]));
.filter((o) => o.module.slots[slot])
.map((o) => o.module.slots[slot]));
return components
.map((component, i) => React.createElement(component, {...props, key: i}));
.map((component, i) => React.createElement(component, {key: i, ...props}));
}
function getComponentFragments(components) {
return components
.map(c => c.fragments)
.filter(fragments => fragments)
const res = components
.map((c) => c.fragments)
.filter((fragments) => fragments)
.reduce((res, fragments) => {
Object.keys(fragments).forEach(key => {
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;
}
/**
@@ -56,10 +65,10 @@ export function getSlotsFragments(slots) {
if (!Array.isArray(slots)) {
slots = [slots];
}
const components = uniq(flattenDeep(slots.map(slot => {
const components = uniq(flattenDeep(slots.map((slot) => {
return plugins
.filter(o => o.module.slots[slot])
.map(o => o.module.slots[slot]);
.filter((o) => o.module.slots[slot])
.map((o) => o.module.slots[slot]);
})));
const fragments = getComponentFragments(components);
@@ -73,3 +82,9 @@ export function getSlotsFragments(slots) {
};
}
export function getGraphQLExtensions() {
return plugins
.map((o) => pick(o.module, ['mutations', 'queries', 'fragments']))
.filter((o) => o);
}
+14 -21
View File
@@ -1,31 +1,22 @@
export const base = '/api/v1';
import * as Storage from './storage';
const buildOptions = (inputOptions = {}) => {
const csurfDOM = document.head.querySelector('[property=csrf]');
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
Accept: 'application/json',
Authorization: `Bearer ${Storage.getItem('token')}`,
'Content-Type': 'application/json'
},
credentials: 'same-origin',
_csrf: csurfDOM ? csurfDOM.content : false
credentials: 'same-origin'
};
let options = Object.assign({}, defaultOptions, inputOptions);
options.headers = Object.assign({}, defaultOptions.headers, inputOptions.headers);
if (options._csrf) {
switch (options.method.toLowerCase()) {
case 'post':
case 'put':
case 'delete':
options.headers['x-csrf-token'] = options._csrf;
break;
}
}
options.headers = Object.assign(
{},
defaultOptions.headers,
inputOptions.headers
);
if (options.method.toLowerCase() !== 'get') {
options.body = JSON.stringify(options.body);
@@ -34,9 +25,9 @@ const buildOptions = (inputOptions = {}) => {
return options;
};
const handleResp = res => {
const handleResp = (res) => {
if (res.status > 399) {
return res.json().then(err => {
return res.json().then((err) => {
let message = err.message || res.status;
const error = new Error();
@@ -58,6 +49,8 @@ const handleResp = res => {
}
};
export const base = '/api/v1';
export default (url, options) => {
return fetch(`${base}${url}`, buildOptions(options)).then(handleResp);
};
+93
View File
@@ -0,0 +1,93 @@
let available, error;
function storageAvailable(type) {
let storage = window[type], x = '__storage_test__';
try {
storage.setItem(x, x);
storage.removeItem(x);
return true;
} catch (e) {
error = e;
return (
e instanceof DOMException &&
// everything except Firefox
(e.code === 22 ||
// Firefox
e.code === 1014 ||
// test name field too, because code might not be present
// everything except Firefox
e.name === 'QuotaExceededError' ||
// Firefox
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
// acknowledge QuotaExceededError only if there's something already stored
storage.length !== 0
);
}
}
function lazyCheckStorage() {
if (typeof available === 'undefined') {
available = storageAvailable('localStorage');
}
}
export function getItem(item = '') {
lazyCheckStorage();
if (available) {
return localStorage.getItem(item);
} else {
console.error(
`Cannot get from localStorage. localStorage is not available. ${error}`
);
}
}
export function setItem(item = '', value) {
lazyCheckStorage();
if (available) {
return localStorage.setItem(item, value);
} else {
console.error(
`Cannot set localStorage. localStorage is not available. ${error}`
);
}
}
export function removeItem(item = '') {
lazyCheckStorage();
if (available) {
return localStorage.removeItem(item);
} else {
console.error(
`Cannot remove item from localStorage. localStorage is not available. ${error}`
);
}
}
export function clear() {
lazyCheckStorage();
if (available) {
return localStorage.clear();
} else {
console.error(
`Cannot clear localStorage. localStorage is not available. ${error}`
);
}
}
// Enable this to debug WEB Storage events
// window.addEventListener('storage', function(e) {
// const msg = `${e.key} " was changed in page ${e.url} from ${e.oldValue} to ${e.newValue}`;
// console.log(msg);
// });
@@ -0,0 +1,4 @@
export function capitalize(str) {
const newString = new String(str);
return newString.charAt(0).toUpperCase() + newString.slice(1);
}
+4 -4
View File
@@ -1,7 +1,7 @@
export default {
email: email => (/^([A-Za-z0-9_\-\.\+])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
password: pass => (/^(?=.{8,}).*$/.test(pass)),
email: (email) => (/^([A-Za-z0-9_\-\.\+])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
password: (pass) => (/^(?=.{8,}).*$/.test(pass)),
confirmPassword: () => true,
username: username => (/^[a-zA-Z0-9_]+$/.test(username)),
organizationName: org => (/^[a-zA-Z0-9_ ]+$/).test(org)
username: (username) => (/^[a-zA-Z0-9_]+$/.test(username)),
organizationName: (org) => (/^[a-zA-Z0-9_ ]+$/).test(org)
};
+5
View File
@@ -0,0 +1,5 @@
export {default as withFragments} from './withFragments';
export {default as withMutation} from './withMutation';
export {default as withQuery} from './withQuery';
export {default as withReaction} from './withReaction';
+2 -6
View File
@@ -1,12 +1,8 @@
import React from 'react';
import {getDisplayName} from '../helpers/hoc';
// TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38.
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
export default fragments => WrappedComponent => {
export default (fragments) => (WrappedComponent) => {
class WithFragments extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
@@ -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} />;
};
};
+243
View File
@@ -0,0 +1,243 @@
import React from 'react';
import get from 'lodash/get';
import uuid from 'uuid/v4';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getDisplayName} from '../helpers/hoc';
import {compose, gql, graphql} from 'react-apollo';
import withFragments from 'coral-framework/hocs/withFragments';
import {showSignInDialog} from 'coral-framework/actions/auth';
import {capitalize} from 'coral-framework/helpers/strings';
import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
export default (reaction) => (WrappedComponent) => {
if (typeof reaction !== 'string') {
console.error('Reaction must be a valid string');
return null;
}
reaction = reaction.toLowerCase();
class WithReactions extends React.Component {
render() {
const {comment} = this.props;
const reactionSummary = getMyActionSummary(
`${capitalize(reaction)}ActionSummary`,
comment
);
const count = getTotalActionCount(
`${capitalize(reaction)}ActionSummary`,
comment
);
const alreadyReacted = () => !!reactionSummary;
const withReactionProps = {reactionSummary, count, alreadyReacted};
return <WrappedComponent {...this.props} {...withReactionProps} />;
}
}
const isReaction = (a) =>
a.__typename === `${capitalize(reaction)}ActionSummary`;
const COMMENT_FRAGMENT = gql`
fragment ${capitalize(reaction)}Button_updateFragment on Comment {
action_summaries {
... on ${capitalize(reaction)}ActionSummary {
count
current_user {
id
}
}
}
}
`;
const withDeleteReaction = graphql(
gql`
mutation deleteReaction($id: ID!) {
deleteAction(id:$id) {
errors {
translation_key
}
}
}
`,
{
props: ({mutate, ownProps}) => ({
deleteReaction: () => {
const reactionSummary = getMyActionSummary(
`${capitalize(reaction)}ActionSummary`,
ownProps.comment
);
const reactionData = {
id: reactionSummary.current_user.id,
commentId: ownProps.comment.id
};
return mutate({
variables: {id: reactionData.id},
optimisticResponse: {
deleteAction: {
__typename: 'DeleteActionResponse',
errors: null
}
},
update: (proxy) => {
const fragmentId = `Comment_${reactionData.commentId}`;
// Read the data from our cache for this query.
const data = proxy.readFragment({
fragment: COMMENT_FRAGMENT,
id: fragmentId
});
// Check whether we liked this comment.
const idx = data.action_summaries.findIndex(isReaction);
if (
idx < 0 ||
get(data.action_summaries[idx], 'current_user.id') !== reactionData.id
) {
return;
}
data.action_summaries[idx] = {
...data.action_summaries[idx],
count: data.action_summaries[idx].count - 1,
current_user: null
};
// Write our data back to the cache.
proxy.writeFragment({
fragment: COMMENT_FRAGMENT,
id: fragmentId,
data
});
}
});
}
})
}
);
const withPostReaction = graphql(
gql`
mutation create${capitalize(reaction)}($${reaction}: Create${capitalize(reaction)}Input!) {
create${capitalize(reaction)}(${reaction}: $${reaction}) {
${reaction} {
id
}
errors {
translation_key
}
}
}
`,
{
props: ({mutate, ownProps}) => ({
postReaction: () => {
const reactionData = {
item_id: ownProps.comment.id,
item_type: 'COMMENTS'
};
return mutate({
variables: {[reaction]: reactionData},
optimisticResponse: {
[`create${capitalize(reaction)}`]: {
__typename: `Create${capitalize(reaction)}Response`,
errors: null,
[reaction]: {
__typename: `${capitalize(reaction)}Action`,
id: uuid()
}
}
},
update: (proxy, mutationResult) => {
const fragmentId = `Comment_${reactionData.item_id}`;
// Read the data from our cache for this query.
const data = proxy.readFragment({
fragment: COMMENT_FRAGMENT,
id: fragmentId
});
// Add our comment from the mutation to the end.
let idx = data.action_summaries.findIndex(isReaction);
// Check whether we already reactioned this comment.
if (idx >= 0 && data.action_summaries[idx].current_user) {
return;
}
if (idx < 0) {
// Add initial action when it doesn't exist.
data.action_summaries.push({
__typename: `${capitalize(reaction)}ActionSummary`,
count: 0,
current_user: null
});
idx = data.action_summaries.length - 1;
}
data.action_summaries[idx] = {
...data.action_summaries[idx],
count: data.action_summaries[idx].count + 1,
current_user: mutationResult.data[
`create${capitalize(reaction)}`
][reaction]
};
// Write our data back to the cache.
proxy.writeFragment({
fragment: COMMENT_FRAGMENT,
id: fragmentId,
data
});
}
});
}
})
}
);
const mapDispatchToProps = (dispatch) =>
bindActionCreators({showSignInDialog}, dispatch);
const enhance = compose(
withFragments({
root: gql`
fragment ${capitalize(reaction)}Button_root on RootQuery {
me {
status
}
}
`,
comment: gql`
fragment ${capitalize(reaction)}Button_comment on Comment {
action_summaries {
... on ${capitalize(reaction)}ActionSummary {
count
current_user {
id
}
}
}
}`
}),
connect(null, mapDispatchToProps),
withDeleteReaction,
withPostReaction
);
WithReactions.displayName = `WithReactions(${getDisplayName(WrappedComponent)})`;
return enhance(WithReactions);
};
@@ -11,7 +11,7 @@ const {stripIndent} = require('common-tags');
function getPluginList(config) {
if (config && config.client) {
return config.client.map(x => typeof x === 'string' ? x : Object.keys(x)[0]);
return config.client.map((x) => typeof x === 'string' ? x : Object.keys(x)[0]);
}
return [];
+2 -5
View File
@@ -20,7 +20,7 @@ const initialState = Map({
fromSignUp: false
});
const purge = user => {
const purge = (user) => {
const {settings, profiles, ...userData} = user; // eslint-disable-line
return fromJS(userData);
};
@@ -64,9 +64,6 @@ export default function auth (state = initialState, action) {
.set('view', action.view);
case actions.CLEAN_STATE:
return initialState;
case actions.CHECK_CSRF_TOKEN:
return state
.set('_csrf', action._csrf);
case actions.FETCH_SIGNIN_REQUEST:
return state
.set('isLoading', true);
@@ -116,7 +113,7 @@ export default function auth (state = initialState, action) {
return state
.set('isLoading', false)
.set('successSignUp', true);
case actions.LOGOUT_SUCCESS:
case actions.LOGOUT:
return state
.set('user', null)
.set('isLoading', false)
+3 -3
View File
@@ -12,7 +12,7 @@ const initialState = Map({
ignoredUsers: Set(),
});
const purge = user => {
const purge = (user) => {
const {_id, created_at, updated_at, __v, roles, ...userData} = user; // eslint-disable-line
return userData;
};
@@ -42,9 +42,9 @@ export default function user (state = initialState, action) {
case 'APOLLO_MUTATION_RESULT':
switch (action.operationName) {
case 'ignoreUser':
return state.updateIn(['ignoredUsers'], i => i.add(action.variables.id));
return state.updateIn(['ignoredUsers'], (i) => i.add(action.variables.id));
case 'stopIgnoringUser':
return state.updateIn(['ignoredUsers'], i => i.delete(action.variables.id));
return state.updateIn(['ignoredUsers'], (i) => i.delete(action.variables.id));
}
break;
}
+1 -3
View File
@@ -1,5 +1,5 @@
import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
import {networkInterface} from './transport';
// import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
@@ -11,8 +11,6 @@ import getNetworkInterface from './transport';
// getNetworkInterface(),
// wsClient,
// );
const networkInterface = getNetworkInterface();
export const client = new ApolloClient({
connectToDevTools: true,
queryTransformer: addTypename,
@@ -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;
}
+1 -1
View File
@@ -3,7 +3,7 @@ import thunk from 'redux-thunk';
import mainReducer from '../reducers';
import {client} from './client';
const apolloErrorReporter = () => next => action => {
const apolloErrorReporter = () => (next) => (action) => {
if (action.type === 'APOLLO_QUERY_ERROR') {
console.error(action.error);
}
+29 -9
View File
@@ -1,11 +1,31 @@
import {createNetworkInterface} from 'apollo-client';
import * as Storage from '../helpers/storage';
export default function getNetworkInterface(apiUrl = '/api/v1/graph/ql', headers = {}) {
return new createNetworkInterface({
uri: apiUrl,
opts: {
credentials: 'same-origin',
headers,
},
});
}
//==============================================================================
// NETWORK INTERFACE
//==============================================================================
const networkInterface = createNetworkInterface({
uri: '/api/v1/graph/ql',
opts: {
credentials: 'same-origin'
}
});
//==============================================================================
// MIDDLEWARES
//==============================================================================
networkInterface.use([{
applyMiddleware(req, next) {
if (!req.options.headers) {
req.options.headers = {}; // Create the header object if needed.
}
req.options.headers['authorization'] = `Bearer ${Storage.getItem('token')}`;
next();
}
}]);
export {
networkInterface
};
+110
View File
@@ -0,0 +1,110 @@
{
"en": {
"MY_COMMENTS": "My Comments",
"profile": "Profile",
"myProfile": "My profile",
"successUpdateSettings": "The changes you have made have been applied to the comment stream on this article",
"successNameUpdate": "Your username has been updated",
"contentNotAvailable": "This content is not available",
"bannedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Report, or write comments. Please contact us if you have any questions.",
"editName": {
"msg": "Your account is currently suspended because your username has been deemed inappropriate. To restore your account, please enter a new username. Please contact us if you have any questions.",
"label": "New Username",
"button": "Submit",
"error": "Usernames can contain letters, numbers and _ only"
},
"viewMoreComments": "view more comments",
"showAllComments": "Show all comments",
"viewReply": "view reply",
"viewAllRepliesInitial": "view all {0} replies",
"viewAllReplies": "view {0} replies",
"newCount": "View {0} new {1}",
"comment": "comment",
"comments": "comments",
"commentIsIgnored": "This comment is hidden because you ignored this user.",
"editComment": {
"bodyInputLabel": "Edit this comment",
"saveButton": "Save changes",
"editWindowExpired": "You can no longer edit this comment. The time window to do so has expired. Why not post another one?",
"editWindowExpiredClose": "Close",
"editWindowTimerPrefix": "Edit Window: ",
"second": "second",
"secondsPlural": "seconds",
"unexpectedError": "Unexpected error while saving changes. Sorry!"
},
"error": {
"COMMENT_TOO_SHORT": "Your comment must have something in it",
"EDIT_WINDOW_ENDED": "You can no longer edit this comment. The time window to do so has expired.",
"emailNotVerified": "Email address {0} not verified.",
"email": "Not a valid E-Mail",
"networkError": "Failed to connect to server. Check your internet connection and try again.",
"password": "Password must be at least 8 characters",
"username": "Usernames can contain letters, numbers and _ only",
"confirmPassword": "Passwords don't match. Please, check again",
"organizationName": "Organization name must only contain letters or numbers.",
"emailPasswordError": "Email and/or password combination incorrect.",
"EMAIL_REQUIRED": "An email address is required",
"PASSWORD_REQUIRED": "Must input a password",
"PASSWORD_LENGTH": "Password is too short",
"EMAIL_IN_USE": "Email address already in use",
"EMAIL_USERNAME_IN_USE": "Email address or username already in use",
"USERNAME_IN_USE": "Username already in use",
"USERNAME_REQUIRED": "Must input a username",
"NO_SPECIAL_CHARACTERS": "Usernames can contain letters, numbers and _ only",
"PROFANITY_ERROR": "Usernames must not contain profanity. Please contact the administrator if you believe this to be in error.",
"NOT_AUTHORIZED": "Not authorized.",
"EDIT_USERNAME_NOT_AUTHORIZED": "You do not have permission to update your username."
}
},
"es": {
"profile": "Pérfil",
"MY_COMMENTS": "Mis Comentarios",
"myProfile": "Mi pérfil",
"successUpdateSettings": "La configuración de este articulo fue actualizada",
"successBioUpdate": "Tu biografia fue actualizada",
"contentNotAvailable": "El contenido no se encuentra disponible",
"bannedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes gustar, marcar o escribir commentarios.",
"editNameMsg": "",
"viewMoreComments": "Ver commentarios más",
"viewReply": "ver respuesta",
"viewAllRepliesInitial": "ver todas las {0} respuestas",
"viewAllReplies": "ver {0} respuestas",
"newCount": "Ver {0} {1} más",
"comment": "commentario",
"comments": "commentarios",
"commentIsIgnored": "Este comentario está escondido porque has ignorado al usuario.",
"showAllComments": "Mostrar todos los comentarios",
"editComment": {
"bodyInputLabel": "Editar este comentario",
"saveButton": "Guardar cambios",
"editWindowExpired": "Ya no puedes editar este comentario. La ventana de tiempo para hacerlo ha caducado. ¿Por qué no publicar otro?",
"editWindowExpiredClose": "Cerca",
"editWindowTimerPrefix": "Ventana de edición: ",
"second": "segundo",
"secondsPlural": "segundos",
"unexpectedError": "Unexpected error while saving changes. Sorry!"
},
"error": {
"editWindowExpired": "Ya no puedes editar este comentario. La ventana de tiempo para hacerlo ha caducado.",
"emailNotVerified": "E-mail {0} no verificado.",
"email": "No es un e-mail válido",
"networkError": "Error al conectar con el servidor. Compruebe su conexión a Internet y vuelva a intentarlo.",
"password": "La contraseña debe tener por lo menos 8 caracteres",
"username": "Los nombres pueden contener letras, números y _",
"organizationName": "El nombre de la organización debe contener letras y/o números.",
"confirmPassword": "Las contraseñas no coinciden",
"emailPasswordError": "E-mail y/o contraseña incorrecta.",
"EMAIL_REQUIRED": "Se requiere un e-mail",
"PASSWORD_REQUIRED": "Debe ingresar una contraseña",
"PASSWORD_LENGTH": "La contraseña es muy corta",
"EMAIL_IN_USE": "El e-mail se encuentra en uso",
"EMAIL_USERNAME_IN_USE": "E-mail o Nombre en uso.",
"USERNAME_IN_USE": "Nombre en uso.",
"USERNAME_REQUIRED": "Debe ingresar un nombre",
"NO_SPECIAL_CHARACTERS": "Los nombres pueden contener letras, números y _",
"PROFANITY_ERROR": "Los nombres no pueden contener blasfemias. Por favor contacte al o la administradora si cree que esto es un error",
"NOT_AUTHORIZED": "Acción no autorizada.",
"EDIT_USERNAME_NOT_AUTHORIZED": "No tiene permiso para editar el nombre de usuario."
}
}
}
+16 -6
View File
@@ -1,6 +1,8 @@
import {gql} from 'react-apollo';
export const getTotalActionCount = (type, comment) => {
return comment.action_summaries
.filter(s => s.__typename === type)
.filter((s) => s.__typename === type)
.reduce((total, summary) => {
return total + summary.count;
}, 0);
@@ -10,14 +12,14 @@ export const iPerformedThisAction = (type, comment) => {
// if there is a current_user on any of the ActionSummary(s), the user performed this action
return comment.action_summaries
.filter(a => a.__typename === type)
.some(a => a.current_user);
.filter((a) => a.__typename === type)
.some((a) => a.current_user);
};
export const getMyActionSummary = (type, comment) => {
return comment.action_summaries
.filter(a => a.__typename === type)
.find(a => a.current_user);
.filter((a) => a.__typename === type)
.find((a) => a.current_user);
};
/**
@@ -27,7 +29,7 @@ export const getMyActionSummary = (type, comment) => {
*/
export const getActionSummary = (type, comment) => {
return comment.action_summaries.filter(a => a.__typename === type);
return comment.action_summaries.filter((a) => a.__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]);
}