Merge branch 'master' into slots-debug

This commit is contained in:
Chi Vinh Le
2018-03-20 18:08:59 +01:00
107 changed files with 1371 additions and 1129 deletions
@@ -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',
@@ -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 (
<div className={styles.root}>
<IfSlotIsNotEmpty
queryData={queryData}
slot={['adminCommentMoreDetails', 'adminCommentMoreFlagDetails']}
passthrough={slotPassthrough}
>
<a onClick={this.toggleDetail} className={styles.moreDetail}>
{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}
</a>
</IfSlotIsNotEmpty>
<Slot
fill="adminCommentDetailArea"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
more={showDetail}
/>
<Slot fill="adminCommentDetailArea" passthrough={slotPassthrough} />
{showDetail && (
<Slot
fill="adminCommentMoreDetails"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>
<Slot fill="adminCommentMoreDetails" passthrough={slotPassthrough} />
)}
</div>
);
@@ -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,
@@ -43,6 +43,9 @@ const CommentLabels = ({
comment,
comment: { className, status, actions, hasParent },
}) => {
const slotPassthrough = {
comment,
};
return (
<div className={cn(className, styles.root)}>
<div className={styles.coreLabels}>
@@ -69,7 +72,7 @@ const CommentLabels = ({
<Slot
className={styles.slot}
fill="adminCommentLabels"
queryData={{ comment }}
passthrough={slotPassthrough}
/>
</div>
);
@@ -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 (
<ClickOutside onClickOutside={modal ? null : hideUserDetail}>
<Drawer
@@ -244,11 +248,7 @@ class UserDetail extends React.Component {
</ul>
</div>
<Slot
fill="userProfile"
data={this.props.data}
queryData={{ root, user }}
/>
<Slot fill="userProfile" passthrough={slotPassthrough} />
<hr />
@@ -301,7 +301,6 @@ class UserDetail extends React.Component {
<UserDetailCommentList
user={user}
root={root}
data={data}
loadMore={loadMore}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
@@ -320,7 +319,6 @@ class UserDetail extends React.Component {
<UserDetailCommentList
user={user}
root={root}
data={data}
loadMore={loadMore}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
@@ -368,9 +366,7 @@ UserDetail.propTypes = {
bulkReject: PropTypes.func.isRequired,
toggleSelectAll: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
data: PropTypes.shape({
refetch: PropTypes.func.isRequired,
}),
data: PropTypes.object,
activeTab: PropTypes.string.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
@@ -32,13 +32,12 @@ class UserDetailComment extends React.Component {
selected,
toggleSelect,
className,
data,
root: { settings },
} = this.props;
const queryData = { root, comment };
const formatterSettings = {
const slotPassthrough = {
root,
comment,
suspectWords: settings.wordlist.suspect,
bannedWords: settings.wordlist.banned,
body: comment.body,
@@ -89,15 +88,13 @@ class UserDetailComment extends React.Component {
<div className={styles.body}>
<Slot
fill="userDetailCommentContent"
data={data}
className={cn(
styles.commentContent,
'talk-admin-user-detail-comment'
)}
queryData={queryData}
slotSize={1}
size={1}
defaultComponent={CommentFormatter}
{...formatterSettings}
passthrough={slotPassthrough}
/>
<a
className={styles.external}
@@ -130,7 +127,7 @@ class UserDetailComment extends React.Component {
</div>
</CommentAnimatedEdit>
</div>
<CommentDetails data={data} root={root} comment={comment} />
<CommentDetails root={root} comment={comment} />
</li>
);
}
@@ -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,
@@ -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,
@@ -75,7 +75,6 @@ export default class Configure extends Component {
</div>
<div className={styles.mainContent}>
<SectionComponent
data={this.props.data}
root={this.props.root}
settings={this.props.settings}
/>
@@ -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,
@@ -52,7 +52,7 @@ class ModerationSettings extends React.Component {
};
render() {
const { settings, data, root, updatePending, errors } = this.props;
const { settings, slotPassthrough } = this.props;
return (
<ConfigurePage title={t('configure.moderation_settings')}>
@@ -82,13 +82,7 @@ class ModerationSettings extends React.Component {
suspectWords={settings.wordlist.suspect}
onChangeWordlist={this.updateWordlist}
/>
<Slot
fill="adminModerationSettings"
data={data}
queryData={{ root, settings }}
updatePending={updatePending}
errors={errors}
/>
<Slot fill="adminModerationSettings" passthrough={slotPassthrough} />
</ConfigurePage>
);
}
@@ -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;
@@ -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 (
<ConfigurePage title={t('configure.stream_settings')}>
@@ -220,13 +220,7 @@ class StreamSettings extends React.Component {
</div>
</ConfigureCard>
{/* the above card should be the last one if at all possible because of z-index issues with the selects */}
<Slot
fill="adminStreamSettings"
data={data}
queryData={{ root, settings }}
updatePending={updatePending}
errors={errors}
/>
<Slot fill="adminStreamSettings" passthrough={slotPassthrough} />
</ConfigurePage>
);
}
@@ -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;
@@ -34,7 +34,7 @@ class TechSettings extends React.Component {
};
render() {
const { settings, data, root, errors, updatePending } = this.props;
const { settings, slotPassthrough } = this.props;
return (
<ConfigurePage title={t('configure.tech_settings')}>
<Domainlist
@@ -50,13 +50,7 @@ class TechSettings extends React.Component {
onChange={this.updateCustomCssUrl}
/>
</ConfigureCard>
<Slot
fill="adminTechSettings"
data={data}
queryData={{ root, settings }}
updatePending={updatePending}
errors={errors}
/>
<Slot fill="adminTechSettings" passthrough={slotPassthrough} />
</ConfigurePage>
);
}
@@ -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;
@@ -31,7 +31,6 @@ class ConfigureContainer extends Component {
return (
<Configure
currentUser={this.props.currentUser}
data={this.props.data}
root={this.props.root}
settings={this.props.mergedSettings}
canSave={this.props.canSave}
@@ -5,6 +5,7 @@ import ModerationSettings from '../components/ModerationSettings';
import withFragments from 'coral-framework/hocs/withFragments';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import { updatePending } from '../../../actions/configure';
import { mapProps } from 'recompose';
const slots = ['adminModerationSettings'];
@@ -41,5 +42,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,
}))
)(ModerationSettings);
@@ -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);
@@ -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);
@@ -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 (
<li
tabIndex={0}
@@ -113,9 +118,7 @@ class Comment extends React.Component {
<CommentLabels comment={comment} />
<Slot
fill="adminCommentInfoBar"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
passthrough={slotPassthrough}
/>
</div>
</div>
@@ -135,13 +138,10 @@ class Comment extends React.Component {
<div className={styles.body}>
<Slot
fill="adminCommentContent"
data={data}
className={cn(styles.commentContent, 'talk-admin-comment')}
clearHeightCache={clearHeightCache}
queryData={queryData}
slotSize={1}
size={1}
defaultComponent={CommentFormatter}
{...formatterSettings}
passthrough={{ ...slotPassthrough, ...formatterSettings }}
/>
<div className={styles.commentContentFooter}>
<a
@@ -171,18 +171,12 @@ class Comment extends React.Component {
onClick={this.reject}
/>
</div>
<Slot
fill="adminSideActions"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>
<Slot fill="adminSideActions" passthrough={slotPassthrough} />
</div>
</div>
</CommentAnimatedEdit>
</div>
<CommentDetails
data={data}
root={root}
comment={comment}
clearHeightCache={clearHeightCache}
@@ -220,7 +214,6 @@ Comment.propTypes = {
id: PropTypes.string,
}),
}),
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
selected: PropTypes.bool,
};
@@ -126,7 +126,6 @@ class Moderation extends Component {
render() {
const {
root,
data,
moderation,
viewUserDetail,
activeTab,
@@ -148,6 +147,13 @@ class Moderation extends Component {
count: root[`${queue}Count`],
}));
const slotPassthrough = {
root,
asset,
activeTab,
handleCommentChange,
};
return (
<div>
<ModerationHeader
@@ -171,7 +177,6 @@ class Moderation extends Component {
/>
<ModerationQueue
key={`${activeTab}_${this.props.moderation.sortOrder}`}
data={this.props.data}
root={this.props.root}
currentAsset={asset}
comments={comments.nodes}
@@ -204,13 +209,7 @@ class Moderation extends Component {
closeSearch={this.closeSearch}
storySearchChange={this.props.storySearchChange}
/>
<Slot
data={data}
queryData={{ root, asset }}
activeTab={activeTab}
handleCommentChange={handleCommentChange}
fill="adminModeration"
/>
<Slot fill="adminModeration" passthrough={slotPassthrough} />
</div>
);
}
@@ -344,7 +344,6 @@ class ModerationQueue extends React.Component {
child = (
<div style={style}>
<Comment
data={this.props.data}
root={this.props.root}
comment={comment}
dangling={
@@ -408,7 +407,6 @@ class ModerationQueue extends React.Component {
return (
<div className={styles.root}>
<Comment
data={this.props.data}
root={this.props.root}
key={comment.id}
comment={comment}
@@ -476,7 +474,6 @@ ModerationQueue.propTypes = {
hasNextPage: PropTypes.bool,
comments: PropTypes.array,
activeTab: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
currentUserId: PropTypes.string.isRequired,
};
@@ -111,14 +111,14 @@ class IndicatorContainer extends Component {
return null;
}
const slotPassthrough = {
handleCommentChange: this.handleCommentChange,
};
return (
<span>
<Indicator />
<Slot
data={this.props.data}
handleCommentChange={this.handleCommentChange}
fill="adminModerationIndicator"
/>
<Slot fill="adminModerationIndicator" passthrough={slotPassthrough} />
</span>
);
}
@@ -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 (
<div
@@ -84,7 +85,7 @@ export default class Embed extends React.Component {
/>
</IfSlotIsNotEmpty>
<Slot data={data} queryData={{ root }} fill="embed" />
<Slot passthrough={slotPassthrough} fill="embed" />
<ExtendableTabPanel
className="talk-embed-stream-tab-bar"
@@ -94,8 +95,7 @@ export default class Embed extends React.Component {
tabSlot="embedStreamTabs"
tabSlotPrepend="embedStreamTabsPrepend"
tabPaneSlot="embedStreamTabPanes"
slotProps={{ data }}
queryData={{ root }}
slotPassthrough={slotPassthrough}
tabs={this.getTabs()}
tabPanes={[
<TabPane
@@ -138,9 +138,5 @@ Embed.propTypes = {
commentId: PropTypes.string,
root: PropTypes.object,
activeTab: PropTypes.string,
data: PropTypes.shape({
loading: PropTypes.bool,
error: PropTypes.object,
refetch: PropTypes.func,
}).isRequired,
data: PropTypes.object.isRequired,
};
@@ -1,17 +1,12 @@
import React from 'react';
import ExtendableTabPanel from '../components/ExtendableTabPanel';
import { connect } from 'react-redux';
import { TabPane } from 'coral-ui';
import ExtendableTab from '../components/ExtendableTab';
import { getShallowChanges } from 'coral-framework/utils';
import isEqual from 'lodash/isEqual';
import PropTypes from 'prop-types';
import { withSlotElements } from 'coral-framework/hocs';
import { compose } from 'recompose';
class ExtendableTabPanelContainer extends React.Component {
static contextTypes = {
plugins: PropTypes.object,
};
componentDidMount() {
this.handleFallback();
}
@@ -20,24 +15,6 @@ class ExtendableTabPanelContainer extends React.Component {
this.handleFallback(next);
}
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 prevKeys = this.getSlotElements(this.props.tabSlot, this.props).map(
el => 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 (
<ExtendableTab
tabId={el.type.talkPluginName}
key={el.type.talkPluginName}
>
{React.cloneElement(el, {
active: this.props.activeTab === el.type.talkPluginName,
})}
</ExtendableTab>
);
});
}
createPluginTabFactory = (props = this.props) => el => {
return (
<ExtendableTab
tabId={el.type.talkPluginName}
key={el.type.talkPluginName}
>
{React.cloneElement(el, {
active: props.activeTab === el.type.talkPluginName,
})}
</ExtendableTab>
);
};
getTabElements(props = this.props) {
const elements = [...this.getPluginTabElementsPrepend(props)];
@@ -92,14 +57,16 @@ class ExtendableTabPanelContainer extends React.Component {
return elements;
}
createPluginTabPane(el) {
return (
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
{el}
</TabPane>
);
}
getPluginTabPaneElements(props = this.props) {
return this.getSlotElements(props.tabPaneSlot).map(el => {
return (
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
{el}
</TabPane>
);
});
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);
@@ -7,11 +7,7 @@ class Configure extends React.Component {
render() {
return (
<div className="talk-embed-stream-configuration-container">
<Settings
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
/>
<Settings root={this.props.root} asset={this.props.asset} />
<hr />
<AssetStatusInfo asset={this.props.asset} />
</div>
@@ -20,7 +16,6 @@ class Configure extends React.Component {
}
Configure.propTypes = {
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
asset: PropTypes.object.isRequired,
};
@@ -25,9 +25,9 @@ class Settings extends React.Component {
onQuestionBoxContentChange,
canSave,
onApply,
slotProps,
queryData,
slotPassthrough,
} = this.props;
return (
<div className={styles.wrapper}>
<div className={styles.container}>
@@ -77,7 +77,7 @@ class Settings extends React.Component {
</div>
)}
</Configuration>
<Slot fill="streamSettings" queryData={queryData} {...slotProps} />
<Slot fill="streamSettings" passthrough={slotPassthrough} />
</div>
</div>
);
@@ -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,
@@ -13,13 +13,7 @@ class ConfigureContainer extends React.Component {
return <div>{this.props.data.error.message}</div>;
}
return (
<Configure
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
/>
);
return <Configure root={this.props.root} asset={this.props.asset} />;
}
}
@@ -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 (
<Settings
settings={mergedSettings}
queryData={{ root, asset, settings: mergedSettings }}
slotProps={{ data, updatePending, errors }}
slotPassthrough={slotPassthrough}
savePending={this.savePending}
onToggleModeration={this.toggleModeration}
onTogglePremodLinks={this.togglePremodLinks}
@@ -82,7 +89,6 @@ class SettingsContainer extends React.Component {
}
SettingsContainer.propTypes = {
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
asset: PropTypes.object.isRequired,
pending: PropTypes.object.isRequired,
@@ -22,9 +22,9 @@ class Comment extends React.Component {
};
render() {
const { comment, data, root } = this.props;
const { comment, root } = this.props;
const reactionCount = getTotalReactionsCount(comment.action_summaries);
const queryData = { root, comment, asset: comment.asset };
const slotPassthrough = { root, comment, asset: comment.asset };
return (
<div className={styles.myComment}>
@@ -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}
/>
<div className={cn(styles.commentSummary, 'comment-summary')}>
<span
@@ -98,9 +97,10 @@ class Comment extends React.Component {
fill="historyCommentTimestamp"
defaultComponent={CommentTimestamp}
className={'talk-history-comment-published-date'}
created_at={comment.created_at}
data={data}
queryData={queryData}
passthrough={{
created_at: comment.created_at,
...slotPassthrough,
}}
inline
/>
</li>
@@ -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,
};
@@ -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 <BlankCommentHistory />;
}
@@ -33,7 +33,6 @@ class CommentHistory extends React.Component {
return (
<Comment
key={i}
data={data}
root={root}
comment={comment}
navigate={navigate}
@@ -56,7 +55,6 @@ CommentHistory.propTypes = {
comments: PropTypes.object.isRequired,
loadMore: PropTypes.func,
navigate: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
};
@@ -1,57 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import Slot from 'coral-framework/components/Slot';
import CommentHistory from '../containers/CommentHistory';
import ExtendableTabPanel from '../../../containers/ExtendableTabPanel';
import { Tab, TabPane } from 'coral-ui';
import styles from './Profile.css';
import t from 'coral-framework/services/i18n';
import TabPanel from '../containers/TabPanel';
const Profile = ({
username,
emailAddress,
data,
root,
activeTab,
setActiveTab,
}) => (
<div className="talk-my-profile talk-profile-container">
<div className={styles.userInfo}>
<h2 className={styles.username}>{username}</h2>
{emailAddress ? <p className={styles.email}>{emailAddress}</p> : null}
const Profile = ({ username, emailAddress, root, slotPassthrough }) => {
return (
<div className="talk-my-profile talk-profile-container">
<div className={styles.userInfo}>
<h2 className={styles.username}>{username}</h2>
{emailAddress ? <p className={styles.email}>{emailAddress}</p> : null}
</div>
<Slot fill="profileSections" passthrough={slotPassthrough} />
<TabPanel root={root} slotPassthrough={slotPassthrough} />
</div>
<Slot fill="profileSections" data={data} queryData={{ root }} />
<ExtendableTabPanel
activeTab={activeTab}
setActiveTab={setActiveTab}
fallbackTab="comments"
tabSlot="profileTabs"
tabSlotPrepend="profileTabsPrepend"
tabPaneSlot="profileTabPanes"
slotProps={{ data }}
queryData={{ root }}
tabs={[
<Tab key="comments" tabId="comments">
{t('framework.my_comments')}
</Tab>,
]}
tabPanes={[
<TabPane key="comments" tabId="comments">
<CommentHistory data={data} root={root} />
</TabPane>,
]}
sub
/>
</div>
);
);
};
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;
@@ -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 (
<div>
<Slot fill="profileSettings" passthrough={slotPassthrough} />
</div>
);
}
}
Settings.propTypes = {
root: PropTypes.object,
};
export default Settings;
@@ -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 = [
<Tab key="comments" tabId="comments">
{t('framework.my_comments')}
</Tab>,
];
if (showSettingsTab) {
tabs.push(
<Tab key="settings" tabId="settings">
{t('profile_settings')}
</Tab>
);
}
return (
<ExtendableTabPanel
activeTab={activeTab}
setActiveTab={setActiveTab}
fallbackTab="comments"
tabSlot="profileTabs"
tabSlotPrepend="profileTabsPrepend"
tabPaneSlot="profileTabPanes"
slotPassthrough={slotPassthrough}
tabs={tabs}
tabPanes={[
<TabPane key="comments" tabId="comments">
<CommentHistory root={root} />
</TabPane>,
<TabPane key="settings" tabId="settings">
<Settings root={root} />
</TabPane>,
]}
sub
/>
);
};
TabPanel.propTypes = {
root: PropTypes.object,
slotPassthrough: PropTypes.object,
activeTab: PropTypes.string.isRequired,
setActiveTab: PropTypes.func.isRequired,
showSettingsTab: PropTypes.bool.isRequired,
};
export default TabPanel;
@@ -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 (
<CommentHistory
comments={this.props.root.me.comments}
data={this.props.data}
root={this.props.root}
loadMore={this.loadMore}
navigate={this.navigate}
@@ -57,8 +56,8 @@ CommentHistoryContainer.contextTypes = {
};
CommentHistoryContainer.propTypes = {
data: PropTypes.object,
root: PropTypes.object,
fetchMore: PropTypes.func.isRequired,
};
const LOAD_MORE_QUERY = gql`
@@ -99,5 +98,6 @@ const mapStateToProps = state => ({
export default compose(
connect(mapStateToProps, null),
withCommentHistoryFragments
withCommentHistoryFragments,
withFetchMore
)(CommentHistoryContainer);
@@ -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 (
<Profile
username={me.username}
emailAddress={emailAddress}
data={data}
root={root}
activeTab={this.props.activeTab}
setActiveTab={this.props.setActiveTab}
slotPassthrough={slotPassthrough}
/>
);
}
@@ -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),
@@ -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 <Settings {...this.props} />;
}
}
const enhance = compose(
withFragments({
root: gql`
fragment TalkEmbedStream_ProfileSettings_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}
`,
})
);
export default enhance(SettingsContainer);
@@ -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 (
<TabPanel
root={this.props.root}
slotPassthrough={this.props.slotPassthrough}
activeTab={this.props.activeTab}
setActiveTab={this.props.setActiveTab}
showSettingsTab={this.props.profileSettingsSlotElements.length > 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);
@@ -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 (
<Comment
commentClassNames={commentClassNames}
data={data}
root={root}
disableReply={disableReply}
setActiveReplyBox={setActiveReplyBox}
@@ -205,7 +203,6 @@ class AllCommentsPane extends React.Component {
}
AllCommentsPane.propTypes = {
data: PropTypes.object,
root: PropTypes.object,
comments: PropTypes.object,
commentClassNames: PropTypes.array,
@@ -181,7 +181,6 @@ export default class Comment extends React.Component {
}),
charCountEnable: PropTypes.bool.isRequired,
maxCharCount: PropTypes.number,
data: PropTypes.object,
root: PropTypes.object,
loadMore: PropTypes.func,
postDontAgree: PropTypes.func,
@@ -417,7 +416,6 @@ export default class Comment extends React.Component {
{view.map(reply => {
return (
<CommentContainer
data={this.props.data}
root={this.props.root}
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
@@ -487,7 +485,6 @@ export default class Comment extends React.Component {
renderComment() {
const {
asset,
data,
root,
depth,
comment,
@@ -535,12 +532,8 @@ export default class Comment extends React.Component {
);
// props that are passed down the slots.
const slotProps = {
data,
const slotPassthrough = {
depth,
};
const queryData = {
root,
asset,
comment,
@@ -551,8 +544,7 @@ export default class Comment extends React.Component {
<Slot
className={cn(styles.commentAvatar, 'talk-stream-comment-avatar')}
fill="commentAvatar"
{...slotProps}
queryData={queryData}
passthrough={slotPassthrough}
inline
/>
@@ -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}
/>
<div
@@ -591,8 +582,7 @@ export default class Comment extends React.Component {
'talk-stream-comment-author-tags'
)}
fill="commentAuthorTags"
queryData={queryData}
{...slotProps}
passthrough={slotPassthrough}
inline
/>
</div>
@@ -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 ? (
<span>
@@ -624,8 +615,7 @@ export default class Comment extends React.Component {
<Slot
className={styles.commentInfoBar}
fill="commentInfoBar"
{...slotProps}
queryData={queryData}
passthrough={slotPassthrough}
/>
{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}
/>
</div>
)}
@@ -678,8 +667,7 @@ export default class Comment extends React.Component {
<div className="talk-embed-stream-comment-actions-container-left commentActionsLeft comment__action-container">
<Slot
fill="commentReactions"
{...slotProps}
queryData={queryData}
passthrough={slotPassthrough}
inline
/>
@@ -696,9 +684,7 @@ export default class Comment extends React.Component {
<div className="talk-embed-stream-comment-actions-container-right commentActionsRight comment__action-container">
<Slot
fill="commentActions"
wrapperComponent={ActionButton}
{...slotProps}
queryData={queryData}
passthrough={slotPassthrough}
inline
/>
<ActionButton>
@@ -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 (
<div>
<div
@@ -69,10 +62,19 @@ export default class DraftArea extends React.Component {
fill="draftArea"
defaultComponent={DraftAreaContent}
className={styles.content}
queryData={queryData}
registerHook={this.props.registerHook}
unregisterHook={this.props.unregisterHook}
{...tASettings}
passthrough={{
root,
comment,
registerHook,
unregisterHook,
value,
placeholder,
id,
onChange,
rows,
disabled,
isReply,
}}
/>
<Slot fill="commentInputArea" />
</div>
@@ -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,
@@ -40,7 +40,6 @@ class Stream extends React.Component {
renderHighlightedComment() {
const {
data,
root,
activeReplyBox,
setActiveReplyBox,
@@ -91,7 +90,6 @@ class Stream extends React.Component {
</div>
<Comment
data={data}
root={root}
commentClassNames={commentClassNames}
setActiveReplyBox={setActiveReplyBox}
@@ -122,7 +120,6 @@ class Stream extends React.Component {
renderExtendableTabPanel() {
const {
data,
root,
activeReplyBox,
setActiveReplyBox,
@@ -147,15 +144,14 @@ class Stream extends React.Component {
loading,
} = this.props;
const slotProps = { data };
const slotQueryData = { root, asset };
const slotPassthrough = { root, asset };
// `key` of `ExtendableTabPanel` depends on sorting so that we always reset
// the state when changing sorting.
return (
<div className={cn('talk-stream-tab-container', styles.tabContainer)}>
<div className={cn('talk-stream-filter-wrapper', styles.filterWrapper)}>
<Slot fill="streamFilter" queryData={slotQueryData} {...slotProps} />
<Slot fill="streamFilter" passthrough={slotPassthrough} />
</div>
<ExtendableTabPanel
@@ -166,8 +162,7 @@ class Stream extends React.Component {
tabSlot="streamTabs"
tabSlotPrepend="streamTabsPrepend"
tabPaneSlot="streamTabPanes"
slotProps={slotProps}
queryData={slotQueryData}
slotPassthrough={slotPassthrough}
loading={loading}
tabs={
<Tab tabId={'all'} key="all">
@@ -180,7 +175,6 @@ class Stream extends React.Component {
tabPanes={
<TabPane tabId="all" key="all">
<AllCommentsPane
data={data}
root={root}
asset={asset}
comments={comments}
@@ -212,7 +206,6 @@ class Stream extends React.Component {
render() {
const {
data,
root,
appendItemArray,
asset,
@@ -243,13 +236,13 @@ class Stream extends React.Component {
!changedUsername &&
!highlightedComment) ||
keepCommentBox);
const slotProps = { data };
const slotQueryData = { root, asset };
if (highlightedComment === null) {
return <StreamError>{t('stream.comment_not_found')}</StreamError>;
}
const slotPassthrough = { root, asset };
return (
<div id="stream" className={styles.root}>
{open ? (
@@ -263,11 +256,7 @@ class Stream extends React.Component {
content={asset.settings.questionBoxContent}
icon={asset.settings.questionBoxIcon}
>
<Slot
fill="streamQuestionArea"
queryData={slotQueryData}
{...slotProps}
/>
<Slot fill="streamQuestionArea" passthrough={slotPassthrough} />
</QuestionBox>
)}
{!banned &&
@@ -304,7 +293,7 @@ class Stream extends React.Component {
<p>{asset.settings.closedMessage}</p>
)}
<Slot fill="stream" queryData={slotQueryData} {...slotProps} />
<Slot fill="stream" passthrough={slotPassthrough} />
{currentUser && (
<ModerationLink
@@ -324,7 +313,6 @@ class Stream extends React.Component {
Stream.propTypes = {
asset: PropTypes.object,
activeStreamTab: PropTypes.string,
data: PropTypes.object,
root: PropTypes.object,
activeReplyBox: PropTypes.string,
setActiveReplyBox: PropTypes.func,
@@ -190,9 +190,11 @@ class CommentBox extends React.Component {
buttonContainerStart={
<Slot
fill="commentInputDetailArea"
registerHook={this.registerHook}
unregisterHook={this.unregisterHook}
isReply={isReply}
passthrough={{
registerHook: this.registerHook,
unregisterHook: this.unregisterHook,
isReply,
}}
inline
/>
}
@@ -42,11 +42,10 @@ class DraftAreaContainer extends React.Component {
}
render() {
const queryData = { comment: this.props.comment, root: this.props.root };
return (
<DraftArea
queryData={queryData}
root={this.props.root}
comment={this.props.comment}
value={this.props.value}
placeholder={this.props.placeholder}
id={this.props.id}
@@ -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 IfSlotIsEmpty 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 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);
@@ -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);
+33 -86
View File
@@ -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 = <DefaultComponent {...props} />;
}
let children = this.props.slotElements;
if (childFactory) {
children = children.map(childFactory);
}
// console.log('pluginsConfig', pluginsConfig);
return (
<Component
className={cn(
{
[styles.inline]: inline,
[styles.debug]: pluginsConfig.debug,
},
{ [styles.inline]: inline, [styles.debug]: debug },
className,
`talk-slot-${kebabCase(fill)}`
)}
@@ -122,13 +45,14 @@ Slot.propTypes = {
fill: PropTypes.string.isRequired,
inline: PropTypes.bool,
className: PropTypes.string,
reduxState: PropTypes.object,
debug: PropTypes.bool,
slotElements: PropTypes.arrayOf(PropTypes.element).isRequired,
defaultComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
/**
* Specifies the number of children that can fill the slot.
*/
slotSize: PropTypes.number,
size: PropTypes.number,
/**
* You may specify the component to use as the root wrapper.
@@ -137,8 +61,12 @@ Slot.propTypes = {
component: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
// props coming from graphql must be passed through this property.
// @Deprecated
queryData: PropTypes.object,
// props that are passed to all Slot Components
passthrough: PropTypes.object,
/**
* You may need to apply reactive updates to a child as it is exiting.
* This is generally done by using `cloneElement` however in the case of an exiting
@@ -152,8 +80,27 @@ Slot.propTypes = {
childFactory: PropTypes.func,
};
const omitProps = [
'fill',
'inline',
'className',
'size',
'defaultComponent',
'queryData',
'childFactory',
'component',
];
const mapStateToProps = state => ({
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);
+7
View File
@@ -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';
@@ -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),
}));
@@ -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 (
<Subscriber channel="queryData">
{data => (
<WrappedComponent
{...this.props}
fetchMore={get(data, 'fetchMore')}
/>
)}
</Subscriber>
);
}
}
return WithFetchMore;
});
+4 -10
View File
@@ -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;
}
@@ -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 {...this.props} />;
}
}
WrappedComponent.graphqlExtension = extension;
return WithGraphQLExtension;
});
+19 -4
View File
@@ -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 */
<Broadcast channel="queryData" value={props.data}>
<WrappedComponent {...props} />
</Broadcast>
/* 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;
};
@@ -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 (
<Subscriber channel="queryData">
{data => (
<WrappedComponent {...this.props} refetch={get(data, 'refetch')} />
)}
</Subscriber>
);
}
}
return WithRefetch;
});
@@ -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(<DefaultComponent key="default" {...p} />);
}
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 <WrappedComponent {...props} />;
}
};
});
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));
};
@@ -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 (
<Subscriber channel="queryData">
{data => (
<WrappedComponent
{...this.props}
subscribeToMore={get(data, 'subscribeToMoreThrottled')}
/>
)}
</Subscriber>
);
}
}
return WithSubscribeToMore;
});
@@ -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 (
<Subscriber channel="queryData">
{data => (
<WrappedComponent
{...this.props}
variables={get(data, 'variables')}
/>
)}
</Subscriber>
);
}
}
return WithVariables;
});
+98 -64
View File
@@ -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() {