diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index d226ac744..71d2ed1a3 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -2,7 +2,6 @@ import React from 'react'; import cn from 'classnames'; import PropTypes from 'prop-types'; import capitalize from 'lodash/capitalize'; -import { getErrorMessages } from 'coral-framework/utils'; import styles from './UserDetail.css'; import AccountHistory from './AccountHistory'; import { Slot } from 'coral-framework/components'; @@ -29,43 +28,23 @@ import UserInfoTooltip from './UserInfoTooltip'; class UserDetail extends React.Component { rejectThenReload = async info => { - try { - await this.props.rejectComment(info); - this.props.data.refetch(); - } catch (err) { - console.error(err); - this.props.notify('error', getErrorMessages(err)); - } + await this.props.rejectComment(info); + this.props.data.refetch(); }; acceptThenReload = async info => { - try { - await this.props.acceptComment(info); - this.props.data.refetch(); - } catch (err) { - console.error(err); - this.props.notify('error', getErrorMessages(err)); - } + await this.props.acceptComment(info); + this.props.data.refetch(); }; bulkAcceptThenReload = async () => { - try { - await this.props.bulkAccept(); - this.props.data.refetch(); - } catch (err) { - console.error(err); - this.props.notify('error', getErrorMessages(err)); - } + await this.props.bulkAccept(); + this.props.data.refetch(); }; bulkRejectThenReload = async () => { - try { - await this.props.bulkReject(); - this.props.data.refetch(); - } catch (err) { - console.error(err); - this.props.notify('error', getErrorMessages(err)); - } + await this.props.bulkReject(); + this.props.data.refetch(); }; changeTab = tab => { @@ -94,6 +73,16 @@ class UserDetail extends React.Component { ); } + renderError() { + return ( + + +
{this.props.data.error.message}
+
+
+ ); + } + getActionMenuLabel() { const { root: { user } } = this.props; @@ -345,6 +334,10 @@ class UserDetail extends React.Component { } render() { + if (this.props.data.error) { + return this.renderError(); + } + if (this.props.loading) { return this.renderLoading(); } @@ -371,7 +364,6 @@ UserDetail.propTypes = { selectedCommentIds: PropTypes.array.isRequired, viewUserDetail: PropTypes.any.isRequired, loadMore: PropTypes.any.isRequired, - notify: PropTypes.func.isRequired, showSuspendUserDialog: PropTypes.func, showBanUserDialog: PropTypes.func, unbanUser: PropTypes.func.isRequired, diff --git a/client/coral-admin/src/containers/BanUserDialog.js b/client/coral-admin/src/containers/BanUserDialog.js index d2dc642a7..3617902ff 100644 --- a/client/coral-admin/src/containers/BanUserDialog.js +++ b/client/coral-admin/src/containers/BanUserDialog.js @@ -10,8 +10,6 @@ import { } from 'coral-framework/graphql/mutations'; import { compose } from 'react-apollo'; import t from 'coral-framework/services/i18n'; -import { getErrorMessages } from 'coral-framework/utils'; -import { notify } from 'coral-framework/actions/notification'; class BanUserDialogContainer extends Component { banUser = async () => { @@ -22,16 +20,11 @@ class BanUserDialogContainer extends Component { banUser, setCommentStatus, hideBanUserDialog, - notify, } = this.props; - try { - await banUser({ id: userId, message: '' }); - hideBanUserDialog(); - if (commentId && commentStatus && commentStatus !== 'REJECTED') { - await setCommentStatus({ commentId, status: 'REJECTED' }); - } - } catch (err) { - notify('error', getErrorMessages(err)); + await banUser({ id: userId, message: '' }); + hideBanUserDialog(); + if (commentId && commentStatus && commentStatus !== 'REJECTED') { + await setCommentStatus({ commentId, status: 'REJECTED' }); } }; @@ -78,14 +71,13 @@ const mapDispatchToProps = dispatch => ({ ...bindActionCreators( { hideBanUserDialog, - notify, }, dispatch ), }); export default compose( + connect(mapStateToProps, mapDispatchToProps), withBanUser, - withSetCommentStatus, - connect(mapStateToProps, mapDispatchToProps) + withSetCommentStatus )(BanUserDialogContainer); diff --git a/client/coral-admin/src/containers/SuspendUserDialog.js b/client/coral-admin/src/containers/SuspendUserDialog.js index e058faad6..f03d48f52 100644 --- a/client/coral-admin/src/containers/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/SuspendUserDialog.js @@ -11,7 +11,6 @@ import { import { compose, gql } from 'react-apollo'; import t, { timeago } from 'coral-framework/services/i18n'; import withQuery from 'coral-framework/hocs/withQuery'; -import { getErrorMessages } from 'coral-framework/utils'; import get from 'lodash/get'; import { notify } from 'coral-framework/actions/notification'; @@ -28,17 +27,13 @@ class SuspendUserDialogContainer extends Component { notify, } = this.props; hideSuspendUserDialog(); - try { - await suspendUser({ id: userId, message, until }); - notify( - 'success', - t('suspenduser.notify_suspend_until', username, timeago(until)) - ); - if (commentId && commentStatus && commentStatus !== 'REJECTED') { - await setCommentStatus({ commentId, status: 'REJECTED' }); - } - } catch (err) { - notify('error', getErrorMessages(err)); + await suspendUser({ id: userId, message, until }); + notify( + 'success', + t('suspenduser.notify_suspend_until', username, timeago(until)) + ); + if (commentId && commentStatus && commentStatus !== 'REJECTED') { + await setCommentStatus({ commentId, status: 'REJECTED' }); } }; diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index bd34ac4d8..fb1508a2f 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -24,7 +24,6 @@ import { } from 'coral-framework/graphql/mutations'; import UserDetailComment from './UserDetailComment'; import update from 'immutability-helper'; -import { notify } from 'coral-framework/actions/notification'; import { showBanUserDialog } from 'actions/banUserDialog'; import { showSuspendUserDialog } from 'actions/suspendUserDialog'; @@ -130,6 +129,7 @@ class UserDetailContainer extends React.Component { acceptComment={this.acceptComment} rejectComment={this.rejectComment} loading={loading} + error={this.props.data && this.props.data.error} loadMore={this.loadMore} {...this.props} /> @@ -271,7 +271,6 @@ const mapDispatchToProps = dispatch => ({ viewUserDetail, hideUserDetail, toggleSelectAllCommentInUserDetail, - notify, }, dispatch ), diff --git a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js index 09240b8f3..1d7e089e2 100644 --- a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js +++ b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js @@ -15,7 +15,6 @@ import { handleFlaggedUsernameChange } from '../graphql'; import { notify } from 'coral-framework/actions/notification'; import { isFlaggedUserDangling } from '../utils'; import t from 'coral-framework/services/i18n'; -import { notifyOnMutationError } from 'coral-framework/hocs'; import FlaggedAccounts from '../components/FlaggedAccounts'; import FlaggedUser from '../containers/FlaggedUser'; @@ -295,7 +294,6 @@ const mapDispatchToProps = dispatch => export default compose( connect(null, mapDispatchToProps), withApproveUsername, - notifyOnMutationError(['approveUsername']), withQuery( gql` query TalkAdmin_Community_FlaggedAccounts { diff --git a/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js b/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js index f9ada0945..325eadfb5 100644 --- a/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js +++ b/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js @@ -4,8 +4,6 @@ import { hideRejectUsernameDialog } from '../../../actions/community'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { compose } from 'react-apollo'; -import { notify } from 'coral-framework/actions/notification'; -import { notifyOnMutationError } from 'coral-framework/hocs'; const mapStateToProps = state => ({ user: state.community.user, @@ -16,13 +14,11 @@ const mapDispatchToProps = dispatch => bindActionCreators( { handleClose: hideRejectUsernameDialog, - notify, }, dispatch ); export default compose( connect(mapStateToProps, mapDispatchToProps), - withRejectUsername, - notifyOnMutationError(['rejectUsername']) + withRejectUsername )(RejectUsernameDialog); diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js index 0d9d5faf9..ce33fa1f4 100644 --- a/client/coral-admin/src/routes/Configure/containers/Configure.js +++ b/client/coral-admin/src/routes/Configure/containers/Configure.js @@ -4,10 +4,9 @@ import { bindActionCreators } from 'redux'; import { compose, gql } from 'react-apollo'; import { withQuery, withMergedSettings } from 'coral-framework/hocs'; import { Spinner } from 'coral-ui'; -import { notify } from 'coral-framework/actions/notification'; import PropTypes from 'prop-types'; import { withUpdateSettings } from 'coral-framework/graphql/mutations'; -import { getErrorMessages, getDefinitionName } from 'coral-framework/utils'; +import { getDefinitionName } from 'coral-framework/utils'; import StreamSettings from './StreamSettings'; import TechSettings from './TechSettings'; import ModerationSettings from './ModerationSettings'; @@ -16,22 +15,21 @@ import Configure from '../components/Configure'; class ConfigureContainer extends Component { savePending = async () => { - try { - await this.props.updateSettings(this.props.pending); - this.props.clearPending(); - } catch (err) { - this.props.notify('error', getErrorMessages(err)); - } + await this.props.updateSettings(this.props.pending); + this.props.clearPending(); }; render() { + if (this.props.data.error) { + return
{this.props.data.error.message}
; + } + if (this.props.data.loading) { return ; } return ( ({ variables: {}, + fetchPolicy: 'network-only', }), } ); @@ -81,7 +80,6 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => bindActionCreators( { - notify, clearPending, setActiveSection, }, @@ -89,9 +87,9 @@ const mapDispatchToProps = dispatch => ); export default compose( + connect(mapStateToProps, mapDispatchToProps), withUpdateSettings, withConfigureQuery, - connect(mapStateToProps, mapDispatchToProps), withMergedSettings('root.settings', 'pending', 'mergedSettings') )(ConfigureContainer); @@ -99,7 +97,6 @@ ConfigureContainer.propTypes = { updateSettings: PropTypes.func.isRequired, clearPending: PropTypes.func.isRequired, setActiveSection: PropTypes.func.isRequired, - notify: PropTypes.func.isRequired, auth: PropTypes.object.isRequired, data: PropTypes.object.isRequired, root: PropTypes.object.isRequired, diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index fd96c598b..e0e3f14fe 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -269,10 +269,6 @@ class ModerationContainer extends Component { const { root, root: { asset, settings }, data } = this.props; const assetId = getAssetId(this.props); - if (data.error) { - return
Error
; - } - if (assetId) { if (asset === null) { // Not found. @@ -280,6 +276,10 @@ class ModerationContainer extends Component { } } + if (data.error) { + return
{data.error.message}
; + } + if (data.loading && data.networkStatus !== 3) { // loading. return ; diff --git a/client/coral-embed-stream/src/tabs/configure/containers/Configure.js b/client/coral-embed-stream/src/tabs/configure/containers/Configure.js index c408d67fc..b6104b067 100644 --- a/client/coral-embed-stream/src/tabs/configure/containers/Configure.js +++ b/client/coral-embed-stream/src/tabs/configure/containers/Configure.js @@ -9,6 +9,10 @@ import { getDefinitionName } from 'coral-framework/utils'; class ConfigureContainer extends React.Component { render() { + if (this.props.data.error) { + return
{this.props.data.error.message}
; + } + return ( { - try { - await this.props.updateAssetSettings( - this.props.asset.id, - this.props.pending - ); - this.props.clearPending(); - } catch (err) { - this.props.notify('error', getErrorMessages(err)); - } + await this.props.updateAssetSettings( + this.props.asset.id, + this.props.pending + ); + this.props.clearPending(); }; render() { @@ -98,7 +90,6 @@ SettingsContainer.propTypes = { mergedSettings: PropTypes.object.isRequired, updateAssetSettings: PropTypes.func.isRequired, clearPending: PropTypes.func.isRequired, - notify: PropTypes.func.isRequired, updatePending: PropTypes.func.isRequired, canSave: PropTypes.bool.isRequired, }; @@ -135,7 +126,6 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => bindActionCreators( { - notify, clearPending, updatePending, }, @@ -143,9 +133,9 @@ const mapDispatchToProps = dispatch => ); const enhance = compose( + connect(mapStateToProps, mapDispatchToProps), withSettingsFragments, withUpdateAssetSettings, - connect(mapStateToProps, mapDispatchToProps), withMergedSettings('asset.settings', 'pending', 'mergedSettings') ); diff --git a/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js index ed1fe73f9..3190ab155 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js +++ b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import LoadMore from './LoadMore'; import NewCount from './NewCount'; import { TransitionGroup } from 'react-transition-group'; -import { forEachError } from 'coral-framework/utils'; import Comment from '../containers/Comment'; import NoComments from './NoComments'; @@ -91,11 +90,8 @@ class AllCommentsPane extends React.Component { .then(() => { this.setState({ loadingState: 'success' }); }) - .catch(error => { + .catch(() => { this.setState({ loadingState: 'error' }); - forEachError(error, ({ msg }) => { - this.props.notify('error', msg); - }); }); }; diff --git a/client/coral-embed-stream/src/tabs/stream/components/Comment.js b/client/coral-embed-stream/src/tabs/stream/components/Comment.js index f4e8404e4..cd0fab9e9 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Comment.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.js @@ -24,7 +24,6 @@ import { EditableCommentContent } from './EditableCommentContent'; import { getActionSummary, iPerformedThisAction, - forEachError, isCommentActive, getShallowChanges, } from 'coral-framework/utils'; @@ -261,11 +260,8 @@ export default class Comment extends React.Component { loadingState: 'success', }); }) - .catch(error => { + .catch(() => { this.setState({ loadingState: 'error' }); - forEachError(error, ({ msg }) => { - this.props.notify('error', msg); - }); }); emit('ui.Comment.showMoreReplies', { id }); return; diff --git a/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js b/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js index bfd2c8bad..f4cfc42e8 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js +++ b/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js @@ -6,7 +6,6 @@ import styles from './Comment.css'; import { CountdownSeconds } from './CountdownSeconds'; import { getEditableUntilDate } from './util'; import { can } from 'coral-framework/services/perms'; -import { forEachError } from 'coral-framework/utils'; import { Icon } from 'coral-ui'; import t from 'coral-framework/services/i18n'; @@ -80,7 +79,7 @@ export class EditableCommentContent extends React.Component { this.setState({ loadingState: 'loading' }); - const { editComment, notify, stopEditing } = this.props; + const { editComment, stopEditing } = this.props; if (typeof editComment !== 'function') { return; } @@ -95,7 +94,6 @@ export class EditableCommentContent extends React.Component { } } catch (error) { this.setState({ loadingState: 'error' }); - forEachError(error, ({ msg }) => notify('error', msg)); } }; diff --git a/client/coral-embed-stream/src/tabs/stream/components/Stream.js b/client/coral-embed-stream/src/tabs/stream/components/Stream.js index d82f49498..6288a2b6b 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Stream.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { StreamError } from './StreamError'; +import StreamError from './StreamError'; import Comment from '../containers/Comment'; import BannedAccount from '../../../components/BannedAccount'; import ChangeUsername from '../containers/ChangeUsername'; diff --git a/client/coral-embed-stream/src/tabs/stream/components/StreamError.js b/client/coral-embed-stream/src/tabs/stream/components/StreamError.js index 0955de2ef..79d6b03fc 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/StreamError.js +++ b/client/coral-embed-stream/src/tabs/stream/components/StreamError.js @@ -1,6 +1,6 @@ import React from 'react'; import styles from './StreamError.css'; -export const StreamError = ({ children }) => ( +export default ({ children }) => (
{children}
); diff --git a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js index 70cbadae5..51b4d9d9e 100644 --- a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js @@ -38,6 +38,7 @@ import { insertFetchedCommentsIntoEmbedQuery, nest, } from '../../../graphql/utils'; +import StreamError from '../components/StreamError'; const { showSignInDialog, editName } = authActions; const { notify } = notificationActions; @@ -208,6 +209,10 @@ class StreamContainer extends React.Component { } render() { + if (this.props.data.error) { + return {this.props.data.error.message}; + } + if ( !this.props.asset || (this.props.asset.comment === undefined && !this.props.asset.comments) @@ -424,7 +429,8 @@ export default compose( withEmit, connect(mapStateToProps, mapDispatchToProps), withPostComment, - withPostFlag, + // `talk-plugin-flags` has a custom error handling logic. + withPostFlag({ notifyOnError: false }), withPostDontAgree, withDeleteAction, withEditComment diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js index 275f6df42..174f25a63 100644 --- a/client/coral-framework/hocs/index.js +++ b/client/coral-framework/hocs/index.js @@ -6,4 +6,3 @@ export { default as withEmit } from './withEmit'; export { default as excludeIf } from './excludeIf'; export { default as connect } from './connect'; export { default as withMergedSettings } from './withMergedSettings'; -export { default as notifyOnMutationError } from './notifyOnMutationError'; diff --git a/client/coral-framework/hocs/notifyOnMutationError.js b/client/coral-framework/hocs/notifyOnMutationError.js deleted file mode 100644 index c9429ad15..000000000 --- a/client/coral-framework/hocs/notifyOnMutationError.js +++ /dev/null @@ -1,35 +0,0 @@ -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { compose } from 'react-apollo'; -import { notify } from 'coral-framework/actions/notification'; -import { forEachError } from 'coral-framework/utils'; -import { withProps } from 'recompose'; - -const notifyOnMutationError = keys => - compose( - connect(null, dispatch => - bindActionCreators( - { - notify, - }, - dispatch - ) - ), - withProps(ownProps => - keys.reduce((props, key) => { - props[key] = async (...args) => { - try { - return await ownProps[key](...args); - } catch (e) { - forEachError(e, ({ msg }) => { - ownProps.notify('error', msg); - }); - throw e; - } - }; - return props; - }, {}) - ) - ); - -export default notifyOnMutationError; diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js index b72e56f40..dae38071f 100644 --- a/client/coral-framework/hocs/withMutation.js +++ b/client/coral-framework/hocs/withMutation.js @@ -4,11 +4,16 @@ import merge from 'lodash/merge'; import uniq from 'lodash/uniq'; import flatten from 'lodash/flatten'; import isEmpty from 'lodash/isEmpty'; -import { getDefinitionName, getResponseErrors } from '../utils'; +import { + getDefinitionName, + getResponseErrors, + getErrorMessages, +} from '../utils'; import PropTypes from 'prop-types'; import t from 'coral-framework/services/i18n'; import hoistStatics from 'recompose/hoistStatics'; import union from 'lodash/union'; +import { notify } from 'coral-framework/actions/notification'; class ResponseErrors extends Error { constructor(errors) { @@ -27,11 +32,7 @@ class ResponseError { } } -/** - * Exports a HOC with the same signature as `graphql`, that will - * apply mutation options registered in the graphRegistry. - */ -export default (document, config = {}) => +const createHOC = (document, config, { notifyOnError = true }) => hoistStatics(WrappedComponent => { config = { ...config, @@ -46,10 +47,18 @@ export default (document, config = {}) => graphql: PropTypes.object, }; + static propTypes = { + notify: PropTypes.func, + }; + get graphqlRegistry() { return this.context.graphql.registry; } + notifyErrors(messages) { + this.context.store.dispatch(notify('error', messages)); + } + resolveDocument(documentOrCallback) { return this.context.graphql.resolveDocument( documentOrCallback, @@ -165,6 +174,11 @@ export default (document, config = {}) => variables, error, }); + + // Show errors as notifications. + if (notifyOnError) { + this.notifyErrors(getErrorMessages(error)); + } throw error; }); }; @@ -213,3 +227,18 @@ export default (document, config = {}) => } }; }); + +/** + * Exports a HOC with the same signature as `graphql`, that will + * apply mutation options registered in the graphRegistry. + * + * The returned HOC accepts a settings object with the following properties: + * notifyOnError: show a notification to the user when an error occured. + * Defaults to true. + */ +export default (document, config = {}) => settingsOrComponent => { + if (typeof settingsOrComponent === 'function') { + return createHOC(document, config, {})(settingsOrComponent); + } + return createHOC(document, config, settingsOrComponent); +}; diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index d5609c340..c4b9074b4 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -9,6 +9,8 @@ import PropTypes from 'prop-types'; import hoistStatics from 'recompose/hoistStatics'; import { getOperationName } from 'apollo-client/queries/getFromAST'; import throttle from 'lodash/throttle'; +import get from 'lodash/get'; +import { notify } from 'coral-framework/actions/notification'; const withSkipOnErrors = reducer => (prev, action, ...rest) => { if ( @@ -36,21 +38,23 @@ function networkStatusToString(networkStatus) { return 'ready'; case 8: return 'error'; + default: + throw new Error(`Unknown network status ${networkStatus}`); } - throw new Error(`Unknown network status ${networkStatus}`); } -/** - * Exports a HOC with the same signature as `graphql`, that will - * apply query options registered in the graphRegistry. - */ -export default (document, config = {}) => +const createHOC = (document, config, { notifyOnError = true }) => hoistStatics(WrappedComponent => { return class WithQuery extends React.Component { static contextTypes = { eventEmitter: PropTypes.object, graphql: PropTypes.object, client: PropTypes.object, + store: PropTypes.object, + }; + + static propTypes = { + notify: PropTypes.func, }; // Lazily resolve fragments from graphRegistry to support circular dependencies. @@ -166,10 +170,24 @@ export default (document, config = {}) => return () => this.client.networkInterface.unsubscribe(id); }; + notifyErrors(messages) { + this.context.store.dispatch(notify('error', messages)); + } + nextData(data) { this.apolloData = data; this.emitWhenNeeded(data); + if ( + get(data, 'error.message') && + get(this, 'data.error.message') !== get(data, 'error.message') + ) { + // Show errors as notifications. + if (notifyOnError) { + this.notifyErrors(data.error.message); + } + } + // If data was previously set, we update it in a immutable way. if (this.data) { if (this.data.loading && !data.loading) { @@ -319,3 +337,18 @@ export default (document, config = {}) => } }; }); + +/** + * Exports a HOC with the same signature as `graphql`, that will + * apply query options registered in the graphRegistry. + * + * The returned HOC accepts a settings object with the following properties: + * notifyOnError: show a notification to the user when an error occured. + * Defaults to true. + */ +export default (document, config = {}) => settingsOrComponent => { + if (typeof settingsOrComponent === 'function') { + return createHOC(document, config, {})(settingsOrComponent); + } + return createHOC(document, config, settingsOrComponent); +}; diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index fb9d8de17..e1bcf7f13 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -65,6 +65,10 @@ class ProfileContainer extends Component { const { me } = this.props.root; const loading = this.props.data.loading; + if (this.props.data.error) { + return
{this.props.data.error.message}
; + } + if (!auth.loggedIn) { return ; } diff --git a/client/talk-plugin-commentbox/CommentBox.js b/client/talk-plugin-commentbox/CommentBox.js index a8da78f3f..8eac1ff2f 100644 --- a/client/talk-plugin-commentbox/CommentBox.js +++ b/client/talk-plugin-commentbox/CommentBox.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import t from 'coral-framework/services/i18n'; import { can } from 'coral-framework/services/perms'; -import { forEachError } from 'coral-framework/utils'; import Slot from 'coral-framework/components/Slot'; import { connect } from 'react-redux'; @@ -93,9 +92,8 @@ class CommentBox extends React.Component { commentPostedHandler(); } }) - .catch(err => { + .catch(() => { this.setState({ loadingState: 'error' }); - forEachError(err, ({ msg }) => notify('error', msg)); }); }; diff --git a/client/talk-plugin-flags/components/FlagButton.js b/client/talk-plugin-flags/components/FlagButton.js index 64841a893..b2048e105 100644 --- a/client/talk-plugin-flags/components/FlagButton.js +++ b/client/talk-plugin-flags/components/FlagButton.js @@ -60,9 +60,10 @@ export default class FlagButton extends Component { }); }; - onPopupContinue = () => { + onPopupContinue = async () => { const { postFlag, postDontAgree, id, author_id } = this.props; const { itemType, reason, step, message } = this.state; + let failed = false; switch (step) { case 0: @@ -75,13 +76,9 @@ export default class FlagButton extends Component { return; } break; - } - - // Proceed to the next step or close the menu if we've reached the end - if (step + 1 >= this.props.getPopupMenu.length) { - this.closeMenu(); - } else { - this.setState({ step: step + 1 }); + case this.props.getPopupMenu.length: + this.closeMenu(); + return; } // If itemType and reason are both set, post the action @@ -96,43 +93,46 @@ export default class FlagButton extends Component { break; } - if (itemType === 'COMMENTS') { - this.setState({ localPost: 'temp' }); - } - let action = { item_id, item_type: itemType, message, }; + if (reason === REASONS.comment.noagree) { - postDontAgree(action) - .then(({ data }) => { - if (itemType === 'COMMENTS') { - this.setState({ localPost: data.createDontAgree.dontagree.id }); - } - }) - .catch(err => { - this.props.notify('error', getErrorMessages(err)); - console.error(err); - }); - } else { - postFlag({ ...action, reason }) - .then(({ data }) => { - if (itemType === 'COMMENTS') { - this.setState({ localPost: data.createFlag.flag.id }); - } - }) - .catch(errors => { - forEachError(errors, ({ error, msg }) => { - if (error.translation_key === 'ALREADY_EXISTS') { - msg = t('already_flagged_username'); - } - this.props.notify('error', msg); + const result = await postDontAgree(action); + try { + if (itemType === 'COMMENTS') { + this.setState({ + localPost: result.data.createDontAgree.dontagree.id, }); + } + } catch (err) { + this.props.notify('error', getErrorMessages(err)); + console.error(err); + failed = true; + } + } else { + try { + const result = await postFlag({ ...action, reason }); + if (itemType === 'COMMENTS') { + this.setState({ localPost: result.data.createFlag.flag.id }); + } + } catch (errors) { + forEachError(errors, ({ error, msg }) => { + if (error.translation_key === 'ALREADY_EXISTS') { + msg = t('already_flagged_username'); + } + this.props.notify('error', msg); }); + failed = true; + } } } + + if (!failed) { + this.setState({ step: step + 1 }); + } }; onPopupOptionClick = sets => e => { diff --git a/client/talk-plugin-history/CommentHistory.js b/client/talk-plugin-history/CommentHistory.js index a56350c4a..a17a347c5 100644 --- a/client/talk-plugin-history/CommentHistory.js +++ b/client/talk-plugin-history/CommentHistory.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import Comment from './Comment'; import LoadMore from './LoadMore'; -import { forEachError } from 'plugin-api/beta/client/utils'; class CommentHistory extends React.Component { state = { @@ -16,11 +15,8 @@ class CommentHistory extends React.Component { .then(() => { this.setState({ loadingState: 'success' }); }) - .catch(error => { + .catch(() => { this.setState({ loadingState: 'error' }); - forEachError(error, ({ msg }) => { - this.props.notify('error', msg); - }); }); }; @@ -55,7 +51,6 @@ class CommentHistory extends React.Component { CommentHistory.propTypes = { comments: PropTypes.object.isRequired, loadMore: PropTypes.func, - notify: PropTypes.func, link: PropTypes.func, data: PropTypes.object, root: PropTypes.object, diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index f5a0167b7..6020d4002 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -9,11 +9,7 @@ import withFragments from 'coral-framework/hocs/withFragments'; import withMutation from 'coral-framework/hocs/withMutation'; import { notify } from 'coral-framework/actions/notification'; import { capitalize } from 'coral-framework/helpers/strings'; -import { - getMyActionSummary, - getTotalActionCount, - getErrorMessages, -} from 'coral-framework/utils'; +import { getMyActionSummary, getTotalActionCount } from 'coral-framework/utils'; import hoistStatics from 'recompose/hoistStatics'; import * as PropTypes from 'prop-types'; import { getDefinitionName } from '../utils'; @@ -282,7 +278,6 @@ export default (reaction, options = {}) => }) .catch(err => { this.duringMutation = false; - this.props.notify('error', getErrorMessages(err)); throw err; }); }; @@ -307,7 +302,6 @@ export default (reaction, options = {}) => }) .catch(err => { this.duringMutation = false; - this.props.notify('error', getErrorMessages(err)); throw err; }); }; diff --git a/plugin-api/beta/client/hocs/withTags.js b/plugin-api/beta/client/hocs/withTags.js index 7b95a5e6c..958028a4b 100644 --- a/plugin-api/beta/client/hocs/withTags.js +++ b/plugin-api/beta/client/hocs/withTags.js @@ -1,13 +1,11 @@ import React from 'react'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import { compose, gql } from 'react-apollo'; import { getDisplayName } from 'coral-framework/helpers/hoc'; import { capitalize } from 'coral-framework/helpers/strings'; import { withAddTag, withRemoveTag } from 'coral-framework/graphql/mutations'; import withFragments from 'coral-framework/hocs/withFragments'; -import { notify } from 'coral-framework/actions/notification'; -import { getErrorMessages, isTagged } from 'coral-framework/utils'; +import { isTagged } from 'coral-framework/utils'; import hoistStatics from 'recompose/hoistStatics'; import { getDefinitionName } from '../utils'; @@ -38,7 +36,7 @@ export default (tag, options = {}) => loading = false; postTag = () => { - const { comment, asset, notify } = this.props; + const { comment, asset } = this.props; if (this.loading) { return; @@ -59,13 +57,12 @@ export default (tag, options = {}) => }) .catch(err => { this.loading = false; - notify('error', getErrorMessages(err)); throw err; }); }; deleteTag = () => { - const { comment, asset, notify } = this.props; + const { comment, asset } = this.props; if (this.loading) { return; @@ -84,7 +81,6 @@ export default (tag, options = {}) => }) .catch(err => { this.loading = false; - notify('error', getErrorMessages(err)); throw err; }); }; @@ -114,9 +110,6 @@ export default (tag, options = {}) => user: state.auth.user, }); - const mapDispatchToProps = dispatch => - bindActionCreators({ notify }, dispatch); - const enhance = compose( withFragments({ ...fragments, @@ -146,7 +139,7 @@ export default (tag, options = {}) => }), withAddTag, withRemoveTag, - connect(mapStateToProps, mapDispatchToProps) + connect(mapStateToProps, null) ); WithTags.displayName = `WithTags(${getDisplayName(WrappedComponent)})`; diff --git a/plugins/talk-plugin-featured-comments/client/components/ModTag.js b/plugins/talk-plugin-featured-comments/client/components/ModTag.js index 420ee9417..659b2cea4 100644 --- a/plugins/talk-plugin-featured-comments/client/components/ModTag.js +++ b/plugins/talk-plugin-featured-comments/client/components/ModTag.js @@ -64,7 +64,6 @@ export default class ModTag extends React.Component { ModTag.propTypes = { alreadyTagged: PropTypes.bool, deleteTag: PropTypes.func, - notify: PropTypes.func, openFeaturedDialog: PropTypes.func, comment: PropTypes.object, asset: PropTypes.object, diff --git a/plugins/talk-plugin-featured-comments/client/components/TabPane.js b/plugins/talk-plugin-featured-comments/client/components/TabPane.js index 084de7d0a..9bdba6fe8 100644 --- a/plugins/talk-plugin-featured-comments/client/components/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/components/TabPane.js @@ -1,7 +1,6 @@ import React from 'react'; import Comment from '../containers/Comment'; import LoadMore from './LoadMore'; -import { getErrorMessages } from 'plugin-api/beta/client/utils'; class TabPane extends React.Component { state = { @@ -15,9 +14,8 @@ class TabPane extends React.Component { .then(() => { this.setState({ loadingState: 'success' }); }) - .catch(error => { + .catch(() => { this.setState({ loadingState: 'error' }); - this.props.notify('error', getErrorMessages(error)); }); }; diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModActionButton.js b/plugins/talk-plugin-featured-comments/client/containers/ModActionButton.js index 1f210cb95..7c8f94fb3 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/ModActionButton.js +++ b/plugins/talk-plugin-featured-comments/client/containers/ModActionButton.js @@ -13,8 +13,8 @@ const mapDispatchToProps = dispatch => ); const enhance = compose( - withTags('featured'), - connect(null, mapDispatchToProps) + connect(null, mapDispatchToProps), + withTags('featured') ); export default enhance(ModActionButton); diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModTag.js b/plugins/talk-plugin-featured-comments/client/containers/ModTag.js index 990a8a2e9..42c653418 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/ModTag.js +++ b/plugins/talk-plugin-featured-comments/client/containers/ModTag.js @@ -3,12 +3,10 @@ import { withTags, connect } from 'plugin-api/beta/client/hocs'; import { gql, compose } from 'react-apollo'; import { bindActionCreators } from 'redux'; import { openFeaturedDialog } from '../actions'; -import { notify } from 'plugin-api/beta/client/actions/notification'; const mapDispatchToProps = dispatch => bindActionCreators( { - notify, openFeaturedDialog, }, dispatch @@ -24,8 +22,8 @@ const fragments = { `, }; const enhance = compose( - withTags('featured', { fragments }), - connect(null, mapDispatchToProps) + connect(null, mapDispatchToProps), + withTags('featured', { fragments }) ); export default enhance(ModTag); diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js index 5bf92fb0a..297f5aedf 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js @@ -4,7 +4,6 @@ import { compose, gql } from 'react-apollo'; import TabPane from '../components/TabPane'; import { withFragments, connect } from 'plugin-api/beta/client/hocs'; import Comment from '../containers/Comment'; -import { notify } from 'plugin-api/beta/client/actions/notification'; import { viewComment } from 'coral-embed-stream/src/actions/stream'; import { appendNewNodes, @@ -81,7 +80,6 @@ const mapDispatchToProps = dispatch => bindActionCreators( { viewComment, - notify, }, dispatch ); diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js index 733dab756..5aa834034 100644 --- a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js @@ -10,21 +10,16 @@ import { bindActionCreators } from 'redux'; import { closeMenu } from 'plugins/talk-plugin-author-menu/client/actions'; import { notify } from 'plugin-api/beta/client/actions/notification'; import { t } from 'plugin-api/beta/client/services'; -import { getErrorMessages } from 'plugin-api/beta/client/utils'; class IgnoreUserConfirmationContainer extends React.Component { ignoreUser = () => { const { ignoreUser, notify, comment, closeMenu } = this.props; - ignoreUser(comment.user.id) - .then(() => { - notify( - 'success', - t('talk-plugin-ignore-user.notify_success', comment.user.username) - ); - }) - .catch(err => { - notify('error', getErrorMessages(err)); - }); + ignoreUser(comment.user.id).then(() => { + notify( + 'success', + t('talk-plugin-ignore-user.notify_success', comment.user.username) + ); + }); closeMenu(); }; diff --git a/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js b/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js index 70cb2ce4b..1f7bed3bb 100644 --- a/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js +++ b/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js @@ -1,23 +1,16 @@ import React from 'react'; import { compose } from 'react-apollo'; -import { bindActionCreators } from 'redux'; -import { getErrorMessages } from 'plugin-api/beta/client/utils'; -import { notify } from 'plugin-api/beta/client/actions/notification'; import ApproveCommentAction from '../components/ApproveCommentAction'; -import { connect, withSetCommentStatus } from 'plugin-api/beta/client/hocs'; +import { withSetCommentStatus } from 'plugin-api/beta/client/hocs'; class ApproveCommentActionContainer extends React.Component { approveComment = async () => { - const { setCommentStatus, comment, hideMenu, notify } = this.props; + const { setCommentStatus, comment, hideMenu } = this.props; - try { - await setCommentStatus({ - commentId: comment.id, - status: 'ACCEPTED', - }); - } catch (err) { - notify('error', getErrorMessages(err)); - } + await setCommentStatus({ + commentId: comment.id, + status: 'ACCEPTED', + }); hideMenu(); }; @@ -32,17 +25,6 @@ class ApproveCommentActionContainer extends React.Component { } } -const mapDispatchToProps = dispatch => - bindActionCreators( - { - notify, - }, - dispatch - ); - -const enhance = compose( - connect(null, mapDispatchToProps), - withSetCommentStatus -); +const enhance = compose(withSetCommentStatus); export default enhance(ApproveCommentActionContainer); diff --git a/plugins/talk-plugin-moderation-actions/client/containers/BanUserDialog.js b/plugins/talk-plugin-moderation-actions/client/containers/BanUserDialog.js index 6d64addca..8dd3cce7d 100644 --- a/plugins/talk-plugin-moderation-actions/client/containers/BanUserDialog.js +++ b/plugins/talk-plugin-moderation-actions/client/containers/BanUserDialog.js @@ -3,19 +3,16 @@ import PropTypes from 'prop-types'; import { compose } from 'react-apollo'; import { bindActionCreators } from 'redux'; import { closeBanDialog, closeMenu } from '../actions'; -import { notify } from 'plugin-api/beta/client/actions/notification'; import { connect, withSetCommentStatus, withBanUser, } from 'plugin-api/beta/client/hocs'; -import { getErrorMessages } from 'plugin-api/beta/client/utils'; import BanUserDialog from '../components/BanUserDialog'; class BanUserDialogContainer extends React.Component { banUser = async () => { const { - notify, authorId, commentId, commentStatus, @@ -25,23 +22,19 @@ class BanUserDialogContainer extends React.Component { banUser, } = this.props; - try { - await banUser({ - id: authorId, - message: '', + await banUser({ + id: authorId, + message: '', + }); + + closeMenu(); + closeBanDialog(); + + if (commentStatus !== 'REJECTED') { + await setCommentStatus({ + commentId: commentId, + status: 'REJECTED', }); - - closeMenu(); - closeBanDialog(); - - if (commentStatus !== 'REJECTED') { - await setCommentStatus({ - commentId: commentId, - status: 'REJECTED', - }); - } - } catch (err) { - notify('error', getErrorMessages(err)); } }; @@ -70,7 +63,6 @@ const mapStateToProps = ({ talkPluginModerationActions: state }) => ({ const mapDispatchToProps = dispatch => bindActionCreators( { - notify, closeBanDialog, closeMenu, }, diff --git a/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js b/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js index e49fdddba..54ca0420d 100644 --- a/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js +++ b/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js @@ -1,23 +1,16 @@ import React from 'react'; import { compose } from 'react-apollo'; -import { bindActionCreators } from 'redux'; -import { getErrorMessages } from 'plugin-api/beta/client/utils'; -import { notify } from 'plugin-api/beta/client/actions/notification'; import RejectCommentAction from '../components/RejectCommentAction'; -import { connect, withSetCommentStatus } from 'plugin-api/beta/client/hocs'; +import { withSetCommentStatus } from 'plugin-api/beta/client/hocs'; class RejectCommentActionContainer extends React.Component { rejectComment = async () => { - const { setCommentStatus, comment, hideMenu, notify } = this.props; + const { setCommentStatus, comment, hideMenu } = this.props; - try { - await setCommentStatus({ - commentId: comment.id, - status: 'REJECTED', - }); - } catch (err) { - notify('error', getErrorMessages(err)); - } + await setCommentStatus({ + commentId: comment.id, + status: 'REJECTED', + }); hideMenu(); }; @@ -27,17 +20,6 @@ class RejectCommentActionContainer extends React.Component { } } -const mapDispatchToProps = dispatch => - bindActionCreators( - { - notify, - }, - dispatch - ); - -const enhance = compose( - connect(null, mapDispatchToProps), - withSetCommentStatus -); +const enhance = compose(withSetCommentStatus); export default enhance(RejectCommentActionContainer);