Merge branch 'master' into coral-rte

This commit is contained in:
Chi Vinh Le
2018-03-21 12:59:21 +01:00
143 changed files with 1995 additions and 1657 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ jobs:
- save_cache:
key: build-cache-{{ .Branch }}-{{ .Revision }}
paths:
- ./node_modules/.cache/hard-source
- ./node_modules/.cache/babel-loader
- persist_to_workspace:
root: .
paths: dist
-2
View File
@@ -62,8 +62,6 @@ plugins/*
!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-viewing-options
!plugins/talk-plugin-rich-text
!plugins/talk-plugin-rich-text-coral
!plugins/talk-plugin-rich-text-pell
**/node_modules/*
yarn-error.log
+7 -3
View File
@@ -16,15 +16,19 @@ From getting up and running, to advanced configuration, to how to scale Talk, ou
## Product Guide
Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https:/docs.coralproject.net/talk/how-talk-works).
Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https://docs.coralproject.net/talk/how-talk-works).
## Relevant Links
## Pre-Launch Guide
Youve installed Talk on your server, and youre preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://blog.coralproject.net/youve-installed-talk-now-what/).
## More Resources
- [Talk Product Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
- [Our Blog](https://blog.coralproject.net/)
- [Community Forums](https://community.coralproject.net/)
- [Community Guides for Journalism](https://guides.coralproject.net/)
- [More About Us](https://coralproject.net/)
- [Talk Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
## End-to-End Testing
@@ -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);
@@ -8,7 +8,7 @@ const initialState = {
errors: {},
};
export default function config(state = initialState, action) {
export default function configure(state = initialState, action) {
switch (action.type) {
case actions.UPDATE_PENDING: {
let next = state;
@@ -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,7 +25,8 @@ class QuestionBoxBuilder extends React.Component {
async loadEditor() {
const {
default: MarkdownEditor,
} = await import('coral-framework/components/MarkdownEditor');
} = await import(/* webpackChunkName: "markdownEditor" */
'coral-framework/components/MarkdownEditor');
return this.setState({
loading: false,
@@ -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}
@@ -465,7 +465,6 @@ const mapStateToProps = state => ({
activeStreamTab: state.stream.activeTab,
previousStreamTab: state.stream.previousTab,
commentClassNames: state.stream.commentClassNames,
pluginConfig: state.config.plugin_config,
sortOrder: state.stream.sortOrder,
sortBy: state.stream.sortBy,
});
+8
View File
@@ -161,6 +161,14 @@ export default class Stream {
);
}
enablePluginsDebug() {
this.pym.sendMessage('enablePluginsDebug');
}
disablePluginsDebug() {
this.pym.sendMessage('disablePluginsDebug');
}
login(token) {
this.pym.sendMessage('login', token);
}
+12
View File
@@ -7,6 +7,10 @@ export default class StreamInterface {
return this._stream.emitter.on(eventName, callback);
}
off(eventName, callback) {
return this._stream.emitter.off(eventName, callback);
}
login(token) {
return this._stream.login(token);
}
@@ -18,4 +22,12 @@ export default class StreamInterface {
remove() {
return this._stream.remove();
}
enablePluginsDebug() {
return this._stream.enablePluginsDebug();
}
disablePluginsDebug() {
return this._stream.disablePluginsDebug();
}
}
+13 -1
View File
@@ -1,6 +1,18 @@
import { MERGE_CONFIG } from '../constants/config';
import {
MERGE_CONFIG,
ENABLE_PLUGINS_DEBUG,
DISABLE_PLUGINS_DEBUG,
} from '../constants/config';
export const mergeConfig = config => ({
type: MERGE_CONFIG,
config,
});
export const enablePluginsDebug = () => ({
type: ENABLE_PLUGINS_DEBUG,
});
export const disablePluginsDebug = () => ({
type: DISABLE_PLUGINS_DEBUG,
});
@@ -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);
+32 -1
View File
@@ -3,5 +3,36 @@
}
.debug {
background-color: coral;
background-color: #e2e2e2;
border-style: dotted solid;
border-width: 2px;
border: dotted 2px coral;
padding: 2px;
margin: 1px;
position: relative;
}
.debug::before {
content: attr(data-slot-name);
display: inline-block;
position: absolute;
background: #000;
color: #FFF;
padding: 5px;
border-radius: 5px;
opacity: 0;
transition: 0.3s;
overflow: hidden;
pointer-events: none;
z-index: 999!important;
white-space: pre-wrap;
min-height: 16px;
top: 50%;
left: 0;
}
.debug:hover::before {
opacity: 1;
top: 100%;
}
+34 -74
View File
@@ -4,86 +4,21 @@ 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';
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();
const pluginConfig =
get(reduxState, 'config.plugins_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);
}
@@ -91,10 +26,11 @@ class Slot extends React.Component {
return (
<Component
className={cn(
{ [styles.inline]: inline, [styles.debug]: pluginConfig.debug },
{ [styles.inline]: inline, [styles.debug]: debug },
className,
`talk-slot-${kebabCase(fill)}`
)}
data-slot-name={fill}
>
{children}
</Component>
@@ -110,13 +46,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.
@@ -125,8 +62,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
@@ -140,8 +81,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);
@@ -1,3 +1,5 @@
const prefix = `TALK_FRAMEWORK`;
export const MERGE_CONFIG = `${prefix}_MERGE_CONFIG`;
export const ENABLE_PLUGINS_DEBUG = `${prefix}_ENABLE_PLUGINS_DEBUG`;
export const DISABLE_PLUGINS_DEBUG = `${prefix}_DISABLE_PLUGINS_DEBUG`;
+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,202 @@
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') {
// If config changed, we'll have to rerender everything.
// Should only happen during development as this is
// usually static.
if (this.props.reduxState.config !== next.reduxState.config) {
return true;
}
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;
});
+21 -1
View File
@@ -1,10 +1,30 @@
import { MERGE_CONFIG } from '../constants/config';
import {
MERGE_CONFIG,
ENABLE_PLUGINS_DEBUG,
DISABLE_PLUGINS_DEBUG,
} from '../constants/config';
import { LOGOUT } from '../constants/auth';
const initialState = {};
export default function config(state = initialState, action) {
switch (action.type) {
case ENABLE_PLUGINS_DEBUG:
return {
...state,
plugins_config: {
...state.plugins_config,
debug: true,
},
};
case DISABLE_PLUGINS_DEBUG:
return {
...state,
plugins_config: {
...state.plugins_config,
debug: false,
},
};
case LOGOUT:
return {
...state,
+26 -3
View File
@@ -25,7 +25,11 @@ import { createIntrospection } from 'coral-framework/services/introspection';
import introspectionData from 'coral-framework/graphql/introspection.json';
import coreReducers from '../reducers';
import { checkLogin as checkLoginAction } from '../actions/auth';
import { mergeConfig } from '../actions/config';
import {
mergeConfig,
enablePluginsDebug,
disablePluginsDebug,
} from '../actions/config';
import { setAuthToken, logout } from '../actions/auth';
/**
@@ -62,8 +66,19 @@ function initExternalConfig({ store, pym, inIframe }) {
}
return new Promise(resolve => {
pym.sendMessage('getConfig');
pym.onMessage('config', config => {
store.dispatch(mergeConfig(JSON.parse(config)));
pym.onMessage('config', rawConfig => {
const config = JSON.parse(rawConfig);
if (config.plugin_config) {
// @Deprecated
if (process.env.NODE_ENV !== 'production') {
console.warn(
'Deprecation Warning: `config.plugin_config` will be phased out soon, please replace `config.plugin_config with `config.plugins_config`'
);
}
config.plugins_config = config.plugin_config;
delete config.plugin_config;
}
store.dispatch(mergeConfig(config));
resolve();
});
});
@@ -215,6 +230,14 @@ export async function createContext({
pym.onMessage('logout', () => {
store.dispatch(logout());
});
pym.onMessage('enablePluginsDebug', () => {
store.dispatch(enablePluginsDebug());
});
pym.onMessage('disablePluginsDebug', () => {
store.dispatch(disablePluginsDebug());
});
}
const preInitList = [];
+68 -37
View File
@@ -7,10 +7,11 @@ 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';
// This is returned for pluginConfig when it is empty.
// This is returned for pluginsConfig when it is empty.
const emptyConfig = {};
// Memoize the warnings so we only show them once.
@@ -67,55 +68,83 @@ function addMetaDataToSlotComponents(plugins) {
});
}
/**
* 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) {
const pluginsConfig = get(reduxState, 'config.plugins_config') || emptyConfig;
return {
...props,
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.
*/
getSlotComponentProps(component, reduxState, props, queryData) {
const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig;
return {
...props,
config: pluginConfig,
...(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 = {}) {
const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig;
const { slotSize = 0 } = options;
getSlotElements(slot, reduxState, props = {}, options = {}) {
const pluginsConfig =
get(reduxState, 'config.plugins_config') || emptyConfig;
const { size = 0 } = options;
const { queryData, rest } = splitProps(props);
const isDisabled = component => {
if (
pluginConfig &&
pluginConfig[component.talkPluginName] &&
pluginConfig[component.talkPluginName].disable_components
pluginsConfig &&
pluginsConfig[component.talkPluginName] &&
pluginsConfig[component.talkPluginName].disable_components
) {
return true;
}
// Check if component is excluded.
if (component.isExcluded) {
let resolvedProps = this.getSlotComponentProps(
let resolvedProps = getSlotComponentProps(
component,
reduxState,
props,
rest,
queryData
);
if (component.mapStateToProps) {
@@ -136,15 +165,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),
@@ -154,12 +183,7 @@ class PluginsService {
.map(({ component, key }) =>
React.createElement(component, {
key,
...this.getSlotComponentProps(
component,
reduxState,
props,
queryData
),
...getSlotComponentProps(component, reduxState, rest, queryData),
})
);
}
@@ -185,9 +209,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() {
+2
View File
@@ -136,6 +136,8 @@ sidebar:
url: /plugins-directory/
- title: Plugin Recipes
url: /plugin-recipes/
- title: Slots and Plugins
url: /slots-and-plugins/
- title: Tutorials
children:
- title: Creating a Basic Plugin
+169
View File
@@ -0,0 +1,169 @@
---
title: Slots and Plugins
permalink: /slots-and-plugins/
---
Plugins make use of **"slots"** in order to change Talk's interface.
By default, Talk has various plugins provided by default. We can see this in `plugins.default.json`:
```json
{
"server": [
"talk-plugin-auth",
"talk-plugin-featured-comments",
"talk-plugin-offtopic",
"talk-plugin-respect"
],
"client": [
"talk-plugin-auth",
"talk-plugin-author-menu",
"talk-plugin-comment-content",
"talk-plugin-featured-comments",
"talk-plugin-flag-details",
"talk-plugin-ignore-user",
"talk-plugin-member-since",
"talk-plugin-moderation-actions",
"talk-plugin-offtopic",
"talk-plugin-permalink",
"talk-plugin-respect",
"talk-plugin-sort-most-replied",
"talk-plugin-sort-most-respected",
"talk-plugin-sort-newest",
"talk-plugin-sort-oldest",
"talk-plugin-viewing-options",
"talk-plugin-profile-settings"
]
}
```
Let's only focus on the plugins which are listed under `client` - these are the plugins that use *slots* to inject certain functionality into the Talk UI.
For example, if we look at the Respect plugin (`talk-plugin-respect`), we can see its `client/index.js` looks like this:
```js
import RespectButton from './RespectButton';
import translations from './translations.yml';
export default {
translations,
slots: {
commentReactions: [RespectButton],
},
};
```
Inside the `slots` property, we specify which **slots** the plugin will use. Above we are saying that the `RespectButton` component is being injected into the slot `commentReactions`.
Slots can receive an Array of components, so we can use one plugin or many for one slot.
### Anatomy of the Slot Component
In Talk core, we have 32 slots available for us to use. The component `Slot` has a `fill` property where we establish the name of the slot. It looks like this:
```js
<Slot
fill="commentReactions"
{...props}
/>
```
You won't have to use this to build plugins, but it's helpful to find where to embed your plugin.
### Slot List
* `adminCommentDetailArea`
* `adminCommentMoreDetails`
* `adminCommentLabels`
* `adminModerationSettings`
* `adminStreamSettings`
* `adminTechSettings`
* `adminCommentInfoBar`
* `adminCommentContent`
* `adminSideActions`
* `adminModeration`
* `adminModerationIndicator`
* `commentInputDetailArea`
* `commentAvatar`
* `commentAuthorName`
* `commentAuthorTags`
* `commentTimestamp`
* `commentInfoBar`
* `commentContent`
* `commentReactions`
* `commentActions`
* `commentInputArea`
* `draftArea`
* `streamSettings`
* `historyCommentTimestamp`
* `profileSections`
* `embed`
* `stream`
* `streamFilter`
* `streamQuestionArea`
* `login`
* `userProfile`
* `userDetailCommentContent`
### Where should I insert my plugin?
The first thing we should consider is what components will be affected by our plugin's functionality. For example, if we want to add functionality to all the comments that are rendered in a total list of comments, we would use the component `Comment`.
The slots that are able to add functionality to comments start with `comment`, like `commentContent`, or `commentReactions`, as you can see above.
### Disabling plugins via `plugins_config`
Typically, you will manage plugins via your `plugins.json` file. If you want to remove a plugin, you would simply delete it there. However, we can also do this directly with the `plugins_config`.
Let's look at our example article, `views/article.ejs`. Here we see we have the Talk embed, and within the embed, we can also send a configuration object. To disable a plugin visually, we can pass `true` to the property `disable_components`. Like so:
```js
plugins_config: {
'talk-plugin-love': {
disable_components: true,
},
}
```
### Sending information to slots and plugins
Inside our `plugins_config`, we can also send properities and our plugins will receive them. For example, if we send this:
```js
plugins_config: {
test: 'data'
}
```
The plugin will receive a config object with the properties we've passed. If we do a `console.log` with `this.props`, we would see:
```js
config: {test: 'data'}
```
### Debugging slots and plugins
You can debug slots and plugins simply by passing the `debug` property with value `true`:
```js
plugins_config: {
debug: true
}
```
This will turn on a visual aid to show you all of Talk's available slots and their names. Just move your mouse around!
### Slot ClassNames
Slots autogenerate their classes with the prefix `talk-slot`, followed by the name of the slot in kebab case.
For example, the class autogenerated for the slot `commentContent` is `talk-slot-comment-content`.
+2 -2
View File
@@ -251,6 +251,6 @@ export default withReaction('pride')(PrideButton);
````
And that's it! You've created your first reaction button! :rainbow:
And that's it! You've created your first reaction button! 🌈
If you would like to continue to the next part of our Plugin Tutorial, see Part 2 in the sidebar.
If you would like to continue to the next part of our Plugin Tutorial, see Customizing Plugins with Coral UI in the left sidebar.
+1 -1
View File
@@ -352,7 +352,7 @@ ar:
personal_info: "هذا التعليق يكشف عن معلومات تعريف شخصية"
post: نشر
profile: الملف الشخصي
profile_settings: "إعدادات الملف الشخصي"
profile_settings: "إعدادات"
reply: رد
report: أبلغ
report_notif: "شكرا على الإبلاغ عن هذا التعليق. تم إبلاغ فريق الإشراف لدينا وسيراجعه قريبًا."
+3 -3
View File
@@ -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."
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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."
+1 -1
View File
@@ -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."
+1 -1
View File
@@ -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."
+2 -2
View File
@@ -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."
+1 -1
View File
@@ -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."
+1 -1
View File
@@ -351,7 +351,7 @@ zh_CN:
personal_info: "这个评论透露了可确定个人身份的信息"
post: "发表"
profile: "资料"
profile_settings: "资料设定"
profile_settings: "设置"
reply: "回复"
report: "举报"
report_notif: "感谢您举报此评论。我们的审核小组已经收到通知,并会很快进行审查。"
+1 -1
View File
@@ -351,7 +351,7 @@ zh_TW:
personal_info: "該評論洩露了個人身份資訊"
post: 帖子
profile: 概況
profile_settings: "概況設置"
profile_settings: "設置"
reply: 回覆
report: 舉報
report_notif: "感謝您舉報此評論。我們的審核小組已經收到通知,並會很快進行審查。"
+46
View File
@@ -1,4 +1,7 @@
const SettingsService = require('../services/settings');
const fs = require('fs');
const path = require('path');
const { merge } = require('lodash');
const {
BASE_URL,
@@ -34,6 +37,45 @@ const attachStaticLocals = locals => {
}
};
// MANIFESTS are all the manifests accessible by Talk.
const MANIFESTS = ['../dist/manifest.json', '../dist/manifest.embed.json'];
// getManifest will retrieve the manifest files and parse the JSON.
function getManifest() {
return merge(
{},
...MANIFESTS.map(f =>
fs.readFileSync(path.resolve(__dirname, f), 'utf8')
).map(JSON.parse)
);
}
/**
* resolve is a function that can be used in templates to resolve an asset from
* the manifest. In production, the manifest is cached.
*/
const resolve = (() => {
if (process.env.NODE_ENV === 'production') {
// In production, we should attempt to load the manifest early.
const manifest = getManifest();
return key => `${STATIC_URL}static/${manifest[key]}`;
}
// In dev mode, we are more forgiving and we always load the
// newest version of the manifest.
return key => {
try {
const manifest = getManifest();
return `${STATIC_URL}static/${manifest[key]}`;
} catch (err) {
console.warn(err);
return '';
}
};
})();
module.exports = async (req, res, next) => {
try {
// Attach the custom css url.
@@ -46,6 +88,10 @@ module.exports = async (req, res, next) => {
// Always attach the locals.
attachStaticLocals(res.locals);
// Resolve will help resolving paths to static files
// using the manifest.
res.locals.resolve = resolve;
// Forward the request.
next();
};
+4 -1
View File
@@ -116,7 +116,6 @@
"graphql-tag": "^1.2.3",
"graphql-tools": "^0.10.1",
"hammerjs": "^2.0.8",
"hard-source-webpack-plugin": "^0.6.0",
"helmet": "3.8.2",
"history": "^3.0.0",
"hjson": "^3.1.1",
@@ -160,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",
@@ -213,6 +213,7 @@
"enzyme-adapter-react-15": "^1.0.0",
"eslint": "^4.5.0",
"eslint-plugin-mocha": "^4.11.0",
"extract-text-webpack-plugin": "^3.0.2",
"husky": "^0.14.3",
"identity-obj-proxy": "^3.0.0",
"ip": "^1.1.5",
@@ -221,11 +222,13 @@
"lint-staged": "^7.0.0",
"mocha": "^3.1.2",
"mocha-junit-reporter": "^1.12.1",
"name-all-modules-plugin": "^1.0.1",
"nightwatch": "^0.9.16",
"nodemon": "^1.11.0",
"selenium-standalone": "^6.11.0",
"sinon": "^3.2.1",
"sinon-chai": "^2.13.0",
"webpack-manifest-plugin": "^2.0.0-rc.2",
"yaml-lint": "^1.0.0"
},
"engines": {
+6
View File
@@ -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';
+1 -1
View File
@@ -1 +1 @@
export const pluginConfigSelector = state => state.config.pluginConfig;
export const pluginsConfigSelector = state => state.config.plugins_config;
@@ -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}
>
<span className={styles.name}>{comment.user.username}</span>
<span className={styles.name}>{username}</span>
</button>
{menuVisible && (
<Menu
data={data}
root={root}
asset={asset}
comment={comment}
contentSlot={contentSlot}
/>
<Menu slotPassthrough={slotPassthrough} contentSlot={contentSlot} />
)}
</div>
</ClickOutside>
);
};
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;
@@ -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 (
<div className={cn(styles.menu, 'talk-plugin-author-menu-popup')}>
<Slot
fill={contentSlot}
data={data}
queryData={{ asset, root, comment }}
/>
<Slot fill={contentSlot} passthrough={slotPassthrough} />
</div>
);
}
@@ -21,15 +18,20 @@ export default ({ data, root, asset, comment, contentSlot }) => {
<Slot
className={cn('talk-plugin-author-menu-infos')}
fill={'authorMenuInfos'}
data={data}
queryData={{ asset, root, comment }}
passthrough={slotPassthrough}
/>
<Slot
className={cn(styles.actions, 'talk-plugin-author-menu-actions')}
fill={'authorMenuActions'}
data={data}
queryData={{ asset, root, comment }}
passthrough={slotPassthrough}
/>
</div>
);
};
Menu.propTypes = {
slotPassthrough: PropTypes.object.isRequired,
contentSlot: PropTypes.string,
};
export default Menu;
@@ -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 (
<AuthorName
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
comment={this.props.comment}
contentSlot={this.props.contentSlot}
menuVisible={this.props.showMenuForComment === this.props.comment.id}
username={comment.user.username}
contentSlot={contentSlot}
menuVisible={showMenuForComment === comment.id}
toggleMenu={this.toggleMenu}
hideMenu={this.hideMenu}
slotPassthrough={slotPassthrough}
/>
);
}
}
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 }) => ({
@@ -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 (
<div className={cn(styles.root, `${pluginName}-comment`)}>
<Slot
@@ -27,8 +27,7 @@ class Comment extends React.Component {
className={cn(styles.quote, `${pluginName}-comment-body`)}
fill="commentContent"
defaultComponent={CommentContent}
data={data}
queryData={queryData}
passthrough={slotPassthrough}
/>
<div className={cn(`${pluginName}-comment-username-box`)}>
@@ -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
/>
</div>
@@ -62,19 +58,11 @@ class Comment extends React.Component {
>
<Slot
fill="commentReactions"
root={root}
data={data}
comment={comment}
asset={asset}
passthrough={slotPassthrough}
inline
/>
<FeaturedButton
root={root}
data={data}
comment={comment}
asset={asset}
/>
<FeaturedButton root={root} comment={comment} asset={asset} />
</div>
<div
className={cn(
@@ -22,7 +22,6 @@ class TabPane extends React.Component {
render() {
const {
root,
data,
asset: { featuredComments, ...asset },
viewComment,
} = this.props;
@@ -32,7 +31,6 @@ class TabPane extends React.Component {
<Comment
key={comment.id}
root={root}
data={data}
comment={comment}
asset={asset}
viewComment={viewComment}
@@ -1,6 +1,7 @@
import React from 'react';
import { gql } from 'react-apollo';
import { subscriptionFields } from 'coral-admin/src/routes/Moderation/graphql';
import { compose, withSubscribeToMore } from 'plugin-api/beta/client/hocs';
class ModIndicatorSubscription extends React.Component {
subscriptions = null;
@@ -27,7 +28,7 @@ class ModIndicatorSubscription extends React.Component {
},
];
this.subscriptions = configs.map(config =>
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);
@@ -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);
@@ -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 {
@@ -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 && (
<IfSlotIsNotEmpty
slot="adminCommentMoreFlagDetails"
queryData={queryData}
passthrough={slotPassthrough}
>
<Slot
fill="adminCommentMoreFlagDetails"
data={data}
queryData={queryData}
passthrough={slotPassthrough}
/>
</IfSlotIsNotEmpty>
)}
@@ -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(
@@ -24,7 +24,7 @@ export default {
createComment: {
comment: {
user: {
created_at: new Date(),
created_at: new Date().toISOString(),
__typename: 'User',
},
__typename: 'Comment',
@@ -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 (
<ClickOutside onClickOutside={hideMenu}>
<div
@@ -45,8 +49,7 @@ export default class ModerationActions extends React.Component {
<Slot
className="talk-plugin-modetarion-actions-slot"
fill="moderationActions"
queryData={{ comment, asset }}
data={data}
passthrough={slotPassthrough}
/>
<ApproveCommentAction comment={comment} hideMenu={hideMenu} />
<RejectCommentAction comment={comment} hideMenu={hideMenu} />
@@ -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,
@@ -42,7 +42,6 @@ class ModerationActionsContainer extends React.Component {
render() {
return (
<ModerationActions
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
comment={this.props.comment}
@@ -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({ onFeatured: false });
if (this.getOnFeaturedSetting()) {
props.indicateOn();
}
}
componentWillReceiveProps(nextProps) {
const prevSetting = this.getOnFeaturedSetting(this.props);
const nextSetting = this.getOnFeaturedSetting(nextProps);
if (prevSetting && !nextSetting) {
nextProps.indicateOff();
} else if (!prevSetting && nextSetting) {
nextProps.indicateOn();
}
}
getOnFeaturedSetting = (props = this.props) =>
props.root.me.notificationSettings.onFeatured;
toggle = () => {
this.props.updateNotificationSettings({
onFeatured: !this.getOnFeaturedSetting(),
});
};
render() {
return (
<Toggle
checked={this.getOnFeaturedSetting()}
onChange={this.toggle}
disabled={this.props.disabled}
>
{t('talk-plugin-notifications-category-featured.toggle_description')}
</Toggle>
);
}
}
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);
@@ -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 });
},
}),
},
};
@@ -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,
};
@@ -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 (
<Toggle
checked={this.getOnReplySetting()}
onChange={this.toggle}
disabled={this.props.disabled}
>
{t('talk-plugin-notifications-category-reply.toggle_description')}
</Toggle>
);
}
}
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);
@@ -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 });
},
}),
},
};

Some files were not shown because too many files have changed in this diff Show More