diff --git a/client/coral-admin/src/components/AccountHistory.js b/client/coral-admin/src/components/AccountHistory.js index a16b334ca..0d85b62af 100644 --- a/client/coral-admin/src/components/AccountHistory.js +++ b/client/coral-admin/src/components/AccountHistory.js @@ -14,7 +14,7 @@ const buildUserHistory = (userState = {}) => { return orderBy( flatten( Object.keys(userState.status) - .filter(k => k !== '__typename') + .filter(k => !k.startsWith('__')) .map(k => userState.status[k].history) ), 'created_at', diff --git a/client/coral-admin/src/components/CommentDetails.js b/client/coral-admin/src/components/CommentDetails.js index dc1be2d85..a9c0884a4 100644 --- a/client/coral-admin/src/components/CommentDetails.js +++ b/client/coral-admin/src/components/CommentDetails.js @@ -25,37 +25,29 @@ class CommentDetails extends Component { }; render() { - const { data, root, comment, clearHeightCache } = this.props; + const { root, comment, clearHeightCache } = this.props; const { showDetail } = this.state; - const queryData = { + + const slotPassthrough = { + clearHeightCache, root, comment, + more: showDetail, }; return (
{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')} - + {showDetail && ( - + )}
); @@ -63,7 +55,6 @@ class CommentDetails extends Component { } CommentDetails.propTypes = { - data: PropTypes.object.isRequired, root: PropTypes.object.isRequired, comment: PropTypes.object.isRequired, clearHeightCache: PropTypes.func, diff --git a/client/coral-admin/src/components/CommentLabels.js b/client/coral-admin/src/components/CommentLabels.js index 8eac152bd..1f71d9771 100644 --- a/client/coral-admin/src/components/CommentLabels.js +++ b/client/coral-admin/src/components/CommentLabels.js @@ -43,6 +43,9 @@ const CommentLabels = ({ comment, comment: { className, status, actions, hasParent }, }) => { + const slotPassthrough = { + comment, + }; return (
@@ -69,7 +72,7 @@ const CommentLabels = ({
); diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index b1f68bac2..31ad999dc 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -98,7 +98,6 @@ class UserDetail extends React.Component { renderLoaded() { const { - data, root, root: { me, user, totalComments, rejectedComments }, activeTab, @@ -123,6 +122,11 @@ class UserDetail extends React.Component { const banned = isBanned(user); const suspended = isSuspended(user); + const slotPassthrough = { + root, + user, + }; + return (
- +
@@ -301,7 +301,6 @@ class UserDetail extends React.Component { - + ); } @@ -138,7 +135,6 @@ class UserDetailComment extends React.Component { UserDetailComment.propTypes = { selected: PropTypes.bool, - data: PropTypes.object, user: PropTypes.object.isRequired, viewUserDetail: PropTypes.func.isRequired, acceptComment: PropTypes.func.isRequired, diff --git a/client/coral-admin/src/components/UserDetailCommentList.js b/client/coral-admin/src/components/UserDetailCommentList.js index a0ff9982b..3a31ee3f9 100644 --- a/client/coral-admin/src/components/UserDetailCommentList.js +++ b/client/coral-admin/src/components/UserDetailCommentList.js @@ -9,7 +9,6 @@ import ApproveButton from './ApproveButton'; const UserDetailCommentList = props => { const { - data, root, root: { user, comments: { nodes, hasNextPage } }, acceptComment, @@ -70,7 +69,6 @@ const UserDetailCommentList = props => { key={comment.id} user={user} root={root} - data={data} comment={comment} acceptComment={acceptComment} rejectComment={rejectComment} @@ -93,7 +91,6 @@ UserDetailCommentList.propTypes = { root: PropTypes.object.isRequired, acceptComment: PropTypes.func.isRequired, rejectComment: PropTypes.func.isRequired, - data: PropTypes.object.isRequired, selectedCommentIds: PropTypes.array.isRequired, viewUserDetail: PropTypes.any.isRequired, loadMore: PropTypes.any.isRequired, diff --git a/client/coral-admin/src/routes/Configure/components/Configure.js b/client/coral-admin/src/routes/Configure/components/Configure.js index 140222f68..efd428378 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.js +++ b/client/coral-admin/src/routes/Configure/components/Configure.js @@ -75,7 +75,6 @@ export default class Configure extends Component {
@@ -88,7 +87,6 @@ export default class Configure extends Component { Configure.propTypes = { savePending: PropTypes.func.isRequired, currentUser: PropTypes.object.isRequired, - data: PropTypes.object.isRequired, root: PropTypes.object.isRequired, settings: PropTypes.object.isRequired, canSave: PropTypes.bool.isRequired, diff --git a/client/coral-admin/src/routes/Configure/components/ModerationSettings.js b/client/coral-admin/src/routes/Configure/components/ModerationSettings.js index 3b5f3bdd9..cf291cab1 100644 --- a/client/coral-admin/src/routes/Configure/components/ModerationSettings.js +++ b/client/coral-admin/src/routes/Configure/components/ModerationSettings.js @@ -52,7 +52,7 @@ class ModerationSettings extends React.Component { }; render() { - const { settings, data, root, updatePending, errors } = this.props; + const { settings, slotPassthrough } = this.props; return ( @@ -82,13 +82,7 @@ class ModerationSettings extends React.Component { suspectWords={settings.wordlist.suspect} onChangeWordlist={this.updateWordlist} /> - + ); } @@ -97,9 +91,8 @@ class ModerationSettings extends React.Component { ModerationSettings.propTypes = { updatePending: PropTypes.func.isRequired, errors: PropTypes.object.isRequired, - data: PropTypes.object.isRequired, - root: PropTypes.object.isRequired, settings: PropTypes.object.isRequired, + slotPassthrough: PropTypes.object.isRequired, }; export default ModerationSettings; diff --git a/client/coral-admin/src/routes/Configure/components/StreamSettings.js b/client/coral-admin/src/routes/Configure/components/StreamSettings.js index c447b8453..e6424848f 100644 --- a/client/coral-admin/src/routes/Configure/components/StreamSettings.js +++ b/client/coral-admin/src/routes/Configure/components/StreamSettings.js @@ -107,7 +107,7 @@ class StreamSettings extends React.Component { }; render() { - const { settings, data, root, errors, updatePending } = this.props; + const { settings, slotPassthrough, errors } = this.props; return ( @@ -220,13 +220,7 @@ class StreamSettings extends React.Component {
{/* the above card should be the last one if at all possible because of z-index issues with the selects */} - + ); } @@ -235,9 +229,8 @@ class StreamSettings extends React.Component { StreamSettings.propTypes = { updatePending: PropTypes.func.isRequired, errors: PropTypes.object.isRequired, - data: PropTypes.object.isRequired, - root: PropTypes.object.isRequired, settings: PropTypes.object.isRequired, + slotPassthrough: PropTypes.object.isRequired, }; export default StreamSettings; diff --git a/client/coral-admin/src/routes/Configure/components/TechSettings.js b/client/coral-admin/src/routes/Configure/components/TechSettings.js index 8bb10e9a3..e6d9ba8af 100644 --- a/client/coral-admin/src/routes/Configure/components/TechSettings.js +++ b/client/coral-admin/src/routes/Configure/components/TechSettings.js @@ -34,7 +34,7 @@ class TechSettings extends React.Component { }; render() { - const { settings, data, root, errors, updatePending } = this.props; + const { settings, slotPassthrough } = this.props; return ( - + ); } @@ -64,10 +58,9 @@ class TechSettings extends React.Component { TechSettings.propTypes = { updatePending: PropTypes.func.isRequired, - errors: PropTypes.object.isRequired, - data: PropTypes.object.isRequired, - root: PropTypes.object.isRequired, settings: PropTypes.object.isRequired, + slotPassthrough: PropTypes.object.isRequired, + errors: PropTypes.object, }; export default TechSettings; diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js index fe87766b6..ce6ea0627 100644 --- a/client/coral-admin/src/routes/Configure/containers/Configure.js +++ b/client/coral-admin/src/routes/Configure/containers/Configure.js @@ -31,7 +31,6 @@ class ConfigureContainer extends Component { return ( ({ + slotPassthrough: { + root, + settings, + updatePending, + errors, + }, + updatePending, + settings, + errors, + ...rest, + })) )(ModerationSettings); diff --git a/client/coral-admin/src/routes/Configure/containers/StreamSettings.js b/client/coral-admin/src/routes/Configure/containers/StreamSettings.js index 24e8ad459..5bc8e69a9 100644 --- a/client/coral-admin/src/routes/Configure/containers/StreamSettings.js +++ b/client/coral-admin/src/routes/Configure/containers/StreamSettings.js @@ -5,6 +5,7 @@ import StreamSettings from '../components/StreamSettings'; import withFragments from 'coral-framework/hocs/withFragments'; import { getSlotFragmentSpreads } from 'coral-framework/utils'; import { updatePending } from '../../../actions/configure'; +import { mapProps } from 'recompose'; const slots = ['adminStreamSettings']; @@ -42,5 +43,17 @@ export default compose( } `, }), - connect(mapStateToProps, mapDispatchToProps) + connect(mapStateToProps, mapDispatchToProps), + mapProps(({ root, settings, updatePending, errors, ...rest }) => ({ + slotPassthrough: { + root, + settings, + updatePending, + errors, + }, + updatePending, + settings, + errors, + ...rest, + })) )(StreamSettings); diff --git a/client/coral-admin/src/routes/Configure/containers/TechSettings.js b/client/coral-admin/src/routes/Configure/containers/TechSettings.js index c096d1af9..7fbe81cdb 100644 --- a/client/coral-admin/src/routes/Configure/containers/TechSettings.js +++ b/client/coral-admin/src/routes/Configure/containers/TechSettings.js @@ -5,6 +5,7 @@ import TechSettings from '../components/TechSettings'; import withFragments from 'coral-framework/hocs/withFragments'; import { getSlotFragmentSpreads } from 'coral-framework/utils'; import { updatePending } from '../../../actions/configure'; +import { mapProps } from 'recompose'; const slots = ['adminTechSettings']; @@ -38,5 +39,17 @@ export default compose( } `, }), - connect(mapStateToProps, mapDispatchToProps) + connect(mapStateToProps, mapDispatchToProps), + mapProps(({ root, settings, updatePending, errors, ...rest }) => ({ + slotPassthrough: { + root, + settings, + updatePending, + errors, + }, + updatePending, + settings, + errors, + ...rest, + })) )(TechSettings); diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index efdcfbd79..bc5780c0a 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -53,7 +53,6 @@ class Comment extends React.Component { comment, selected, className, - data, root, root: { settings }, currentAsset, @@ -62,7 +61,6 @@ class Comment extends React.Component { } = this.props; const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'; - const queryData = { root, comment, asset: comment.asset }; const formatterSettings = { suspectWords: settings.wordlist.suspect, @@ -70,6 +68,13 @@ class Comment extends React.Component { body: comment.body, }; + const slotPassthrough = { + clearHeightCache, + root, + comment, + asset: comment.asset, + }; + return (
  • @@ -135,13 +138,10 @@ class Comment extends React.Component { - + ); } diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js index 0e7cbe347..e47a162ef 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js @@ -344,7 +344,6 @@ class ModerationQueue extends React.Component { child = (
    - + ); } diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index 0e251e9a3..0c4fed129 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -63,6 +63,7 @@ export default class Embed extends React.Component { } = this.props; const hasHighlightedComment = !!commentId; const popupUrl = `login?parentUrl=${encodeURIComponent(parentUrl)}`; + const slotPassthrough = { root }; return (
    - + el.key - ); - const nextKeys = this.getSlotElements(next.tabSlot, next).map( - el => el.key - ); - return !isEqual(prevKeys, nextKeys); - } - - // Prevent Slot from rerendering when no props has shallowly changed. - return changes.length !== 0; - } - handleFallback(props = this.props) { if (this.getTabNames(props).indexOf(props.activeTab) === -1) { props.setActiveTab(props.fallbackTab); @@ -48,38 +25,26 @@ class ExtendableTabPanelContainer extends React.Component { return this.getTabElements(props).map(el => el.props.tabId); } - getSlotElements(slot, props = this.props) { - const { plugins } = this.context; - return plugins.getSlotElements( - slot, - props.reduxState, - props.slotProps, - props.queryData - ); - } - getPluginTabElements(props = this.props) { - return this.getSlotTabElements(props.tabSlot); + return props.slotElements[0].map(this.createPluginTabFactory(props)); } getPluginTabElementsPrepend(props = this.props) { - return this.getSlotTabElements(props.tabSlotPrepend); + return props.slotElements[1].map(this.createPluginTabFactory(props)); } - getSlotTabElements(slot) { - return this.getSlotElements(slot).map(el => { - return ( - - {React.cloneElement(el, { - active: this.props.activeTab === el.type.talkPluginName, - })} - - ); - }); - } + createPluginTabFactory = (props = this.props) => el => { + return ( + + {React.cloneElement(el, { + active: props.activeTab === el.type.talkPluginName, + })} + + ); + }; getTabElements(props = this.props) { const elements = [...this.getPluginTabElementsPrepend(props)]; @@ -92,14 +57,16 @@ class ExtendableTabPanelContainer extends React.Component { return elements; } + createPluginTabPane(el) { + return ( + + {el} + + ); + } + getPluginTabPaneElements(props = this.props) { - return this.getSlotElements(props.tabPaneSlot).map(el => { - return ( - - {el} - - ); - }); + return props.slotElements[2].map(this.createPluginTabPane); } render() { @@ -132,15 +99,15 @@ ExtendableTabPanelContainer.propTypes = { tabSlot: PropTypes.string.isRequired, tabSlotPrepend: PropTypes.string.isRequired, tabPaneSlot: PropTypes.string.isRequired, - slotProps: PropTypes.object.isRequired, - queryData: PropTypes.object, + slotPassthrough: PropTypes.object, className: PropTypes.string, sub: PropTypes.bool, loading: PropTypes.bool, }; -const mapStateToProps = state => ({ - reduxState: state, -}); - -export default connect(mapStateToProps, null)(ExtendableTabPanelContainer); +export default compose( + withSlotElements({ + slot: props => [props.tabSlot, props.tabSlotPrepend, props.tabPaneSlot], + passthroughPropName: 'slotPassthrough', + }) +)(ExtendableTabPanelContainer); diff --git a/client/coral-embed-stream/src/tabs/configure/components/Configure.js b/client/coral-embed-stream/src/tabs/configure/components/Configure.js index b4b925d92..566aefe00 100644 --- a/client/coral-embed-stream/src/tabs/configure/components/Configure.js +++ b/client/coral-embed-stream/src/tabs/configure/components/Configure.js @@ -7,11 +7,7 @@ class Configure extends React.Component { render() { return (
    - +
    @@ -20,7 +16,6 @@ class Configure extends React.Component { } Configure.propTypes = { - data: PropTypes.object.isRequired, root: PropTypes.object.isRequired, asset: PropTypes.object.isRequired, }; diff --git a/client/coral-embed-stream/src/tabs/configure/components/Settings.js b/client/coral-embed-stream/src/tabs/configure/components/Settings.js index 7e11b1311..2b1e49c84 100644 --- a/client/coral-embed-stream/src/tabs/configure/components/Settings.js +++ b/client/coral-embed-stream/src/tabs/configure/components/Settings.js @@ -25,9 +25,9 @@ class Settings extends React.Component { onQuestionBoxContentChange, canSave, onApply, - slotProps, - queryData, + slotPassthrough, } = this.props; + return (
    @@ -77,7 +77,7 @@ class Settings extends React.Component {
    )} - +
    ); @@ -85,8 +85,7 @@ class Settings extends React.Component { } Settings.propTypes = { - queryData: PropTypes.object.isRequired, - slotProps: PropTypes.object.isRequired, + slotPassthrough: PropTypes.object.isRequired, settings: PropTypes.object.isRequired, canSave: PropTypes.bool.isRequired, onToggleModeration: PropTypes.func.isRequired, 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 b6104b067..1ba86509e 100644 --- a/client/coral-embed-stream/src/tabs/configure/containers/Configure.js +++ b/client/coral-embed-stream/src/tabs/configure/containers/Configure.js @@ -13,13 +13,7 @@ class ConfigureContainer extends React.Component { return
    {this.props.data.error.message}
    ; } - return ( - - ); + return ; } } diff --git a/client/coral-embed-stream/src/tabs/configure/containers/Settings.js b/client/coral-embed-stream/src/tabs/configure/containers/Settings.js index 2058ceaa2..cfb72c0ae 100644 --- a/client/coral-embed-stream/src/tabs/configure/containers/Settings.js +++ b/client/coral-embed-stream/src/tabs/configure/containers/Settings.js @@ -57,17 +57,24 @@ class SettingsContainer extends React.Component { const { mergedSettings, canSave, - data, root, asset, errors, updatePending, } = this.props; + + const slotPassthrough = { + root, + asset, + settings: mergedSettings, + updatePending, + errors, + }; + return ( @@ -33,8 +33,7 @@ class Comment extends React.Component { fill="commentContent" defaultComponent={CommentContent} className={cn(styles.commentBody, 'my-comment-body')} - data={data} - queryData={queryData} + passthrough={slotPassthrough} />
  • @@ -114,7 +114,6 @@ class Comment extends React.Component { Comment.propTypes = { comment: PropTypes.object.isRequired, navigate: PropTypes.func.isRequired, - data: PropTypes.object.isRequired, root: PropTypes.object.isRequired, }; diff --git a/client/coral-embed-stream/src/tabs/profile/components/CommentHistory.js b/client/coral-embed-stream/src/tabs/profile/components/CommentHistory.js index 64d9e97ae..c15314f29 100644 --- a/client/coral-embed-stream/src/tabs/profile/components/CommentHistory.js +++ b/client/coral-embed-stream/src/tabs/profile/components/CommentHistory.js @@ -22,7 +22,7 @@ class CommentHistory extends React.Component { }; render() { - const { navigate, comments, data, root } = this.props; + const { navigate, comments, root } = this.props; if (!comments.nodes.length) { return ; } @@ -33,7 +33,6 @@ class CommentHistory extends React.Component { return ( ( -
    -
    -

    {username}

    - {emailAddress ?

    {emailAddress}

    : null} +const Profile = ({ username, emailAddress, root, slotPassthrough }) => { + return ( +
    +
    +

    {username}

    + {emailAddress ?

    {emailAddress}

    : null} +
    + +
    - - - {t('framework.my_comments')} - , - ]} - tabPanes={[ - - - , - ]} - sub - /> -
    -); + ); +}; Profile.propTypes = { username: PropTypes.string, emailAddress: PropTypes.string, - data: PropTypes.object, root: PropTypes.object, - activeTab: PropTypes.string.isRequired, - setActiveTab: PropTypes.func.isRequired, + slotPassthrough: PropTypes.object, }; export default Profile; diff --git a/client/coral-embed-stream/src/tabs/profile/components/Settings.js b/client/coral-embed-stream/src/tabs/profile/components/Settings.js new file mode 100644 index 000000000..c61a09a92 --- /dev/null +++ b/client/coral-embed-stream/src/tabs/profile/components/Settings.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Slot } from 'coral-framework/components'; + +class Settings extends React.Component { + render() { + const { root } = this.props; + const slotPassthrough = { root }; + return ( +
    + +
    + ); + } +} + +Settings.propTypes = { + root: PropTypes.object, +}; + +export default Settings; diff --git a/client/coral-embed-stream/src/tabs/profile/components/TabPanel.js b/client/coral-embed-stream/src/tabs/profile/components/TabPanel.js new file mode 100644 index 000000000..08270bcd8 --- /dev/null +++ b/client/coral-embed-stream/src/tabs/profile/components/TabPanel.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import CommentHistory from '../containers/CommentHistory'; +import ExtendableTabPanel from '../../../containers/ExtendableTabPanel'; +import { Tab, TabPane } from 'coral-ui'; +import t from 'coral-framework/services/i18n'; +import Settings from '../containers/Settings'; + +const TabPanel = ({ + root, + activeTab, + setActiveTab, + showSettingsTab, + slotPassthrough, +}) => { + const tabs = [ + + {t('framework.my_comments')} + , + ]; + + if (showSettingsTab) { + tabs.push( + + {t('profile_settings')} + + ); + } + + return ( + + + , + + + , + ]} + sub + /> + ); +}; + +TabPanel.propTypes = { + root: PropTypes.object, + slotPassthrough: PropTypes.object, + activeTab: PropTypes.string.isRequired, + setActiveTab: PropTypes.func.isRequired, + showSettingsTab: PropTypes.bool.isRequired, +}; + +export default TabPanel; diff --git a/client/coral-embed-stream/src/tabs/profile/containers/CommentHistory.js b/client/coral-embed-stream/src/tabs/profile/containers/CommentHistory.js index a02225939..27727216e 100644 --- a/client/coral-embed-stream/src/tabs/profile/containers/CommentHistory.js +++ b/client/coral-embed-stream/src/tabs/profile/containers/CommentHistory.js @@ -4,7 +4,7 @@ import { connect } from 'react-redux'; import { compose, gql } from 'react-apollo'; import CommentHistory from '../components/CommentHistory'; import Comment from './Comment'; -import { withFragments } from 'coral-framework/hocs'; +import { withFragments, withFetchMore } from 'coral-framework/hocs'; import { appendNewNodes } from 'plugin-api/beta/client/utils'; import update from 'immutability-helper'; @@ -16,7 +16,7 @@ class CommentHistoryContainer extends Component { }; loadMore = () => { - return this.props.data.fetchMore({ + return this.props.fetchMore({ query: LOAD_MORE_QUERY, variables: { limit: 5, @@ -43,7 +43,6 @@ class CommentHistoryContainer extends Component { return ( ({ export default compose( connect(mapStateToProps, null), - withCommentHistoryFragments + withCommentHistoryFragments, + withFetchMore )(CommentHistoryContainer); diff --git a/client/coral-embed-stream/src/tabs/profile/containers/Profile.js b/client/coral-embed-stream/src/tabs/profile/containers/Profile.js index 82a0a49eb..30d634d94 100644 --- a/client/coral-embed-stream/src/tabs/profile/containers/Profile.js +++ b/client/coral-embed-stream/src/tabs/profile/containers/Profile.js @@ -7,11 +7,10 @@ import { withQuery } from 'coral-framework/hocs'; import NotLoggedIn from '../components/NotLoggedIn'; import { Spinner } from 'coral-ui'; import Profile from '../components/Profile'; -import CommentHistory from './CommentHistory'; +import TabPanel from './TabPanel'; import { getDefinitionName } from 'coral-framework/utils'; import { showSignInDialog } from 'coral-embed-stream/src/actions/login'; -import { setActiveTab } from '../../../actions/profile'; import { getSlotFragmentSpreads } from 'coral-framework/utils'; class ProfileContainer extends Component { @@ -23,7 +22,7 @@ class ProfileContainer extends Component { } render() { - const { currentUser, showSignInDialog, root, data } = this.props; + const { currentUser, showSignInDialog, root } = this.props; const { me } = this.props.root; const loading = this.props.data.loading; @@ -41,15 +40,14 @@ class ProfileContainer extends Component { const localProfile = currentUser.profiles.find(p => p.provider === 'local'); const emailAddress = localProfile && localProfile.id; + const slotPassthrough = { root }; return ( ); } @@ -60,16 +58,9 @@ ProfileContainer.propTypes = { root: PropTypes.object, currentUser: PropTypes.object, showSignInDialog: PropTypes.func, - activeTab: PropTypes.string.isRequired, - setActiveTab: PropTypes.func.isRequired, }; -const slots = [ - 'profileSections', - 'profileTabs', - 'profileTabsPrepend', - 'profileTabPanes', -]; +const slots = ['profileSections']; const withProfileQuery = withQuery( gql` @@ -78,10 +69,10 @@ const withProfileQuery = withQuery( id username } - ...${getDefinitionName(CommentHistory.fragments.root)} + ...${getDefinitionName(TabPanel.fragments.root)} ${getSlotFragmentSpreads(slots, 'root')} } - ${CommentHistory.fragments.root} + ${TabPanel.fragments.root} `, { options: { @@ -92,11 +83,10 @@ const withProfileQuery = withQuery( const mapStateToProps = state => ({ currentUser: state.auth.user, - activeTab: state.profile.activeTab, }); const mapDispatchToProps = dispatch => - bindActionCreators({ showSignInDialog, setActiveTab }, dispatch); + bindActionCreators({ showSignInDialog }, dispatch); export default compose( connect(mapStateToProps, mapDispatchToProps), diff --git a/client/coral-embed-stream/src/tabs/profile/containers/Settings.js b/client/coral-embed-stream/src/tabs/profile/containers/Settings.js new file mode 100644 index 000000000..795a3c10a --- /dev/null +++ b/client/coral-embed-stream/src/tabs/profile/containers/Settings.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { compose, gql } from 'react-apollo'; +import Settings from '../components/Settings'; +import { withFragments } from 'coral-framework/hocs'; +import { getSlotFragmentSpreads } from 'coral-framework/utils'; + +const slots = ['profileSettings']; + +class SettingsContainer extends React.Component { + render() { + return ; + } +} + +const enhance = compose( + withFragments({ + root: gql` + fragment TalkEmbedStream_ProfileSettings_root on RootQuery { + __typename + ${getSlotFragmentSpreads(slots, 'root')} + } + `, + }) +); + +export default enhance(SettingsContainer); diff --git a/client/coral-embed-stream/src/tabs/profile/containers/TabPanel.js b/client/coral-embed-stream/src/tabs/profile/containers/TabPanel.js new file mode 100644 index 000000000..46c24227f --- /dev/null +++ b/client/coral-embed-stream/src/tabs/profile/containers/TabPanel.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { compose, gql } from 'react-apollo'; +import { bindActionCreators } from 'redux'; +import { withSlotElements, withFragments } from 'coral-framework/hocs'; +import Settings from './Settings'; +import CommentHistory from './CommentHistory'; +import { getDefinitionName } from 'coral-framework/utils'; +import TabPanel from '../components/TabPanel'; +import { setActiveTab } from '../../../actions/profile'; +import { getSlotFragmentSpreads } from 'coral-framework/utils'; + +class TabPanelContainer extends Component { + render() { + return ( + 0} + /> + ); + } +} + +TabPanelContainer.propTypes = { + root: PropTypes.object, + slotPassthrough: PropTypes.object, + activeTab: PropTypes.string.isRequired, + setActiveTab: PropTypes.func.isRequired, + profileSettingsSlotElements: PropTypes.array.isRequired, +}; + +const slots = ['profileTabs', 'profileTabsPrepend', 'profileTabPanes']; + +const mapStateToProps = state => ({ + activeTab: state.profile.activeTab, +}); + +const mapDispatchToProps = dispatch => + bindActionCreators({ setActiveTab }, dispatch); + +export default compose( + withFragments({ + root: gql` + fragment TalkEmbedStream_ProfileTabPanel_root on RootQuery { + __typename + ...${getDefinitionName(CommentHistory.fragments.root)} + ...${getDefinitionName(Settings.fragments.root)} + ${getSlotFragmentSpreads(slots, 'root')} + } + ${CommentHistory.fragments.root} + ${Settings.fragments.root} +`, + }), + connect(mapStateToProps, mapDispatchToProps), + withSlotElements({ + slot: 'profileSettings', + propName: 'profileSettingsSlotElements', + passthroughPropName: 'slotPassthrough', + }) +)(TabPanelContainer); 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 3190ab155..9aa2b8a4c 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js +++ b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js @@ -126,7 +126,6 @@ class AllCommentsPane extends React.Component { render() { const { - data, root, comments, commentClassNames, @@ -164,7 +163,6 @@ class AllCommentsPane extends React.Component { return ( { return ( @@ -573,8 +565,7 @@ export default class Comment extends React.Component { className={cn(styles.username, 'talk-stream-comment-user-name')} fill="commentAuthorName" defaultComponent={CommentAuthorName} - queryData={queryData} - {...slotProps} + passthrough={slotPassthrough} />
    @@ -607,9 +597,10 @@ export default class Comment extends React.Component { fill="commentTimestamp" defaultComponent={CommentTimestamp} className={'talk-stream-comment-published-date'} - created_at={comment.created_at} - queryData={queryData} - {...slotProps} + passthrough={{ + created_at: comment.created_at, + ...slotPassthrough, + }} /> {comment.editing && comment.editing.edited ? ( @@ -624,8 +615,7 @@ export default class Comment extends React.Component { {isActive && @@ -665,9 +655,8 @@ export default class Comment extends React.Component { fill="commentContent" className="talk-stream-comment-content" defaultComponent={CommentContent} - {...slotProps} - queryData={queryData} - slotSize={1} + size={1} + passthrough={slotPassthrough} />
    )} @@ -678,8 +667,7 @@ export default class Comment extends React.Component {
    @@ -696,9 +684,7 @@ export default class Comment extends React.Component {
    diff --git a/client/coral-embed-stream/src/tabs/stream/components/DraftArea.js b/client/coral-embed-stream/src/tabs/stream/components/DraftArea.js index 202349370..7150a124d 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/DraftArea.js +++ b/client/coral-embed-stream/src/tabs/stream/components/DraftArea.js @@ -43,20 +43,13 @@ export default class DraftArea extends React.Component { charCountEnable, maxCharCount, onChange, - queryData, isReply, + registerHook, + unregisterHook, + root, + comment, } = this.props; - const tASettings = { - value, - placeholder, - id, - onChange, - rows, - disabled, - isReply, - }; - return (
    @@ -96,7 +98,8 @@ DraftArea.propTypes = { onChange: PropTypes.func, disabled: PropTypes.bool, rows: PropTypes.number, - queryData: PropTypes.object.isRequired, + root: PropTypes.object.isRequired, + comment: PropTypes.object, registerHook: PropTypes.func, unregisterHook: PropTypes.func, isReply: PropTypes.bool, 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 adcc4aae8..75f04da0a 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Stream.js @@ -40,7 +40,6 @@ class Stream extends React.Component { renderHighlightedComment() { const { - data, root, activeReplyBox, setActiveReplyBox, @@ -91,7 +90,6 @@ class Stream extends React.Component {
    - +
    @@ -180,7 +175,6 @@ class Stream extends React.Component { tabPanes={ {t('stream.comment_not_found')}; } + const slotPassthrough = { root, asset }; + return (
    {open ? ( @@ -263,11 +256,7 @@ class Stream extends React.Component { content={asset.settings.questionBoxContent} icon={asset.settings.questionBoxIcon} > - + )} {!banned && @@ -304,7 +293,7 @@ class Stream extends React.Component {

    {asset.settings.closedMessage}

    )} - + {currentUser && ( } diff --git a/client/coral-embed-stream/src/tabs/stream/containers/DraftArea.js b/client/coral-embed-stream/src/tabs/stream/containers/DraftArea.js index 47338fc8c..f68c41dda 100644 --- a/client/coral-embed-stream/src/tabs/stream/containers/DraftArea.js +++ b/client/coral-embed-stream/src/tabs/stream/containers/DraftArea.js @@ -42,11 +42,10 @@ class DraftAreaContainer extends React.Component { } render() { - const queryData = { comment: this.props.comment, root: this.props.root }; - return ( - this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData) - ); + const { slotElements } = props; + return slotElements.length === 0 + ? false + : slotElements.every(elements => elements.length === 0); } render() { @@ -44,10 +19,15 @@ class IfSlotIsEmpty extends React.Component { IfSlotIsEmpty.propTypes = { slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + children: PropTypes.node.isRequired, + passthrough: PropTypes.object.isRequired, }; -const mapStateToProps = state => ({ - reduxState: state, -}); +const omitProps = ['slot', 'children']; -export default connect(mapStateToProps, null)(IfSlotIsEmpty); +export default compose( + withCompatPassthrough(omitProps), + withSlotElements({ + slot: props => props.slot, + }) +)(IfSlotIsEmpty); diff --git a/client/coral-framework/components/IfSlotIsNotEmpty.js b/client/coral-framework/components/IfSlotIsNotEmpty.js index 03f300858..6318e6447 100644 --- a/client/coral-framework/components/IfSlotIsNotEmpty.js +++ b/client/coral-framework/components/IfSlotIsNotEmpty.js @@ -1,39 +1,14 @@ import React, { Children } from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { getShallowChanges } from 'coral-framework/utils'; +import { withSlotElements, withCompatPassthrough } from '../hocs'; +import { compose } from 'recompose'; class IfSlotIsNotEmpty extends React.Component { - static contextTypes = { - plugins: PropTypes.object, - }; - - shouldComponentUpdate(next) { - // Prevent Slot from rerendering when only reduxState has changed and - // it does not result in a change. - const changes = getShallowChanges(this.props, next); - if (changes.length === 1 && changes[0] === 'reduxState') { - return this.isSlotEmpty(this.props) !== this.isSlotEmpty(next); - } - - // Prevent Slot from rerendering when no props has shallowly changed. - return changes.length !== 0; - } - isSlotEmpty(props = this.props) { - const { - slot, - className: _a, - reduxState, - component: _b = 'div', - children: _c, - queryData, - ...rest - } = props; - const slots = Array.isArray(slot) ? slot : [slot]; - return slots.every(slot => - this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData) - ); + const { slotElements } = props; + return slotElements.length === 0 + ? false + : slotElements.every(elements => elements.length === 0); } render() { @@ -44,10 +19,15 @@ class IfSlotIsNotEmpty extends React.Component { IfSlotIsNotEmpty.propTypes = { slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + children: PropTypes.node.isRequired, + passthrough: PropTypes.object.isRequired, }; -const mapStateToProps = state => ({ - reduxState: state, -}); +const omitProps = ['slot', 'children']; -export default connect(mapStateToProps, null)(IfSlotIsNotEmpty); +export default compose( + withCompatPassthrough(omitProps), + withSlotElements({ + slot: props => props.slot, + }) +)(IfSlotIsNotEmpty); diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index e6a3eda2d..a2f90161f 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -4,106 +4,29 @@ import styles from './Slot.css'; import { connect } from 'react-redux'; import kebabCase from 'lodash/kebabCase'; import PropTypes from 'prop-types'; -import isEqual from 'lodash/isEqual'; import get from 'lodash/get'; -import { getShallowChanges } from 'coral-framework/utils'; -import omit from 'lodash/omit'; -import merge from 'lodash/merge'; - -const emptyConfig = {}; +import { withSlotElements, withCompatPassthrough } from '../hocs'; +import { compose } from 'recompose'; class Slot extends React.Component { - static contextTypes = { - plugins: PropTypes.object, - }; - - shouldComponentUpdate(next) { - // Prevent Slot from rerendering when only reduxState has changed and - // it does not result in a change of slot children. - const changes = getShallowChanges(this.props, next); - if (changes.length === 1 && changes[0] === 'reduxState') { - const prevChildrenKeys = this.getChildren(this.props).map( - child => child.key - ); - const nextChildrenKeys = this.getChildren(next).map(child => child.key); - return !isEqual(prevChildrenKeys, nextChildrenKeys); - } - - // Prevent Slot from rerendering when no props has shallowly changed. - return changes.length !== 0; - } - - getSlotProps(props = this.props) { - return omit(props, [ - 'fill', - 'inline', - 'className', - 'reduxState', - 'slotSize', - 'defaultComponent', - 'queryData', - 'childFactory', - 'component', - ]); - } - - getChildren(props = this.props) { - const { slotSize = 0 } = props; - const { plugins } = this.context; - - return plugins.getSlotElements( - props.fill, - props.reduxState, - this.getSlotProps(props), - props.queryData, - { slotSize } - ); - } - render() { const { inline = false, className, - reduxState, + debug, component: Component, childFactory, - defaultComponent: DefaultComponent, - queryData, fill, } = this.props; - const { plugins } = this.context; - let children = this.getChildren(); - - // @Deprecated plugin_config - const pluginsConfig = - merge( - get(reduxState, 'config.plugins_config'), - get(reduxState, 'config.plugin_config') - ) || emptyConfig; - - if (children.length === 0 && DefaultComponent) { - const props = plugins.getSlotComponentProps( - DefaultComponent, - reduxState, - this.getSlotProps(this.props), - queryData - ); - children = ; - } - + let children = this.props.slotElements; if (childFactory) { children = children.map(childFactory); } - // console.log('pluginsConfig', pluginsConfig); - return ( ({ - reduxState: state, + debug: get(state, 'config.plugins_config.debug'), }); -export default connect(mapStateToProps, null)(Slot); +export default compose( + withCompatPassthrough(omitProps), + withSlotElements({ + slot: props => props.fill, + size: props => props.size, + defaultComponent: props => props.defaultComponent, + }), + connect(mapStateToProps, null) +)(Slot); diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js index 7ed114b73..26d69f8e7 100644 --- a/client/coral-framework/hocs/index.js +++ b/client/coral-framework/hocs/index.js @@ -12,6 +12,13 @@ export { default as withForgotPassword } from './withForgotPassword'; export { default as withSetUsername } from './withSetUsername'; export { default as withPopupAuthHandler } from './withPopupAuthHandler'; export { default as withEnumValues } from './withEnumValues'; +export { default as withCompatPassthrough } from './withCompatPassthrough'; +export { default as withSlotElements } from './withSlotElements'; +export { default as withVariables } from './withVariables'; +export { default as WithRefetch } from './withRefetch'; +export { default as withFetchMore } from './withFetchMore'; +export { default as withSubscribeToMore } from './withSubscribeToMore'; +export { default as withGraphQLExtension } from './withGraphQLExtension'; export { default as withResendEmailConfirmation, } from './withResendEmailConfirmation'; diff --git a/client/coral-framework/hocs/withCompatPassthrough.js b/client/coral-framework/hocs/withCompatPassthrough.js new file mode 100644 index 000000000..57589854d --- /dev/null +++ b/client/coral-framework/hocs/withCompatPassthrough.js @@ -0,0 +1,53 @@ +import withProps from 'recompose/withProps'; +import omit from 'lodash/omit'; + +function getPassthrough(props, omitProps) { + const slotProps = omit(props, [...omitProps, 'passthrough']); + + // @Deprecated + if (process.env.NODE_ENV !== 'production') { + if (Object.keys(slotProps).length) { + /* eslint-disable no-console */ + console.warn( + `Slot '${ + props.fill + }' passing through unknown props is deprecated, please use 'passthrough' instead`, + slotProps + ); + /* eslint-enable no-console */ + } + } + + if (props.passthrough) { + return props.passthrough; + } + + if (props.queryData) { + if (process.env.NODE_ENV !== 'production') { + /* eslint-disable no-console */ + console.warn( + `Slot '${ + props.fill + }' property 'queryData' is deprecated, please use 'passthrough' instead` + ); + /* eslint-enable no-console */ + } + return { + ...props.queryData, + ...slotProps, + }; + } + + return slotProps; +} + +/** + * @Deprecated + * withCompatPassthrough is a compatibility HOC that supports our old + * API which puts unknown props and `queryData` to `passhtrough` to be + * used with HOC `withSlotElements`. + */ +export default omitProps => + withProps(props => ({ + passthrough: getPassthrough(props, omitProps), + })); diff --git a/client/coral-framework/hocs/withFetchMore.js b/client/coral-framework/hocs/withFetchMore.js new file mode 100644 index 000000000..cc2106436 --- /dev/null +++ b/client/coral-framework/hocs/withFetchMore.js @@ -0,0 +1,27 @@ +import React from 'react'; +import hoistStatics from 'recompose/hoistStatics'; +import { Subscriber } from 'react-broadcast'; +import get from 'lodash/get'; + +/** + * WithFetchMore provides a property `fetchMore` to the wrapped component. + * Calling `fetchMore` is the same as calling `data.fetchMore` from the + * Apollo React API. + */ +export default hoistStatics(WrappedComponent => { + class WithFetchMore extends React.Component { + render() { + return ( + + {data => ( + + )} + + ); + } + } + return WithFetchMore; +}); diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js index f98411de1..98ab76922 100644 --- a/client/coral-framework/hocs/withFragments.js +++ b/client/coral-framework/hocs/withFragments.js @@ -6,27 +6,21 @@ import { getShallowChanges } from 'coral-framework/utils'; import PropTypes from 'prop-types'; import union from 'lodash/union'; -// TODO: Should not depend on `props.data` -// Currently necessary because of this https://github.com/apollographql/graphql-anywhere/issues/38 -function filter(doc, data, variables) { +function filter(doc, data) { const resolver = (fieldName, root, args, context, info) => { return root[info.resultKey]; }; - return graphql(resolver, doc, data, null, variables); + return graphql(resolver, doc, data, null, null, { includeAll: true }); } -// filterProps returns only the property as defined in the fragments. -// TODO: Should not depend on `props.data` function filterProps(props, fragments) { const filtered = {}; Object.keys(fragments).forEach(key => { - if (!(key in props)) { + if (!(key in props) || props[key] === undefined) { return; } - filtered[key] = props.data - ? filter(fragments[key], props[key], props.data.variables) - : props[key]; + filtered[key] = filter(fragments[key], props[key]); }); return filtered; } diff --git a/client/coral-framework/hocs/withGraphQLExtension.js b/client/coral-framework/hocs/withGraphQLExtension.js new file mode 100644 index 000000000..c44a94e47 --- /dev/null +++ b/client/coral-framework/hocs/withGraphQLExtension.js @@ -0,0 +1,19 @@ +import React from 'react'; +import hoistStatics from 'recompose/hoistStatics'; + +/** + * WithGraphQLExtension adds graphql configuration to the + * GraphQL registry, only works on Components used + * directly in a Slot. + */ +export default extension => + hoistStatics(WrappedComponent => { + class WithGraphQLExtension extends React.Component { + render() { + return ; + } + } + + WrappedComponent.graphqlExtension = extension; + return WithGraphQLExtension; + }); diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index c4b9074b4..77fb7380e 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -11,6 +11,8 @@ import { getOperationName } from 'apollo-client/queries/getFromAST'; import throttle from 'lodash/throttle'; import get from 'lodash/get'; import { notify } from 'coral-framework/actions/notification'; +import { Broadcast } from 'react-broadcast'; +import { compose } from 'recompose'; const withSkipOnErrors = reducer => (prev, action, ...rest) => { if ( @@ -43,6 +45,15 @@ function networkStatusToString(networkStatus) { } } +// When wrapped broadcast all data changes to channel "queryData". +const withBroadcaster = WrappedComponent => props => ( + /* eslint-disable react/prop-types */ + + + + /* eslint-enable react/prop-types */ +); + const createHOC = (document, config, { notifyOnError = true }) => hoistStatics(WrappedComponent => { return class WithQuery extends React.Component { @@ -283,6 +294,7 @@ const createHOC = (document, config, { notifyOnError = true }) => props: args => { const nextData = this.nextData(args.data); const { root } = separateDataAndRoot(args.data); + if (config.props) { // Custom props, in this case we just pass the wrapped args to it. return config.props({ @@ -323,10 +335,13 @@ const createHOC = (document, config, { notifyOnError = true }) => if (!this.memoized) { this.resolvedDocument = this.resolveDocument(document); this.name = getDefinitionName(this.resolvedDocument); - this.memoized = graphql(this.resolvedDocument, { - ...this.wrappedConfig, - options: this.wrappedOptions, - })(WrappedComponent); + this.memoized = compose( + graphql(this.resolvedDocument, { + ...this.wrappedConfig, + options: this.wrappedOptions, + }), + withBroadcaster + )(WrappedComponent); } return this.memoized; }; diff --git a/client/coral-framework/hocs/withRefetch.js b/client/coral-framework/hocs/withRefetch.js new file mode 100644 index 000000000..04848fe11 --- /dev/null +++ b/client/coral-framework/hocs/withRefetch.js @@ -0,0 +1,23 @@ +import React from 'react'; +import hoistStatics from 'recompose/hoistStatics'; +import { Subscriber } from 'react-broadcast'; +import get from 'lodash/get'; + +/** + * WithRefetch provides a property `refetch` to the wrapped component. + * Calling refetch will perform a refetch of the parent Query. + */ +export default hoistStatics(WrappedComponent => { + class WithRefetch extends React.Component { + render() { + return ( + + {data => ( + + )} + + ); + } + } + return WithRefetch; +}); diff --git a/client/coral-framework/hocs/withSlotElements.js b/client/coral-framework/hocs/withSlotElements.js new file mode 100644 index 000000000..a4b39c907 --- /dev/null +++ b/client/coral-framework/hocs/withSlotElements.js @@ -0,0 +1,195 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import isEqual from 'lodash/isEqual'; +import { getShallowChanges } from 'coral-framework/utils'; +import { compose } from 'recompose'; +import hoistStatics from 'recompose/hoistStatics'; +import isFunction from 'lodash/isFunction'; + +function resolvePrimitiveOrFunction(primitiveOrFunction, props) { + if (isFunction(primitiveOrFunction)) { + return primitiveOrFunction(props); + } + return primitiveOrFunction; +} + +const createHOC = ({ + slot, + defaultComponent = null, + passthroughPropName = 'passthrough', + size = null, + propName = 'slotElements', +}) => + hoistStatics(WrappedComponent => { + return class withSlotElements extends React.Component { + static contextTypes = { + plugins: PropTypes.object, + }; + + static propTypes = { + reduxState: PropTypes.object, + }; + + getSlots(props = this.props) { + const tmp = resolvePrimitiveOrFunction(slot, props); + if (Array.isArray(tmp)) { + return tmp; + } + return [tmp]; + } + + getDefaultComponents(props = this.props, fill = 1) { + const tmp = resolvePrimitiveOrFunction(defaultComponent, props); + if (Array.isArray(tmp)) { + return tmp; + } + return new Array(fill).fill(tmp); + } + + getSizes(props = this.props, fill = 1) { + const tmp = resolvePrimitiveOrFunction(size, props); + if (Array.isArray(tmp)) { + return tmp; + } + return new Array(fill).fill(tmp); + } + + getPassthrough(props = this.props) { + return passthroughPropName ? props[passthroughPropName] : null; + } + + shouldComponentUpdate(next) { + // Prevent Slot from rerendering when only reduxState has changed and + // it does not result in a change of slot children. + const changes = getShallowChanges(this.props, next); + + // Handle special `passthrough` props. + if (passthroughPropName) { + const passthroughIndex = changes.indexOf(passthroughPropName); + if (passthroughIndex !== -1) { + if (!this.props[passthroughPropName] || next[passthroughPropName]) { + return true; + } + if ( + getShallowChanges( + this.props[passthroughPropName], + next[passthroughPropName] + ).lenght === 0 + ) { + changes.splice(passthroughIndex, 1); + } + } + } + + if (changes.length === 1 && changes[0] === 'reduxState') { + const prevChildrenKeys = this.getSlotElements(this.props).map( + child => child.key + ); + const nextChildrenKeys = this.getSlotElements(next).map( + child => child.key + ); + return !isEqual(prevChildrenKeys, nextChildrenKeys); + } + + // Prevent Slot from rerendering when no props has shallowly changed. + return changes.length !== 0; + } + + /** + * Returns slot elements for configured slots. If only one slot is given + * slot elements are returned directly. If more than one slot is specified + * returns an array of slot elements. + */ + getSlotElements(props = this.props) { + const { plugins } = this.context; + const slots = this.getSlots(props); + const sizes = this.getSizes(props, slots.length); + const defaultComponents = this.getDefaultComponents( + props, + slots.length + ); + const slotPassthrough = this.getPassthrough(props); + + if (process.env.NODE_ENV !== 'production') { + if (slotPassthrough.data && slotPassthrough.data.refetch) { + /* eslint-disable no-console */ + console.warn( + 'Slots no longer need `data` property.', + 'Plugins can use the new HOCs `withRefetch`,', + '`withFetchMore`, `withSubscribeToMore` and `withVariables` instead.', + 'Affected slots: ', + slots + ); + /* eslint-enable no-console */ + } + } + + const elements = []; + slots.forEach((s, i) => { + const DefaultComponent = defaultComponents[i]; + const size = sizes[i]; + const slotElements = plugins.getSlotElements( + s, + props.reduxState, + slotPassthrough, + { size } + ); + + if (slotElements.length === 0 && DefaultComponent) { + const p = plugins.getSlotComponentProps( + DefaultComponent, + props.reduxState, + slotPassthrough + ); + slotElements.push(); + } + + elements.push(slotElements); + }); + + if (elements.length === 1) { + return elements[0]; + } + + return elements; + } + + render() { + const { reduxState: _a, ...rest } = this.props; + const slotElements = this.getSlotElements(); + const props = { + [propName]: slotElements, + ...rest, + }; + return ; + } + }; + }); + +const mapStateToProps = state => ({ + reduxState: state, +}); + +/** + * Exports a HOC that provides a property `slotElements`. + * @param {Object} [options] configuration + * @param {string|array} [options.slot] Slot name or Array of Slot Names + * @param {element|array} [options.defaultComponent] Default Components or Array of such + * @param {number|array} [options.size] Slot size or an Array of slot size + * @param {string} [options.passthroughPropName] The property to find the passthrough prop + * @param {string} [options.propName] New property name, defaults to `slotElements` + * + * @return {func} Returns a HOC that per default provides the property `slotElements` with an + * Array of Slot Elements or in case of multiple slots, an Array of Slot Element + * Arrays. + * + * Example: + * withSlotElements({ + * slot: 'awesomeSlot', + * size: 1, + * })(MyComponent); + */ +export default settings => { + return compose(connect(mapStateToProps, null), createHOC(settings)); +}; diff --git a/client/coral-framework/hocs/withSubscribeToMore.js b/client/coral-framework/hocs/withSubscribeToMore.js new file mode 100644 index 000000000..67ffe9ae2 --- /dev/null +++ b/client/coral-framework/hocs/withSubscribeToMore.js @@ -0,0 +1,27 @@ +import React from 'react'; +import hoistStatics from 'recompose/hoistStatics'; +import { Subscriber } from 'react-broadcast'; +import get from 'lodash/get'; + +/** + * WithSubscribeToMore provides a property `subscribeToMore` to the wrapped component. + * Calling `subscribeToMore` is the same as calling `data.subscribeToMore` from the + * Apollo React API. + */ +export default hoistStatics(WrappedComponent => { + class WithSubscribeToMore extends React.Component { + render() { + return ( + + {data => ( + + )} + + ); + } + } + return WithSubscribeToMore; +}); diff --git a/client/coral-framework/hocs/withVariables.js b/client/coral-framework/hocs/withVariables.js new file mode 100644 index 000000000..886742f36 --- /dev/null +++ b/client/coral-framework/hocs/withVariables.js @@ -0,0 +1,26 @@ +import React from 'react'; +import hoistStatics from 'recompose/hoistStatics'; +import { Subscriber } from 'react-broadcast'; +import get from 'lodash/get'; + +/** + * WithVariables provides a property `variables` to the wrapped component. + * These are the variables of the parent Query. + */ +export default hoistStatics(WrappedComponent => { + class WithVariables extends React.Component { + render() { + return ( + + {data => ( + + )} + + ); + } + } + return WithVariables; +}); diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js index c411cca98..3bc9bc5bc 100644 --- a/client/coral-framework/services/plugins.js +++ b/client/coral-framework/services/plugins.js @@ -7,6 +7,7 @@ import isEmpty from 'lodash/isEmpty'; import flatten from 'lodash/flatten'; import mapValues from 'lodash/mapValues'; import get from 'lodash/get'; +import values from 'lodash/values'; import { getDisplayName } from 'coral-framework/helpers/hoc'; import camelize from '../helpers/camelize'; @@ -67,71 +68,102 @@ function addMetaDataToSlotComponents(plugins) { }); } +// @Deprecated +const showPluginConfigDeprecationWarningOnce = (() => { + let shown = false; + return () => { + if (!shown) { + shown = true; + console.warn( + `deprecation warning: config.plugin_config will be phased out soon, please replace calls from config.plugin_config to config.plugins_config` + ); + } + }; +})(); + +/** + * getSlotComponentProps calculate the props we would pass to the slot component. + * query datas are only passed to the component if it is defined in `component.fragments`. + */ +function getSlotComponentProps(component, reduxState, props, queryData) { + // @Deprecated + const pluginsConfig = + get(reduxState, 'config.plugins_config') || + get(reduxState, 'config.plugin_config') || + emptyConfig; + + if ( + process.env.NODE_ENV !== 'production' && + !!get(reduxState, 'config.plugin_config') + ) { + showPluginConfigDeprecationWarningOnce(); + } + + console.log('slot plugins_config', get(reduxState, 'config.plugins_config')); + + const debugProps = pluginsConfig.debug + ? { + 'data-slot-name': props.fill, + } + : {}; + return { + ...props, + ...debugProps, + config: pluginsConfig, + ...(component.fragments + ? pick(queryData, Object.keys(component.fragments)) + : withWarnings(component, queryData)), + }; +} + +/** + * splitProps detects objects coming from the query and + * returns `queryData` and `rest`. We use `__typename` + * in order to detect objects from the query. + */ +function splitProps(props) { + const rest = { ...props }; + const queryData = {}; + Object.keys(props).forEach(k => { + if ( + get(props[k], `__typename`) || + get(props[k], `0.__typename`) // Arrays + ) { + queryData[k] = props[k]; + delete rest[k]; + } + }); + return { queryData, rest }; +} + class PluginsService { constructor(plugins) { this.plugins = plugins; addMetaDataToSlotComponents(plugins); } - isSlotEmpty(slot, reduxState, props = {}, queryData = {}) { - return ( - this.getSlotElements(slot, reduxState, props, queryData).length === 0 - ); + isSlotEmpty(slot, reduxState, props = {}) { + return this.getSlotElements(slot, reduxState, props).length === 0; } /** - * getSlotComponentProps calculate the props we would pass to the slot component. - * query datas are only passed to the component if it is defined in `component.fragments`. + * Returns props that would pass to the given slot component. */ - showPluginsConfigWarning = true; - - getSlotComponentProps(component, reduxState, props, queryData) { - if ( - process.env.NODE_ENV !== 'production' && - !!get(reduxState, 'config.plugin_config') && - this.showPluginsConfigWarning - ) { - console.warn( - `deprecation warning: config.plugin_config will be phased out soon, please replace calls from config.plugin_config to config.plugins_config` - ); - this.showPluginsConfigWarning = false; - } - - console.log( - 'slot plugins_config', - get(reduxState, 'config.plugins_config') - ); - - // @Deprecated plugin_config - const pluginsConfig = - merge( - get(reduxState, 'config.plugins_config'), - get(reduxState, 'config.plugin_config') - ) || emptyConfig; - - const debugProps = pluginsConfig.debug - ? { - 'data-slot-name': props.fill, - } - : {}; - - return { - ...props, - ...debugProps, - config: pluginsConfig, - ...(component.fragments - ? pick(queryData, Object.keys(component.fragments)) - : withWarnings(component, queryData)), - }; + getSlotComponentProps(component, reduxState, props) { + const { queryData, rest } = splitProps(props); + return getSlotComponentProps(component, reduxState, rest, queryData); } /** * Returns React Elements for given slot. */ - getSlotElements(slot, reduxState, props = {}, queryData = {}, options = {}) { + getSlotElements(slot, reduxState, props = {}, options = {}) { const pluginsConfig = - get(reduxState, 'config.plugins_config') || emptyConfig; - const { slotSize = 0 } = options; + get(reduxState, 'config.plugins_config') || + get(reduxState, 'config.plugin_config') || + emptyConfig; + const { size = 0 } = options; + const { queryData, rest } = splitProps(props); const isDisabled = component => { if ( @@ -144,10 +176,10 @@ class PluginsService { // Check if component is excluded. if (component.isExcluded) { - let resolvedProps = this.getSlotComponentProps( + let resolvedProps = getSlotComponentProps( component, reduxState, - props, + rest, queryData ); if (component.mapStateToProps) { @@ -168,15 +200,15 @@ class PluginsService { .map(o => o.module.slots[slot]) ); - if (slotSize > 0 && slots.length > slotSize) { + if (size > 0 && slots.length > size) { console.warn( - `Slot[${slot}] supports a maximum of ${slotSize} plugins providing slots, got ${ + `Slot[${slot}] supports a maximum of ${size} plugins providing slots, got ${ slots.length - }, will only use the first ${slotSize}` + }, will only use the first ${size}` ); } - return (slotSize > 0 ? slots.slice(0, slotSize) : slots) + return (size > 0 ? slots.slice(0, size) : slots) .map((component, i) => ({ component, disabled: isDisabled(component), @@ -186,12 +218,7 @@ class PluginsService { .map(({ component, key }) => React.createElement(component, { key, - ...this.getSlotComponentProps( - component, - reduxState, - props, - queryData - ), + ...getSlotComponentProps(component, reduxState, rest, queryData), }) ); } @@ -217,9 +244,16 @@ class PluginsService { } getGraphQLExtensions() { - return this.plugins - .map(o => pick(o.module, ['mutations', 'queries', 'fragments'])) - .filter(o => !isEmpty(o)); + return flatten( + this.plugins.map(o => { + const extension = pick(o.module, ['mutations', 'queries', 'fragments']); + // Include extension defined in Slot Components. + const slotComponentExtensions = !o.module.slots + ? [] + : flatten(values(o.module.slots)).map(cmp => cmp.graphqlExtension); + return [extension, ...slotComponentExtensions]; + }) + ).filter(o => !isEmpty(o)); } getModQueueConfigs() { diff --git a/locales/ar.yml b/locales/ar.yml index dccda3826..3c94c06f0 100644 --- a/locales/ar.yml +++ b/locales/ar.yml @@ -352,7 +352,7 @@ ar: personal_info: "هذا التعليق يكشف عن معلومات تعريف شخصية" post: نشر profile: الملف الشخصي - profile_settings: "إعدادات الملف الشخصي" + profile_settings: "إعدادات" reply: رد report: أبلغ report_notif: "شكرا على الإبلاغ عن هذا التعليق. تم إبلاغ فريق الإشراف لدينا وسيراجعه قريبًا." diff --git a/locales/da.yml b/locales/da.yml index 2238a36fb..9618b2f58 100644 --- a/locales/da.yml +++ b/locales/da.yml @@ -16,7 +16,7 @@ da: send: "Send" notify_ban_headline: "Underret brugeren om bannet" notify_ban_description: "Dette meddeler brugeren via e-mail, at der er blevet bandlyst fra fællesskabet" - email_message_ban: "Kære {0},\n\nEn person med adgang til din konto har overtrådt vores retningslinjer for fællesskabet. Som følge heraf er din konto blevet forbudt. Du vil ikke længere kunne kommentere, like eller rapportere kommentarer. Hvis du mener at dette er sket ved en fejl, bedes du kontakte vores fællesskabsteam." + email_message_ban: "Kære {0},\n\nEn person med adgang til din konto har overtrådt vores retningslinjer for fællesskabet. Som følge heraf er din konto blevet forbudt. Du vil ikke længere kunne kommentere, like eller rapportere kommentarer. Hvis du mener at dette er sket ved en fejl, bedes du kontakte vores fællesskabsteam." bio_offensive: "Denne biografi er stødende" cancel: "Afbryd" confirm_email: @@ -256,7 +256,7 @@ da: label: "Nyt brugernavn" msg: "Din konto er midlertidigt suspenderet, fordi dit brugernavn er blevet anset for upassende. For at gendanne din konto skal du indtaste et nyt brugernavn. Kontakt os venligst, hvis du har spørgsmål." changed_name: - msg: "Dit brugernavn ændring er nuværende under gennemgang af vores modereringsteam." + msg: "Dit brugernavn ændring er nuværende under gennemgang af vores modereringsteam." my_comments: "Mine kommentarer" my_profile: "Min profil" new_count: "Se {0} mere {1}" @@ -350,7 +350,7 @@ da: personal_info: "Denne kommentar afslører personligt identificerbare oplysninger" post: "Post" profile: "Profil" - profile_settings: "Profilindstillinger" + profile_settings: "Indstillinger" reply: "Svar" report: "Rapporter" report_notif: "Tak fordi du rapporterede denne kommentar. Vores modereringsteam har fået besked, og vil gennemgå det inden for kort tid." diff --git a/locales/de.yml b/locales/de.yml index b5ced1096..972c2735b 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -349,7 +349,7 @@ de: personal_info: "Dieser Kommentar enthält zu viel personenbezogene Daten" post: Post # Kontext? profile: Profil - profile_settings: "Profil-Einstellungen" + profile_settings: "Einstellungen" reply: Antworten report: Melden report_notif: "Vielen Dank für Ihre Meldung. Unsere Moderatoren wurden informiert und werden sich in Kürze darum kümmern." @@ -397,7 +397,7 @@ de: suspend_user: "Nutzer vorübergehend sperren" email_message_suspend: "Sehr geehrte/r {0}, entsprechend der Community-Richtlinien von {1} wurde Ihr Konto vorübergehend gesperrt. Während der Sperrung können Sie weder kommentieren noch andere Aktionen ausführen. Nehmen Sie {2} wieder an der Diskussion teil." title_notify: "Den Nutzer über die vorübergehende Kontosperrung informieren" - notify_suspend_until: "Nutzer {0} wurde vorübergehend gesperrt. Diese Sperrung endet automatisch {1}." + notify_suspend_until: "Nutzer {0} wurde vorübergehend gesperrt. Diese Sperrung endet automatisch {1}." description_notify: "Während der Sperrung hat der Nutzer keinen Zugriff auf das Konto." write_message: "Nachricht verfassen" send: Senden diff --git a/locales/en.yml b/locales/en.yml index d540e8c1e..59689d1b1 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -352,7 +352,7 @@ en: personal_info: "This comment reveals personally identifiable information" post: Post profile: Profile - profile_settings: "Profile Settings" + profile_settings: "Settings" reply: Reply report: Report report_notif: "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly." diff --git a/locales/es.yml b/locales/es.yml index 8edd1f8fc..ba6cb8ad3 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -351,7 +351,7 @@ es: personal_info: "Este comentario muestra información personal" post: Publicar profile: Perfil - profile_settings: "Configuración del Perfil" + profile_settings: "Configuración" reply: Responder report: Reportar report_notif: "Gracias por reportar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar." diff --git a/locales/fr.yml b/locales/fr.yml index fb2b3be99..dd14bea81 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -351,7 +351,7 @@ fr: personal_info: "Ce commentaire révèle des informations personnelles identifiables" post: Publier profile: Profil - profile_settings: "Paramètres du profil" + profile_settings: "Paramètres" reply: Répondre report: Signaler report_notif: "Merci de signaler ce commentaire. Notre équipe de modération a é té informée." diff --git a/locales/nl_NL.yml b/locales/nl_NL.yml index a60268e42..aa80f58be 100644 --- a/locales/nl_NL.yml +++ b/locales/nl_NL.yml @@ -65,7 +65,7 @@ nl_NL: notify_approved: '{0} heeft gebruikersnaam {1} goedgekeurd' notify_rejected: '{0} heeft gebruikersnaam {1} afgekeurd' notify_flagged: '{0} heeft gebruikersnaam {1} gerapporteerd' - notify_changed: 'gebruiker {0} heeft zijn/haar gebruikersnaam veranderd naar {1}' + notify_changed: 'gebruiker {0} heeft zijn/haar gebruikersnaam veranderd naar {1}' community: account_creation_date: "Aanmaakdatum account" active: Actief @@ -350,7 +350,7 @@ nl_NL: personal_info: "Deze reactie onthult persoonlijk identificeerbare gegevens." post: Plaats profile: Profiel - profile_settings: "Profiel instellingen" + profile_settings: "Instellingen" reply: Beantwoord report: Rapporteer report_notif: "Dank voor het rapporteren van deze reactie. Deze wordt zo snel mogelijk gemodereerd." diff --git a/locales/pt_BR.yml b/locales/pt_BR.yml index b19a3789b..86267d51d 100644 --- a/locales/pt_BR.yml +++ b/locales/pt_BR.yml @@ -349,7 +349,7 @@ pt_BR: personal_info: "Este comentário revela informações de identificação pessoal" post: Publicar profile: Perfil - profile_settings: "Configurações de perfil" + profile_settings: "Configurações" reply: Responder report: Denunciar report_notif: "Obrigado por denunciar este comentário. Nossa equipe de moderação foi notificada e irá revisá-la em breve." diff --git a/locales/zh_CN.yml b/locales/zh_CN.yml index 31d6623a3..59e028eb7 100644 --- a/locales/zh_CN.yml +++ b/locales/zh_CN.yml @@ -351,7 +351,7 @@ zh_CN: personal_info: "这个评论透露了可确定个人身份的信息" post: "发表" profile: "资料" - profile_settings: "资料设定" + profile_settings: "设置" reply: "回复" report: "举报" report_notif: "感谢您举报此评论。我们的审核小组已经收到通知,并会很快进行审查。" diff --git a/locales/zh_TW.yml b/locales/zh_TW.yml index 8f2f54dfd..1b131f636 100644 --- a/locales/zh_TW.yml +++ b/locales/zh_TW.yml @@ -351,7 +351,7 @@ zh_TW: personal_info: "該評論洩露了個人身份資訊" post: 帖子 profile: 概況 - profile_settings: "概況設置" + profile_settings: "設置" reply: 回覆 report: 舉報 report_notif: "感謝您舉報此評論。我們的審核小組已經收到通知,並會很快進行審查。" diff --git a/package.json b/package.json index 0aa4f247b..93b9d0205 100644 --- a/package.json +++ b/package.json @@ -159,6 +159,7 @@ "query-string": "^5.0.0", "react": "^15.4.2", "react-apollo": "^1.4.12", + "react-broadcast": "^0.6.2", "react-dom": "^15.4.2", "react-input-autosize": "^1.1.4", "react-mdl": "^1.11.0", diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js index 35899a418..215641f32 100644 --- a/plugin-api/beta/client/hocs/index.js +++ b/plugin-api/beta/client/hocs/index.js @@ -13,6 +13,11 @@ export { withResendEmailConfirmation, withSetUsername, withEnumValues, + withVariables, + withFetchMore, + withSubscribeToMore, + withRefetch, + withGraphQLExtension, } from 'coral-framework/hocs'; export { withIgnoreUser, @@ -21,3 +26,4 @@ export { withStopIgnoringUser, withSetCommentStatus, } from 'coral-framework/graphql/mutations'; +export { compose } from 'recompose'; diff --git a/plugins/talk-plugin-author-menu/client/components/AuthorName.js b/plugins/talk-plugin-author-menu/client/components/AuthorName.js index 5d6e835e3..204d46e52 100644 --- a/plugins/talk-plugin-author-menu/client/components/AuthorName.js +++ b/plugins/talk-plugin-author-menu/client/components/AuthorName.js @@ -1,14 +1,13 @@ import React from 'react'; +import PropTypes from 'prop-types'; import Menu from './Menu'; import styles from './AuthorName.css'; import { ClickOutside } from 'plugin-api/beta/client/components'; import cn from 'classnames'; -export default ({ - data, - root, - asset, - comment, +const AuthorName = ({ + slotPassthrough, + username, contentSlot, menuVisible, toggleMenu, @@ -21,18 +20,23 @@ export default ({ className={cn(styles.button, 'talk-plugin-author-menu-button')} onClick={toggleMenu} > - {comment.user.username} + {username} {menuVisible && ( - + )}
    ); }; + +AuthorName.propTypes = { + slotPassthrough: PropTypes.object.isRequired, + username: PropTypes.string.isRequired, + menuVisible: PropTypes.bool.isRequired, + toggleMenu: PropTypes.func.isRequired, + hideMenu: PropTypes.func.isRequired, + contentSlot: PropTypes.string, +}; + +export default AuthorName; diff --git a/plugins/talk-plugin-author-menu/client/components/Menu.js b/plugins/talk-plugin-author-menu/client/components/Menu.js index e041720eb..6c46fb1c3 100644 --- a/plugins/talk-plugin-author-menu/client/components/Menu.js +++ b/plugins/talk-plugin-author-menu/client/components/Menu.js @@ -1,17 +1,14 @@ import React from 'react'; +import PropTypes from 'prop-types'; import styles from './Menu.css'; import { Slot } from 'plugin-api/beta/client/components'; import cn from 'classnames'; -export default ({ data, root, asset, comment, contentSlot }) => { +const Menu = ({ slotPassthrough, contentSlot }) => { if (contentSlot) { return (
    - +
    ); } @@ -21,15 +18,20 @@ export default ({ data, root, asset, comment, contentSlot }) => {
    ); }; + +Menu.propTypes = { + slotPassthrough: PropTypes.object.isRequired, + contentSlot: PropTypes.string, +}; + +export default Menu; diff --git a/plugins/talk-plugin-author-menu/client/containers/AuthorName.js b/plugins/talk-plugin-author-menu/client/containers/AuthorName.js index b06dbf1bb..e385058c4 100644 --- a/plugins/talk-plugin-author-menu/client/containers/AuthorName.js +++ b/plugins/talk-plugin-author-menu/client/containers/AuthorName.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { connect, withFragments } from 'plugin-api/beta/client/hocs'; import { bindActionCreators } from 'redux'; import AuthorName from '../components/AuthorName'; @@ -47,21 +48,39 @@ class AuthorNameContainer extends React.Component { }; render() { + const { + root, + asset, + comment, + contentSlot, + showMenuForComment, + } = this.props; + + const slotPassthrough = { root, asset, comment }; + return ( ); } } +AuthorNameContainer.propTypes = { + root: PropTypes.object.isRequired, + asset: PropTypes.object.isRequired, + comment: PropTypes.object.isRequired, + contentSlot: PropTypes.string, + showMenuForComment: PropTypes.string, + openMenu: PropTypes.func.isRequired, + closeMenu: PropTypes.func.isRequired, +}; + const slots = ['authorMenuInfos', 'authorMenuActions']; const mapStateToProps = ({ talkPluginAuthorMenu: state }) => ({ diff --git a/plugins/talk-plugin-featured-comments/client/components/Comment.js b/plugins/talk-plugin-featured-comments/client/components/Comment.js index ce4aa7a5c..589e6e2a6 100644 --- a/plugins/talk-plugin-featured-comments/client/components/Comment.js +++ b/plugins/talk-plugin-featured-comments/client/components/Comment.js @@ -18,8 +18,8 @@ class Comment extends React.Component { }; render() { - const { comment, asset, root, data } = this.props; - const queryData = { comment, asset, root }; + const { comment, asset, root } = this.props; + const slotPassthrough = { comment, asset, root }; return (
    @@ -36,8 +35,7 @@ class Comment extends React.Component { className={cn(styles.username, `${pluginName}-comment-username`)} fill="commentAuthorName" defaultComponent={CommentAuthorName} - queryData={queryData} - data={data} + passthrough={slotPassthrough} inline /> @@ -45,9 +43,7 @@ class Comment extends React.Component { fill="commentTimestamp" defaultComponent={CommentTimestamp} className={cn(styles.timestamp, `${pluginName}-comment-timestamp`)} - created_at={comment.created_at} - data={data} - queryData={queryData} + passthrough={{ created_at: comment.created_at, ...slotPassthrough }} inline />
    @@ -62,19 +58,11 @@ class Comment extends React.Component { > - +
    - this.props.data.subscribeToMore(config) + this.props.subscribeToMore(config) ); } @@ -60,4 +61,4 @@ const COMMENT_UNFEATURED_SUBSCRIPTION = gql` } `; -export default ModIndicatorSubscription; +export default compose(withSubscribeToMore)(ModIndicatorSubscription); diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js index 6d0fdd811..d91d69304 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js +++ b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js @@ -6,6 +6,11 @@ import { getDefinitionName } from 'coral-framework/utils'; import truncate from 'lodash/truncate'; import t from 'coral-framework/services/i18n'; import { subscriptionFields } from 'coral-admin/src/routes/Moderation/graphql'; +import { + compose, + withSubscribeToMore, + withVariables, +} from 'plugin-api/beta/client/hocs'; function prepareNotificationText(text) { return truncate(text, { length: 50 }).replace('\n', ' '); @@ -19,7 +24,7 @@ class ModSubscription extends React.Component { { document: COMMENT_FEATURED_SUBSCRIPTION, variables: { - assetId: this.props.data.variables.asset_id, + assetId: this.props.variables.asset_id, }, updateQuery: ( prev, @@ -39,7 +44,7 @@ class ModSubscription extends React.Component { { document: COMMENT_UNFEATURED_SUBSCRIPTION, variables: { - assetId: this.props.data.variables.asset_id, + assetId: this.props.variables.asset_id, }, updateQuery: ( prev, @@ -62,7 +67,7 @@ class ModSubscription extends React.Component { }, ]; this.subscriptions = configs.map(config => - this.props.data.subscribeToMore(config) + this.props.subscribeToMore(config) ); } @@ -111,4 +116,8 @@ const mapStateToProps = state => ({ user: state.auth.user, }); -export default connect(mapStateToProps, null)(ModSubscription); +export default compose( + connect(mapStateToProps, null), + withVariables, + withSubscribeToMore +)(ModSubscription); diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js index 297f5aedf..b3f44b429 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js @@ -2,7 +2,12 @@ import React from 'react'; import { bindActionCreators } from 'redux'; import { compose, gql } from 'react-apollo'; import TabPane from '../components/TabPane'; -import { withFragments, connect } from 'plugin-api/beta/client/hocs'; +import { + withFragments, + connect, + withFetchMore, + withVariables, +} from 'plugin-api/beta/client/hocs'; import Comment from '../containers/Comment'; import { viewComment } from 'coral-embed-stream/src/actions/stream'; import { @@ -13,15 +18,15 @@ import update from 'immutability-helper'; class TabPaneContainer extends React.Component { loadMore = () => { - return this.props.data.fetchMore({ + return this.props.fetchMore({ query: LOAD_MORE_QUERY, variables: { limit: 5, cursor: this.props.asset.featuredComments.endCursor, asset_id: this.props.asset.id, - sortOrder: this.props.data.variables.sortOrder, - sortBy: this.props.data.variables.sortBy, - excludeIgnored: this.props.data.variables.excludeIgnored, + sortOrder: this.props.variables.sortOrder, + sortBy: this.props.variables.sortBy, + excludeIgnored: this.props.variables.excludeIgnored, }, updateQuery: (previous, { fetchMoreResult: { comments } }) => { const updated = update(previous, { @@ -86,6 +91,8 @@ const mapDispatchToProps = dispatch => const enhance = compose( connect(null, mapDispatchToProps), + withFetchMore, + withVariables, withFragments({ root: gql` fragment TalkFeaturedComments_TabPane_root on RootQuery { diff --git a/plugins/talk-plugin-flag-details/client/components/FlagDetails.js b/plugins/talk-plugin-flag-details/client/components/FlagDetails.js index 607ba48a2..7900f5b50 100644 --- a/plugins/talk-plugin-flag-details/client/components/FlagDetails.js +++ b/plugins/talk-plugin-flag-details/client/components/FlagDetails.js @@ -10,7 +10,7 @@ import { class FlagDetails extends Component { render() { - const { comment: { actions }, more, data, root, comment } = this.props; + const { comment: { actions }, more, root, comment } = this.props; const flagActions = actions && actions.filter(a => a.__typename === 'FlagAction'); @@ -26,7 +26,7 @@ class FlagDetails extends Component { }, {}); const reasons = Object.keys(summaries); - const queryData = { + const slotPassthrough = { root, comment, }; @@ -52,12 +52,11 @@ class FlagDetails extends Component { {more && ( )} @@ -68,7 +67,6 @@ class FlagDetails extends Component { FlagDetails.propTypes = { more: PropTypes.bool, - data: PropTypes.object, root: PropTypes.object, comment: PropTypes.shape({ actions: PropTypes.arrayOf( diff --git a/plugins/talk-plugin-member-since/client/index.js b/plugins/talk-plugin-member-since/client/index.js index 2091e4805..fe0b5b14b 100644 --- a/plugins/talk-plugin-member-since/client/index.js +++ b/plugins/talk-plugin-member-since/client/index.js @@ -24,7 +24,7 @@ export default { createComment: { comment: { user: { - created_at: new Date(), + created_at: new Date().toISOString(), __typename: 'User', }, __typename: 'Comment', diff --git a/plugins/talk-plugin-moderation-actions/client/components/ModerationActions.js b/plugins/talk-plugin-moderation-actions/client/components/ModerationActions.js index 68abce826..272202d9c 100644 --- a/plugins/talk-plugin-moderation-actions/client/components/ModerationActions.js +++ b/plugins/talk-plugin-moderation-actions/client/components/ModerationActions.js @@ -16,12 +16,16 @@ export default class ModerationActions extends React.Component { comment, root, asset, - data, menuVisible, toogleMenu, hideMenu, } = this.props; + const slotPassthrough = { + comment, + asset, + }; + return (
    @@ -67,7 +70,6 @@ ModerationActions.propTypes = { comment: PropTypes.object, root: PropTypes.object, asset: PropTypes.object, - data: PropTypes.object, menuVisible: PropTypes.bool, toogleMenu: PropTypes.func, hideMenu: PropTypes.func, diff --git a/plugins/talk-plugin-moderation-actions/client/containers/ModerationActions.js b/plugins/talk-plugin-moderation-actions/client/containers/ModerationActions.js index 9e0edb9b3..891d248ce 100644 --- a/plugins/talk-plugin-moderation-actions/client/containers/ModerationActions.js +++ b/plugins/talk-plugin-moderation-actions/client/containers/ModerationActions.js @@ -42,7 +42,6 @@ class ModerationActionsContainer extends React.Component { render() { return ( - props.root.me.notificationSettings.onFeatured; - - toggle = () => { - this.props.updateNotificationSettings({ - onFeatured: !this.getOnFeaturedSetting(), - }); - }; - - render() { - return ( - - {t('talk-plugin-notifications-category-featured.toggle_description')} - - ); - } -} - -ToggleContainer.propTypes = { - data: PropTypes.object, - root: PropTypes.object, - indicateOn: PropTypes.func.isRequired, - indicateOff: PropTypes.func.isRequired, - setTurnOffInputFragment: PropTypes.func.isRequired, - updateNotificationSettings: PropTypes.func.isRequired, - disabled: PropTypes.bool.isRequired, -}; - -const enhance = compose( - withFragments({ - root: gql` - fragment TalkNotificationsCategoryFeatured_Toggle_root on RootQuery { - me { - notificationSettings { - onFeatured - } - } - } - `, - }) -); - -export default enhance(ToggleContainer); diff --git a/plugins/talk-plugin-notifications-category-featured/client/graphql.js b/plugins/talk-plugin-notifications-category-featured/client/graphql.js deleted file mode 100644 index 440a6f7fa..000000000 --- a/plugins/talk-plugin-notifications-category-featured/client/graphql.js +++ /dev/null @@ -1,33 +0,0 @@ -import { gql } from 'react-apollo'; - -export default { - mutations: { - UpdateNotificationSettings: ({ - variables: { input }, - state: { auth: { user: { id } } }, - }) => ({ - update: proxy => { - if (input.onFeatured === undefined) { - return; - } - - const fragment = gql` - fragment TalkNotificationsCategoryFeatured_User_Fragment on User { - notificationSettings { - onFeatured - } - } - `; - const fragmentId = `User_${id}`; - const data = { - __typename: 'User', - notificationSettings: { - __typename: 'NotificationSettings', - onFeatured: input.onFeatured, - }, - }; - proxy.writeFragment({ fragment, id: fragmentId, data }); - }, - }), - }, -}; diff --git a/plugins/talk-plugin-notifications-category-featured/client/index.js b/plugins/talk-plugin-notifications-category-featured/client/index.js index 1e22c3a93..74c0c2aa5 100644 --- a/plugins/talk-plugin-notifications-category-featured/client/index.js +++ b/plugins/talk-plugin-notifications-category-featured/client/index.js @@ -1,11 +1,14 @@ -import Toggle from './containers/Toggle'; import translations from './translations.yml'; -import graphql from './graphql'; +import { t } from 'plugin-api/beta/client/services'; +import { createSettingsToggle } from 'talk-plugin-notifications/client/api/factories'; + +const SettingsToggle = createSettingsToggle('onFeatured', () => + t('talk-plugin-notifications-category-featured.toggle_description') +); export default { slots: { - notificationSettings: [Toggle], + notificationSettings: [SettingsToggle], }, translations, - ...graphql, }; diff --git a/plugins/talk-plugin-notifications-category-reply/client/containers/Toggle.js b/plugins/talk-plugin-notifications-category-reply/client/containers/Toggle.js deleted file mode 100644 index d261946d0..000000000 --- a/plugins/talk-plugin-notifications-category-reply/client/containers/Toggle.js +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { compose, gql } from 'react-apollo'; -import Toggle from 'talk-plugin-notifications/client/components/Toggle'; -import { t } from 'plugin-api/beta/client/services'; -import { withFragments } from 'plugin-api/beta/client/hocs'; - -class ToggleContainer extends React.Component { - constructor(props) { - super(props); - props.setTurnOffInputFragment({ onReply: false }); - - if (this.getOnReplySetting()) { - props.indicateOn(); - } - } - - componentWillReceiveProps(nextProps) { - const prevSetting = this.getOnReplySetting(this.props); - const nextSetting = this.getOnReplySetting(nextProps); - if (prevSetting && !nextSetting) { - nextProps.indicateOff(); - } else if (!prevSetting && nextSetting) { - nextProps.indicateOn(); - } - } - - getOnReplySetting = (props = this.props) => - props.root.me.notificationSettings.onReply; - - toggle = () => { - this.props.updateNotificationSettings({ - onReply: !this.getOnReplySetting(), - }); - }; - - render() { - return ( - - {t('talk-plugin-notifications-category-reply.toggle_description')} - - ); - } -} - -ToggleContainer.propTypes = { - data: PropTypes.object, - root: PropTypes.object, - indicateOn: PropTypes.func.isRequired, - indicateOff: PropTypes.func.isRequired, - setTurnOffInputFragment: PropTypes.func.isRequired, - updateNotificationSettings: PropTypes.func.isRequired, - disabled: PropTypes.bool.isRequired, -}; - -const enhance = compose( - withFragments({ - root: gql` - fragment TalkNotificationsCategoryReply_Toggle_root on RootQuery { - me { - notificationSettings { - onReply - } - } - } - `, - }) -); - -export default enhance(ToggleContainer); diff --git a/plugins/talk-plugin-notifications-category-reply/client/graphql.js b/plugins/talk-plugin-notifications-category-reply/client/graphql.js deleted file mode 100644 index 623e72366..000000000 --- a/plugins/talk-plugin-notifications-category-reply/client/graphql.js +++ /dev/null @@ -1,33 +0,0 @@ -import { gql } from 'react-apollo'; - -export default { - mutations: { - UpdateNotificationSettings: ({ - variables: { input }, - state: { auth: { user: { id } } }, - }) => ({ - update: proxy => { - if (input.onReply === undefined) { - return; - } - - const fragment = gql` - fragment TalkNotificationsCategoryReply_User_Fragment on User { - notificationSettings { - onReply - } - } - `; - const fragmentId = `User_${id}`; - const data = { - __typename: 'User', - notificationSettings: { - __typename: 'NotificationSettings', - onReply: input.onReply, - }, - }; - proxy.writeFragment({ fragment, id: fragmentId, data }); - }, - }), - }, -}; diff --git a/plugins/talk-plugin-notifications-category-reply/client/index.js b/plugins/talk-plugin-notifications-category-reply/client/index.js index 1e22c3a93..098a1397e 100644 --- a/plugins/talk-plugin-notifications-category-reply/client/index.js +++ b/plugins/talk-plugin-notifications-category-reply/client/index.js @@ -1,11 +1,14 @@ -import Toggle from './containers/Toggle'; import translations from './translations.yml'; -import graphql from './graphql'; +import { t } from 'plugin-api/beta/client/services'; +import { createSettingsToggle } from 'talk-plugin-notifications/client/api/factories'; + +const SettingsToggle = createSettingsToggle('onReply', () => + t('talk-plugin-notifications-category-reply.toggle_description') +); export default { slots: { - notificationSettings: [Toggle], + notificationSettings: [SettingsToggle], }, translations, - ...graphql, }; diff --git a/plugins/talk-plugin-notifications-category-staff/client/containers/Toggle.js b/plugins/talk-plugin-notifications-category-staff/client/containers/Toggle.js deleted file mode 100644 index 62a4cbbce..000000000 --- a/plugins/talk-plugin-notifications-category-staff/client/containers/Toggle.js +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { compose, gql } from 'react-apollo'; -import Toggle from 'talk-plugin-notifications/client/components/Toggle'; -import { t } from 'plugin-api/beta/client/services'; -import { withFragments } from 'plugin-api/beta/client/hocs'; - -class ToggleContainer extends React.Component { - constructor(props) { - super(props); - props.setTurnOffInputFragment({ onStaffReply: false }); - - if (this.getOnReplySetting()) { - props.indicateOn(); - } - } - - componentWillReceiveProps(nextProps) { - const prevSetting = this.getOnReplySetting(this.props); - const nextSetting = this.getOnReplySetting(nextProps); - if (prevSetting && !nextSetting) { - nextProps.indicateOff(); - } else if (!prevSetting && nextSetting) { - nextProps.indicateOn(); - } - } - - getOnReplySetting = (props = this.props) => - props.root.me.notificationSettings.onStaffReply; - - toggle = () => { - this.props.updateNotificationSettings({ - onStaffReply: !this.getOnReplySetting(), - }); - }; - - render() { - return ( - - {t('talk-plugin-notifications-category-staff.toggle_description')} - - ); - } -} - -ToggleContainer.propTypes = { - data: PropTypes.object, - root: PropTypes.object, - indicateOn: PropTypes.func.isRequired, - indicateOff: PropTypes.func.isRequired, - setTurnOffInputFragment: PropTypes.func.isRequired, - updateNotificationSettings: PropTypes.func.isRequired, - disabled: PropTypes.bool.isRequired, -}; - -const enhance = compose( - withFragments({ - root: gql` - fragment TalkNotificationsCategoryStaffReply_User_Fragment on RootQuery { - me { - notificationSettings { - onStaffReply - } - } - } - `, - }) -); - -export default enhance(ToggleContainer); diff --git a/plugins/talk-plugin-notifications-category-staff/client/graphql.js b/plugins/talk-plugin-notifications-category-staff/client/graphql.js deleted file mode 100644 index 2e8007d4f..000000000 --- a/plugins/talk-plugin-notifications-category-staff/client/graphql.js +++ /dev/null @@ -1,33 +0,0 @@ -import { gql } from 'react-apollo'; - -export default { - mutations: { - UpdateNotificationSettings: ({ - variables: { input }, - state: { auth: { user: { id } } }, - }) => ({ - update: proxy => { - if (input.onStaffReply === undefined) { - return; - } - - const fragment = gql` - fragment TalkNotificationsCategoryStaffReply_User_Fragment on User { - notificationSettings { - onStaffReply - } - } - `; - const fragmentId = `User_${id}`; - const data = { - __typename: 'User', - notificationSettings: { - __typename: 'NotificationSettings', - onStaffReply: input.onStaffReply, - }, - }; - proxy.writeFragment({ fragment, id: fragmentId, data }); - }, - }), - }, -}; diff --git a/plugins/talk-plugin-notifications-category-staff/client/index.js b/plugins/talk-plugin-notifications-category-staff/client/index.js index 1e22c3a93..e273fceaa 100644 --- a/plugins/talk-plugin-notifications-category-staff/client/index.js +++ b/plugins/talk-plugin-notifications-category-staff/client/index.js @@ -1,11 +1,14 @@ -import Toggle from './containers/Toggle'; import translations from './translations.yml'; -import graphql from './graphql'; +import { t } from 'plugin-api/beta/client/services'; +import { createSettingsToggle } from 'talk-plugin-notifications/client/api/factories'; + +const SettingsToggle = createSettingsToggle('onStaffReply', () => + t('talk-plugin-notifications-category-staff.toggle_description') +); export default { slots: { - notificationSettings: [Toggle], + notificationSettings: [SettingsToggle], }, translations, - ...graphql, }; diff --git a/plugins/talk-plugin-notifications/client/api/components/Toggle.css b/plugins/talk-plugin-notifications/client/api/components/Toggle.css new file mode 100644 index 000000000..3f5b02705 --- /dev/null +++ b/plugins/talk-plugin-notifications/client/api/components/Toggle.css @@ -0,0 +1,20 @@ +.title { + display: inline-block; + width: 100%; + cursor: pointer; + user-select: none; + + &.disabled { + color: #e5e5e5; + cursor: default; + } +} + +.toggle { + display: flex; + align-items: center; +} + +.checkBox { + text-align: right; +} diff --git a/plugins/talk-plugin-notifications/client/api/components/Toggle.js b/plugins/talk-plugin-notifications/client/api/components/Toggle.js new file mode 100644 index 000000000..40e7da4e6 --- /dev/null +++ b/plugins/talk-plugin-notifications/client/api/components/Toggle.js @@ -0,0 +1,41 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Checkbox } from 'plugin-api/beta/client/components/ui'; +import styles from './Toggle.css'; +import uuid from 'uuid/v4'; +import cn from 'classnames'; + +class Toggle extends React.Component { + id = uuid(); + + render() { + const { checked, onChange, children, disabled } = this.props; + return ( +
    + +
    + +
    +
    + ); + } +} + +Toggle.propTypes = { + disabled: PropTypes.bool, + checked: PropTypes.bool, + onChange: PropTypes.func, + children: PropTypes.node, +}; + +export default Toggle; diff --git a/plugins/talk-plugin-notifications/client/api/components/index.js b/plugins/talk-plugin-notifications/client/api/components/index.js new file mode 100644 index 000000000..81e73b58b --- /dev/null +++ b/plugins/talk-plugin-notifications/client/api/components/index.js @@ -0,0 +1 @@ +export { default as Toggle } from './Toggle'; diff --git a/plugins/talk-plugin-notifications/client/api/factories/createSettingsToggle.js b/plugins/talk-plugin-notifications/client/api/factories/createSettingsToggle.js new file mode 100644 index 000000000..95b7789cb --- /dev/null +++ b/plugins/talk-plugin-notifications/client/api/factories/createSettingsToggle.js @@ -0,0 +1,22 @@ +import React from 'react'; +import Toggle from '../components/Toggle'; +import withSettingsToggle from '../hocs/withSettingsToggle'; + +/** + * createSettingsToggle will add a boolean setting with the + * name `settingsName` to notification settings and return + * a full Toggle Component. + * + * You must provide a `label` either as a string or as a callback. + * E.g. to provide translations you could do: + * + * `const SettingsToggle = createSettingsToggle('onReply', () => t('translate'));` + */ +const createSettingsToggle = (settingsName, label) => { + const SettingsToggle = props => ( + {typeof label === 'function' ? label() : label} + ); + return withSettingsToggle(settingsName)(SettingsToggle); +}; + +export default createSettingsToggle; diff --git a/plugins/talk-plugin-notifications/client/api/factories/index.js b/plugins/talk-plugin-notifications/client/api/factories/index.js new file mode 100644 index 000000000..b18ad7679 --- /dev/null +++ b/plugins/talk-plugin-notifications/client/api/factories/index.js @@ -0,0 +1 @@ +export { default as createSettingsToggle } from './createSettingsToggle'; diff --git a/plugins/talk-plugin-notifications/client/api/hocs/index.js b/plugins/talk-plugin-notifications/client/api/hocs/index.js new file mode 100644 index 000000000..c55d371bc --- /dev/null +++ b/plugins/talk-plugin-notifications/client/api/hocs/index.js @@ -0,0 +1 @@ +export { default as withSettingsToggle } from './withSettingsToggle'; diff --git a/plugins/talk-plugin-notifications/client/api/hocs/withSettingsToggle.js b/plugins/talk-plugin-notifications/client/api/hocs/withSettingsToggle.js new file mode 100644 index 000000000..fc7b1bac6 --- /dev/null +++ b/plugins/talk-plugin-notifications/client/api/hocs/withSettingsToggle.js @@ -0,0 +1,121 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { compose, gql } from 'react-apollo'; +import { + withFragments, + withGraphQLExtension, +} from 'plugin-api/beta/client/hocs'; + +const createHOC = settingsName => WrappedComponent => { + class WithSettingsToggle extends React.Component { + constructor(props) { + super(props); + props.setTurnOffInputFragment({ [settingsName]: false }); + + if (this.isChecked()) { + props.indicateOn(); + } + } + + componentWillReceiveProps(nextProps) { + const prevSetting = this.isChecked(this.props); + const nextSetting = this.isChecked(nextProps); + if (prevSetting && !nextSetting) { + nextProps.indicateOff(); + } else if (!prevSetting && nextSetting) { + nextProps.indicateOn(); + } + } + + isChecked = (props = this.props) => + props.root.me.notificationSettings[settingsName]; + + toggle = () => { + this.props.updateNotificationSettings({ + [settingsName]: !this.isChecked(), + }); + }; + + render() { + return ( + + ); + } + } + + WithSettingsToggle.propTypes = { + root: PropTypes.object, + indicateOn: PropTypes.func.isRequired, + indicateOff: PropTypes.func.isRequired, + setTurnOffInputFragment: PropTypes.func.isRequired, + updateNotificationSettings: PropTypes.func.isRequired, + disabled: PropTypes.bool.isRequired, + }; + + return WithSettingsToggle; +}; + +/** + * withSettingsToggle will add a boolean setting with the + * name `settingsName` to notification settings and provide + * the folliwng props: + * + * `checked: boolean` Whether setting is on or off + * `onChange: () => void` Calling this will toggle the setting + * `disabled: boolean` Whether setting is disabled + */ +const withSettingsToggle = settingsName => { + const extension = { + mutations: { + UpdateNotificationSettings: ({ + variables: { input }, + state: { auth: { user: { id } } }, + }) => ({ + update: proxy => { + if (input[settingsName] === undefined) { + return; + } + + const fragment = gql` + fragment TalkNotifications_Toggle_${settingsName}_Fragment on User { + notificationSettings { + ${settingsName} + } + } + `; + const fragmentId = `User_${id}`; + const data = { + __typename: 'User', + notificationSettings: { + __typename: 'NotificationSettings', + [settingsName]: input[settingsName], + }, + }; + proxy.writeFragment({ fragment, id: fragmentId, data }); + }, + }), + }, + }; + + return compose( + withFragments({ + root: gql` + fragment TalkNotifications_Toggle_${settingsName}_root on RootQuery { + me { + notificationSettings { + ${settingsName} + } + } + } + `, + }), + withGraphQLExtension(extension), + createHOC(settingsName) + ); +}; + +export default withSettingsToggle; diff --git a/plugins/talk-plugin-notifications/client/components/Settings.js b/plugins/talk-plugin-notifications/client/components/Settings.js index a5c3b4fcd..f71affcbb 100644 --- a/plugins/talk-plugin-notifications/client/components/Settings.js +++ b/plugins/talk-plugin-notifications/client/components/Settings.js @@ -37,15 +37,18 @@ class Settings extends React.Component { onChangeDigestFrequency, } = this.props; - const slotProps = { - queryData: { root }, - setTurnOffInputFragment: setTurnOffInputFragment, - updateNotificationSettings: updateNotificationSettings, + const slotPassthrough = { + root, + setTurnOffInputFragment, + updateNotificationSettings, disabled: needEmailVerification, }; return ( - +

    {t('talk-plugin-notifications.settings_title')}

    @@ -63,7 +66,7 @@ class Settings extends React.Component { className={styles.notifcationSettingsSlot} fill="notificationSettings" childFactory={this.childFactory} - {...slotProps} + passthrough={slotPassthrough} />
    {digestFrequencyValues.length > 1 && ( diff --git a/plugins/talk-plugin-notifications/client/containers/Settings.js b/plugins/talk-plugin-notifications/client/containers/Settings.js index f3e6c7e6d..27af031e8 100644 --- a/plugins/talk-plugin-notifications/client/containers/Settings.js +++ b/plugins/talk-plugin-notifications/client/containers/Settings.js @@ -50,7 +50,6 @@ class SettingsContainer extends React.Component { render() { return ( { - return {t('talk-plugin-profile-settings.tab')}; -}; - -export default Tab; diff --git a/plugins/talk-plugin-profile-settings/client/components/TabPane.js b/plugins/talk-plugin-profile-settings/client/components/TabPane.js deleted file mode 100644 index c0347a95c..000000000 --- a/plugins/talk-plugin-profile-settings/client/components/TabPane.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Slot } from 'plugin-api/beta/client/components'; - -class TabPane extends React.Component { - render() { - const { data, root } = this.props; - return ( -
    - -
    - ); - } -} - -TabPane.propTypes = { - data: PropTypes.object, - root: PropTypes.object, -}; - -export default TabPane; diff --git a/plugins/talk-plugin-profile-settings/client/containers/TabPane.js b/plugins/talk-plugin-profile-settings/client/containers/TabPane.js deleted file mode 100644 index 6384c7160..000000000 --- a/plugins/talk-plugin-profile-settings/client/containers/TabPane.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { compose, gql } from 'react-apollo'; -import TabPane from '../components/TabPane'; -import { withFragments } from 'plugin-api/beta/client/hocs'; -import { getSlotFragmentSpreads } from 'plugin-api/beta/client/utils'; - -const slots = ['profileSettings']; - -class TabPaneContainer extends React.Component { - render() { - return ; - } -} - -const enhance = compose( - withFragments({ - root: gql` - fragment TalkProfileSettings_TabPane_root on RootQuery { - __typename - ${getSlotFragmentSpreads(slots, 'root')} - } - `, - }) -); - -export default enhance(TabPaneContainer); diff --git a/plugins/talk-plugin-profile-settings/client/index.js b/plugins/talk-plugin-profile-settings/client/index.js deleted file mode 100644 index 00e37a0f1..000000000 --- a/plugins/talk-plugin-profile-settings/client/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import Tab from './components/Tab'; -import TabPane from './containers/TabPane'; -import translations from './translations.yml'; - -export default { - slots: { - profileTabs: [Tab], - profileTabPanes: [TabPane], - }, - translations, -}; diff --git a/plugins/talk-plugin-profile-settings/client/translations.yml b/plugins/talk-plugin-profile-settings/client/translations.yml deleted file mode 100644 index 33805a49b..000000000 --- a/plugins/talk-plugin-profile-settings/client/translations.yml +++ /dev/null @@ -1,30 +0,0 @@ -ar: - talk-plugin-profile-settings: - tab: إعدادات -en: - talk-plugin-profile-settings: - tab: Settings -de: - talk-plugin-profile-settings: - tab: Einstellungen -es: - talk-plugin-profile-settings: - tab: Configuración -fr: - talk-plugin-profile-settings: - tab: Paramètres -nl_NL: - talk-plugin-profile-settings: - tab: Instellingen -da: - talk-plugin-profile-settings: - tab: Indstillinger -pt_PR: - talk-plugin-profile-settings: - tab: Configurações -zh_TW: - talk-plugin-profile-settings: - tab: 設置 -zh_CN: - talk-plugin-profile-settings: - tab: 设置 diff --git a/plugins/talk-plugin-profile-settings/index.js b/plugins/talk-plugin-profile-settings/index.js deleted file mode 100644 index f053ebf79..000000000 --- a/plugins/talk-plugin-profile-settings/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/plugins/talk-plugin-viewing-options/client/components/Category.js b/plugins/talk-plugin-viewing-options/client/components/Category.js index 0fc13d16f..78ce4dae3 100644 --- a/plugins/talk-plugin-viewing-options/client/components/Category.js +++ b/plugins/talk-plugin-viewing-options/client/components/Category.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import styles from './Category.css'; import { Slot } from 'plugin-api/beta/client/components'; @@ -8,7 +9,7 @@ const childFactory = child => ( ); -const ViewingOptions = ({ slot, title, data, asset, root }) => { +const Category = ({ slot, title, slotPassthrough }) => { return (
    {title}
    @@ -17,11 +18,16 @@ const ViewingOptions = ({ slot, title, data, asset, root }) => { childFactory={childFactory} className={styles.list} component={'ul'} - data={data} - queryData={{ asset, root }} + passthrough={slotPassthrough} />
    ); }; -export default ViewingOptions; +Category.propTypes = { + slot: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + slotPassthrough: PropTypes.object.isRequired, +}; + +export default Category; diff --git a/plugins/talk-plugin-viewing-options/client/components/Menu.js b/plugins/talk-plugin-viewing-options/client/components/Menu.js index 7ff5ac316..c3ea40fe1 100644 --- a/plugins/talk-plugin-viewing-options/client/components/Menu.js +++ b/plugins/talk-plugin-viewing-options/client/components/Menu.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import cn from 'classnames'; import styles from './Menu.css'; import { capitalize } from 'plugin-api/beta/client/utils'; @@ -13,16 +14,19 @@ class Menu extends React.Component { }; render() { + const { slotPassthrough } = this.props; return (
    {Object.keys(this.categories).map(category => ( ))} @@ -31,4 +35,8 @@ class Menu extends React.Component { } } +Menu.propTypes = { + slotPassthrough: PropTypes.object.isRequired, +}; + export default Menu; diff --git a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js index 8a0c09f61..40aa96d1e 100644 --- a/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js +++ b/plugins/talk-plugin-viewing-options/client/components/ViewingOptions.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import cn from 'classnames'; import styles from './ViewingOptions.css'; import { t } from 'plugin-api/beta/client/services'; @@ -24,7 +25,7 @@ class ViewingOptions extends React.Component { }; render() { - const { open, data, root, asset } = this.props; + const { open, slotPassthrough } = this.props; return (
    @@ -41,11 +42,18 @@ class ViewingOptions extends React.Component { )}
    - {open && } + {open && }
    ); } } +ViewingOptions.propTypes = { + slotPassthrough: PropTypes.object.isRequired, + open: PropTypes.bool.isRequired, + openMenu: PropTypes.func.isRequired, + closeMenu: PropTypes.func.isRequired, +}; + export default ViewingOptions; diff --git a/plugins/talk-plugin-viewing-options/client/containers/ViewingOptions.js b/plugins/talk-plugin-viewing-options/client/containers/ViewingOptions.js index b30721346..0e1919383 100644 --- a/plugins/talk-plugin-viewing-options/client/containers/ViewingOptions.js +++ b/plugins/talk-plugin-viewing-options/client/containers/ViewingOptions.js @@ -4,6 +4,7 @@ import ViewingOptions from '../components/ViewingOptions'; import { openMenu, closeMenu } from '../actions'; import { compose, gql } from 'react-apollo'; import { getSlotFragmentSpreads } from 'plugin-api/beta/client/utils'; +import { mapProps } from 'recompose'; const slots = ['viewingOptionsSort', 'viewingOptionsFilter']; @@ -29,7 +30,14 @@ const withViewingOptionsFragments = withFragments({ const enhance = compose( connect(mapStateToProps, mapDispatchToProps), - withViewingOptionsFragments + withViewingOptionsFragments, + mapProps(({ root, asset, ...rest }) => ({ + slotPassthrough: { + root, + asset, + }, + ...rest, + })) ); export default enhance(ViewingOptions); diff --git a/yarn.lock b/yarn.lock index 359a615d6..3556a26ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -81,8 +81,8 @@ prettier "^1.10.2" "@coralproject/graphql-anywhere-optimized@^0.1.0": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@coralproject/graphql-anywhere-optimized/-/graphql-anywhere-optimized-0.1.5.tgz#67c862bf908ea717d9521ea76266b5bc9f109c65" + version "0.1.6" + resolved "https://registry.yarnpkg.com/@coralproject/graphql-anywhere-optimized/-/graphql-anywhere-optimized-0.1.6.tgz#073b33764c04788b0290788da9ebf0ed21af6437" dependencies: graphql-ast-tools "^0.2.2" @@ -9187,6 +9187,13 @@ react-apollo@^1.4.12: object-assign "^4.0.1" prop-types "^15.5.8" +react-broadcast@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/react-broadcast/-/react-broadcast-0.6.2.tgz#9555c73b80ca5b2673830872e54f6bb3092cf8a9" + dependencies: + invariant "^2.2.1" + prop-types "^15.6.0" + react-dom@>=0.14.0: version "16.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044"