Implement excludeIf for the plugins API

This commit is contained in:
Chi Vinh Le
2017-07-14 22:43:47 +07:00
parent 562f2350c1
commit fb2662572a
17 changed files with 281 additions and 72 deletions
@@ -6,6 +6,8 @@ import t from 'coral-framework/services/i18n';
import {TabBar, Tab, TabContent, TabPane} from 'coral-ui';
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
import Popup from 'coral-framework/components/Popup';
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
import ConfigureStreamContainer
from 'coral-configure/containers/ConfigureStreamContainer';
import cn from 'classnames';
@@ -26,12 +28,24 @@ export default class Embed extends React.Component {
};
render() {
const {activeTab, commentId} = this.props;
const {activeTab, commentId, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog} = this.props;
const {user} = this.props.auth;
const hasHighlightedComment = !!commentId;
return (
<div className={cn('talk-embed-stream', {'talk-embed-stream-highlight-comment': hasHighlightedComment})}>
<IfSlotIsNotEmpty slot="login">
<Popup
href='/embed/stream/login'
title='Login'
features='menubar=0,resizable=0,width=500,height=550,top=200,left=500'
open={showSignInDialog}
focus={signInDialogFocus}
onFocus={focusSignInDialog}
onBlur={blurSignInDialog}
onClose={hideSignInDialog}
/>
</IfSlotIsNotEmpty>
<TabBar
onTabClick={this.changeTab}
activeTab={activeTab}
@@ -75,6 +75,7 @@ class Stream extends React.Component {
viewAllComments,
auth: {loggedIn, user},
removeTag,
pluginConfig,
editName
} = this.props;
const {keepCommentBox} = this.state;
@@ -98,6 +99,7 @@ class Stream extends React.Component {
};
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
const streamTabProps = {data, root, asset};
if (!comment && !comments) {
console.error('Talk: No comments came back from the graph given that query. Please, check the query params.');
@@ -207,13 +209,11 @@ class Stream extends React.Component {
<Slot fill="streamFilter" />
</div>
<TabBar activeTab={activeStreamTab} onTabClick={setActiveStreamTab} sub>
{getSlotComponents('streamTabs').map((PluginComponent) => (
{getSlotComponents('streamTabs', pluginConfig, streamTabProps).map((PluginComponent) => (
<Tab tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
<PluginComponent
{...streamTabProps}
active={activeStreamTab === PluginComponent.talkPluginName}
data={data}
root={root}
asset={asset}
/>
</Tab>
))}
@@ -222,12 +222,10 @@ class Stream extends React.Component {
</Tab>
</TabBar>
<TabContent activeTab={activeStreamTab} sub>
{getSlotComponents('streamTabPanes').map((PluginComponent) => (
{getSlotComponents('streamTabPanes', pluginConfig, streamTabProps).map((PluginComponent) => (
<TabPane tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
<PluginComponent
data={data}
root={root}
asset={asset}
{...streamTabProps}
/>
</TabPane>
))}
@@ -19,7 +19,7 @@ import t from 'coral-framework/services/i18n';
import {setActiveTab} from '../actions/embed';
const {logout, checkLogin} = authActions;
const {logout, checkLogin, focusSignInDialog, blurSignInDialog, hideSignInDialog} = authActions;
const {fetchAssetSuccess} = assetActions;
class EmbedContainer extends React.Component {
@@ -185,6 +185,9 @@ const mapDispatchToProps = (dispatch) =>
setActiveTab,
fetchAssetSuccess,
addNotification,
focusSignInDialog,
blurSignInDialog,
hideSignInDialog,
},
dispatch
);
@@ -310,7 +310,8 @@ const mapStateToProps = (state) => ({
previousTab: state.embed.previousTab,
activeStreamTab: state.stream.activeTab,
previousStreamTab: state.stream.previousTab,
commentClassNames: state.stream.commentClassNames
commentClassNames: state.stream.commentClassNames,
pluginConfig: state.config.plugin_config,
});
const mapDispatchToProps = (dispatch) =>
+21 -33
View File
@@ -7,45 +7,33 @@ import pym from '../services/pym';
import {resetWebsocket} from 'coral-framework/services/client';
import t from 'coral-framework/services/i18n';
import {isSlotEmpty} from 'plugin-api/beta/client/services';
export const showSignInDialog = () => (dispatch, getState) => {
if (isSlotEmpty('login')) {
return;
}
const signInPopUp = window.open(
'/embed/stream/login',
'Login',
'menubar=0,resizable=0,width=500,height=550,top=200,left=500'
);
export const showSignInDialog = () => ({
type: actions.SHOW_SIGNIN_DIALOG,
});
// Workaround odd behavior in older WebKit versions, where
// onunload is called twice. (Encountered in IOS 8.3)
let loaded = false;
signInPopUp.onload = () => {
loaded = true;
// Fire some actions inside the popups reducer, to initialize required state.
const required = getState().asset.toJS().settings.requireEmailConfirmation;
const redirectUri = getState().auth.toJS().redirectUri;
signInPopUp.coralStore.dispatch(setRequireEmailVerification(required));
signInPopUp.coralStore.dispatch(setRedirectUri(redirectUri));
};
// Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari.
signInPopUp.onunload = () => {
if (loaded) {
dispatch(checkLogin());
}
};
dispatch({type: actions.SHOW_SIGNIN_DIALOG});
};
export const hideSignInDialog = () => (dispatch) => {
if (window.opener && window.opener !== window) {
// TODO: We need to address this when we refactor the
// login popup out of the embed.
// we are in a popup
window.close();
} else {
dispatch(checkLogin());
}
dispatch({type: actions.HIDE_SIGNIN_DIALOG});
window.close();
};
export const focusSignInDialog = () => ({
type: actions.FOCUS_SIGNIN_DIALOG,
});
export const blurSignInDialog = () => ({
type: actions.BLUR_SIGNIN_DIALOG,
});
export const createUsernameRequest = () => ({
type: actions.CREATE_USERNAME_REQUEST
});
@@ -0,0 +1,22 @@
import React from 'react';
import {connect} from 'react-redux';
import {isSlotEmpty} from 'coral-framework/helpers/plugins';
import PropTypes from 'prop-types';
function IfSlotIsEmpty({slot, className, pluginConfig, component: Component = 'div', children, ...rest}) {
return (
<Component className={className}>
{isSlotEmpty(slot, pluginConfig, rest) ? children : null}
</Component>
);
}
IfSlotIsEmpty.propTypes = {
slot: PropTypes.string,
className: PropTypes.string,
};
const mapStateToProps = (state) => ({pluginConfig: state.config.plugin_config});
export default connect(mapStateToProps, null)(IfSlotIsEmpty);
@@ -0,0 +1,22 @@
import React from 'react';
import {connect} from 'react-redux';
import {isSlotEmpty} from 'coral-framework/helpers/plugins';
import PropTypes from 'prop-types';
function IfSlotIsNotEmpty({slot, className, pluginConfig, component: Component = 'div', children, ...rest}) {
return (
<Component className={className}>
{!isSlotEmpty(slot, pluginConfig, rest) ? children : null}
</Component>
);
}
IfSlotIsNotEmpty.propTypes = {
slot: PropTypes.string,
className: PropTypes.string,
};
const mapStateToProps = (state) => ({pluginConfig: state.config.plugin_config});
export default connect(mapStateToProps, null)(IfSlotIsNotEmpty);
+152
View File
@@ -0,0 +1,152 @@
import {Component} from 'react';
import PropTypes from 'prop-types';
export default class Popup extends Component {
ref = null;
detectCloseInterval = null;
constructor(props) {
super(props);
if (props.open) {
this.openWindow(props);
}
}
openWindow(props = this.props) {
this.ref = window.open(
props.href,
props.title,
props.features,
);
this.setCallbacks();
}
setCallbacks() {
this.ref.onload = () => {
clearInterval(this.detectCloseInterval);
this.onLoad();
};
this.ref.onfocus = () => {
this.onFocus();
};
this.ref.onblur = () => {
this.onBlur();
};
// Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari.
this.ref.onunload = () => {
this.onUnload();
const interval = setInterval(() => {
if (this.ref.onload === null) {
this.setCallbacks();
clearInterval(interval);
}
}, 50);
this.detectCloseInterval = setInterval(() => {
if (this.ref.closed) {
clearInterval(this.detectCloseInterval);
this.onClose();
}
}, 50);
};
}
closeWindow() {
if (this.ref) {
if (!this.ref.closed) {
this.ref.close();
}
this.ref = null;
}
}
focusWindow() {
if (this.ref && !this.ref.closed) {
this.ref.focus();
}
}
blurWindow() {
if (this.ref && !this.ref.closed) {
this.ref.blur();
}
}
onLoad = () => {
if (this.props.onLoad) {
this.props.onLoad();
}
}
onUnload = () => {
if (this.props.onUnload) {
this.props.onUnload();
}
}
onClose = () => {
if (this.props.onClose) {
this.props.onClose();
}
}
onFocus = () => {
if (this.props.onFocus) {
this.props.onFocus();
}
}
onBlur = () => {
if (this.props.onBlur) {
this.props.onBlur();
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.open && !this.ref) {
this.openWindow(nextProps);
}
if (this.props.open && !nextProps.open) {
this.closeWindow();
}
if (!this.props.focus && nextProps.focus) {
this.focusWindow();
}
if (this.props.focus && !nextProps.focus) {
this.blurWindow();
}
if (this.props.href !== nextProps.href) {
this.ref.location.href = nextProps.href;
}
}
componentWillUnmount() {
this.closeWindow();
}
render() {
return null;
}
}
Popup.propTypes = {
open: PropTypes.bool,
focus: PropTypes.bool,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
onLoad: PropTypes.func,
onUnload: PropTypes.func,
onClose: PropTypes.func,
href: PropTypes.string.isRequired,
features: PropTypes.string,
};
+4 -4
View File
@@ -4,14 +4,14 @@ import styles from './Slot.css';
import {connect} from 'react-redux';
import {getSlotElements} from 'coral-framework/helpers/plugins';
function Slot ({fill, inline = false, className, plugin_config: config, defaultComponent: DefaultComponent, ...rest}) {
let children = getSlotElements(fill, {...rest, config});
function Slot ({fill, inline = false, className, pluginConfig = {}, defaultComponent: DefaultComponent, ...rest}) {
let children = getSlotElements(fill, pluginConfig, rest);
if (children.length === 0 && DefaultComponent) {
children = <DefaultComponent {...rest} />;
}
return (
<div className={cn({[styles.inline]: inline, [styles.debug]: config.debug}, className)}>
<div className={cn({[styles.inline]: inline, [styles.debug]: pluginConfig.debug}, className)}>
{children}
</div>
);
@@ -21,7 +21,7 @@ Slot.propTypes = {
fill: React.PropTypes.string
};
const mapStateToProps = ({config: {plugin_config = {}}}) => ({plugin_config});
const mapStateToProps = (state) => ({pluginConfig: state.config.plugin_config});
export default connect(mapStateToProps, null)(Slot);
+2
View File
@@ -3,6 +3,8 @@ export const CLEAN_STATE = 'CLEAN_STATE';
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
export const FOCUS_SIGNIN_DIALOG = 'FOCUS_SIGNIN_DIALOG';
export const BLUR_SIGNIN_DIALOG = 'BLUR_SIGNIN_DIALOG';
export const CREATE_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST';
export const CREATE_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS';
+10 -11
View File
@@ -7,32 +7,31 @@ import flatten from 'lodash/flatten';
import flattenDeep from 'lodash/flattenDeep';
import {getDefinitionName, mergeDocuments} from 'coral-framework/utils';
import {loadTranslations} from 'coral-framework/services/i18n';
import {injectReducers, getStore} from 'coral-framework/services/store';
import {injectReducers} from 'coral-framework/services/store';
import camelize from './camelize';
export function getSlotComponents(slot) {
const pluginConfig = getStore().getState().config.plugin_config;
export function getSlotComponents(slot, pluginConfig, props = {}) {
return flatten(plugins
// Filter out components that have slots and have been disabled in `plugin_config`
// Filter out components that have slots and have been disabled in `plugin_config`
.filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components))
.filter((o) => o.module.slots[slot])
.map((o) => o.module.slots[slot])
);
)
.filter((component) => !component.isExcluded || !component.isExcluded({...props, config: pluginConfig}));
}
export function isSlotEmpty(slot) {
return getSlotComponents(slot).length === 0;
export function isSlotEmpty(slot, pluginConfig, props) {
return getSlotComponents(slot, pluginConfig, props).length === 0;
}
/**
* Returns React Elements for given slot.
*/
export function getSlotElements(slot, props = {}) {
return getSlotComponents(slot)
.map((component, i) => React.createElement(component, {key: i, ...props}));
export function getSlotElements(slot, pluginConfig, props = {}) {
return getSlotComponents(slot, pluginConfig, props)
.map((component, i) => React.createElement(component, {key: i, ...props, config: pluginConfig}));
}
function getComponentFragments(components) {
+4
View File
@@ -0,0 +1,4 @@
export default (condition) => (BaseComponent) => {
BaseComponent.isExcluded = condition;
return BaseComponent;
};
+3 -12
View File
@@ -1,14 +1,5 @@
import React from 'react';
import {getDisplayName} from '../helpers/hoc';
// TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38.
export default (fragments) => (WrappedComponent) => {
class WithFragments extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
WithFragments.fragments = fragments;
WithFragments.displayName = `WithFragments(${getDisplayName(WrappedComponent)})`;
return WithFragments;
export default (fragments) => (BaseComponent) => {
BaseComponent.fragments = fragments;
return BaseComponent;
};
+10 -1
View File
@@ -7,6 +7,7 @@ const initialState = Map({
loggedIn: false,
user: null,
showSignInDialog: false,
signInDialogFocus: false,
showCreateUsernameDialog: false,
checkedInitialLogin: false,
view: 'SIGNIN',
@@ -29,13 +30,21 @@ const purge = (user) => {
export default function auth (state = initialState, action) {
switch (action.type) {
case actions.FOCUS_SIGNIN_DIALOG:
return state
.set('signInDialogFocus', true);
case actions.BLUR_SIGNIN_DIALOG:
return state
.set('signInDialogFocus', false);
case actions.SHOW_SIGNIN_DIALOG :
return state
.set('showSignInDialog', true);
.set('showSignInDialog', true)
.set('signInDialogFocus', true);
case actions.HIDE_SIGNIN_DIALOG :
return state.merge(Map({
isLoading: false,
showSignInDialog: false,
signInDialogFocus: false,
view: 'SIGNIN',
error: '',
passwordRequestFailure: null,
+1
View File
@@ -1,2 +1,3 @@
export {default as withReaction} from './withReaction';
export {default as withFragments} from 'coral-framework/hocs/withFragments';
export {default as excludeIf} from 'coral-framework/hocs/excludeIf';
@@ -0,0 +1 @@
export const pluginConfigSelector = (state) => state.config.pluginConfig;
@@ -1,5 +1,6 @@
import {compose, gql} from 'react-apollo';
import withFragments from 'coral-framework/hocs/withFragments';
import excludeIf from 'coral-framework/hocs/excludeIf';
import Tab from '../components/Tab';
// TODO: This is just example code, and needs to replaced by an actual implementation.
@@ -12,6 +13,7 @@ const enhance = compose(
}
}`,
}),
excludeIf((props) => props.asset.recentComments.length === 0)
);
export default enhance(Tab);