diff --git a/client/coral-admin/src/components/CommentAnimatedEdit.css b/client/coral-admin/src/components/CommentAnimatedEdit.css index 83be36c11..a03f66fc5 100644 --- a/client/coral-admin/src/components/CommentAnimatedEdit.css +++ b/client/coral-admin/src/components/CommentAnimatedEdit.css @@ -1,3 +1,7 @@ +.root { + position: relative; +} + .bodyLeave { position: absolute; width: 100%; diff --git a/client/coral-admin/src/components/CommentAnimatedEdit.js b/client/coral-admin/src/components/CommentAnimatedEdit.js index 141dc12cc..2515d7d94 100644 --- a/client/coral-admin/src/components/CommentAnimatedEdit.js +++ b/client/coral-admin/src/components/CommentAnimatedEdit.js @@ -7,6 +7,7 @@ export default ({children, body}) => { return ( cmp.talkUuid); - const nextUuid = this.getSlotComponents(next.tabSlot, next).map((cmp) => cmp.talkUuid); - return !isEqual(prevUuid, nextUuid); + 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. @@ -37,42 +37,33 @@ class StreamTabPanelContainer extends React.Component { fallbackAllTab(props = this.props) { if (props.activeTab !== props.fallbackTab) { - const slotPlugins = this.getSlotComponents(props.tabSlot, props).map((c) => c.talkPluginName); + const slotPlugins = this.getSlotElements(props.tabSlot, props).map((el) => el.type.talkPluginName); if (slotPlugins.indexOf(props.activeTab) === -1) { props.setActiveTab(props.fallbackTab); } } } - getSlotComponents(slot, props = this.props) { + getSlotElements(slot, props = this.props) { const {plugins} = this.context; - return plugins.getSlotComponents(slot, props.reduxState, props.slotProps, props.queryData); + return plugins.getSlotElements(slot, props.reduxState, props.slotProps, props.queryData); } getPluginTabElements(props = this.props) { - const {plugins} = this.context; - return this.getSlotComponents(props.tabSlot).map((PluginComponent) => { - const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData); + return this.getSlotElements(props.tabSlot).map((el) => { return ( - - + + {React.cloneElement(el, {active: this.props.activeTab === el.type.talkPluginName})} ); }); } getPluginTabPaneElements(props = this.props) { - const {plugins} = this.context; - return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => { - const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData); + return this.getSlotElements(props.tabPaneSlot).map((el) => { return ( - - + + {el} ); }); diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index 06f1d4cb7..0fe0d8896 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -20,9 +20,9 @@ class Slot extends React.Component { // it does not result in a change of slot children. const changes = getShallowChanges(this.props, next); if (changes.length === 1 && changes[0] === 'reduxState') { - const prevChildrenUuid = this.getChildren(this.props).map((child) => child.type.talkUuid); - const nextChildrenUuid = this.getChildren(next).map((child) => child.type.talkUuid); - return !isEqual(prevChildrenUuid, nextChildrenUuid); + 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. diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js index dbda672ba..c8eb737fd 100644 --- a/client/coral-framework/services/plugins.js +++ b/client/coral-framework/services/plugins.js @@ -8,7 +8,6 @@ import flatten from 'lodash/flatten'; import mapValues from 'lodash/mapValues'; import {getDisplayName} from 'coral-framework/helpers/hoc'; import camelize from '../helpers/camelize'; -import uuid from 'uuid/v4'; // This is returned for pluginConfig when it is empty. const emptyConfig = {}; @@ -63,9 +62,6 @@ function addMetaDataToSlotComponents(plugins) { // Attach plugin name to the component component.talkPluginName = plugin.name; - - // Attach uuid to the component - component.talkUuid = uuid(); }); }); }); @@ -77,30 +73,8 @@ class PluginsService { addMetaDataToSlotComponents(plugins); } - getSlotComponents(slot, reduxState, props = {}, queryData = {}) { - const pluginConfig = reduxState.config.plugin_config || emptyConfig; - return flatten(this.plugins - - // 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) => { - if(!component.isExcluded) { - return true; - } - let resolvedProps = this.getSlotComponentProps(component, reduxState, props, queryData); - if (component.mapStateToProps) { - resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)}; - } - return !component.isExcluded(resolvedProps); - }); - } - isSlotEmpty(slot, reduxState, props = {}, queryData = {}) { - return this.getSlotComponents(slot, reduxState, props, queryData).length === 0; + return this.getSlotElements(slot, reduxState, props, queryData).length === 0; } /** @@ -124,10 +98,42 @@ class PluginsService { * Returns React Elements for given slot. */ getSlotElements(slot, reduxState, props = {}, queryData = {}) { - return this.getSlotComponents(slot, reduxState, props, queryData) - .map((component, i) => { - return React.createElement(component, {key: i, ...this.getSlotComponentProps(component, reduxState, props, queryData)}); - }); + const pluginConfig = reduxState.config.plugin_config || emptyConfig; + + const isDisabled = (component) => { + if ( + pluginConfig && + pluginConfig[component.talkPluginName] && + pluginConfig[component.talkPluginName].disable_components + ) { + return true; + } + + // Check if component is excluded. + if(component.isExcluded) { + let resolvedProps = this.getSlotComponentProps(component, reduxState, props, queryData); + if (component.mapStateToProps) { + resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)}; + } + return component.isExcluded(resolvedProps); + } + + return false; + }; + + return flatten(this.plugins + .filter((o) => o.module.slots && o.module.slots[slot]) + .map((o) => o.module.slots[slot]) + ) + .map((component, i) => ({ + component, + disabled: isDisabled(component), + key: i, + })) + .filter((o) => !o.disabled) + .map(({component, key}) => + React.createElement(component, {key, ...this.getSlotComponentProps(component, reduxState, props, queryData)}) + ); } getSlotFragments(slot, part) { diff --git a/locales/en.yml b/locales/en.yml index 703de4b28..6d94393b2 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -322,7 +322,7 @@ en: to_access: "to access Profile" user_no_comment: "You've never left a comment. Join the conversation!" stream: - temporarily_suspended: "In accordance with {0}'s community guidlines, your account has been temporarily suspended. Please rejoin the conversation {1}." + temporarily_suspended: "In accordance with {0}'s community guidelines, your account has been temporarily suspended. Please rejoin the conversation {1}." step_1_header: "Report an issue" step_2_header: "Help us understand" step_3_header: "Thank you for your input" diff --git a/plugins/talk-plugin-featured-comments/client/components/Comment.js b/plugins/talk-plugin-featured-comments/client/components/Comment.js index 1c2045110..779844177 100644 --- a/plugins/talk-plugin-featured-comments/client/components/Comment.js +++ b/plugins/talk-plugin-featured-comments/client/components/Comment.js @@ -5,7 +5,7 @@ import {t, timeago} from 'plugin-api/beta/client/services'; import {Slot, CommentAuthorName} from 'plugin-api/beta/client/components'; import {Icon} from 'plugin-api/beta/client/components/ui'; import {pluginName} from '../../package.json'; -import Button from './Button'; +import FeaturedButton from '../containers/FeaturedButton'; class Comment extends React.Component { @@ -50,7 +50,7 @@ class Comment extends React.Component { inline /> - + isApproved(status) ? ( + + + {t('talk-plugin-moderation-actions.approved_comment')} + + ) : ( + + ) ); diff --git a/plugins/talk-plugin-moderation-actions/client/components/Tooltip.css b/plugins/talk-plugin-moderation-actions/client/components/Menu.css similarity index 95% rename from plugins/talk-plugin-moderation-actions/client/components/Tooltip.css rename to plugins/talk-plugin-moderation-actions/client/components/Menu.css index def7cac82..3ef43d82e 100644 --- a/plugins/talk-plugin-moderation-actions/client/components/Tooltip.css +++ b/plugins/talk-plugin-moderation-actions/client/components/Menu.css @@ -1,4 +1,4 @@ -.tooltip { +.menu { background-color: white; border: solid 1px #999; border-radius: 3px; @@ -14,7 +14,7 @@ color: #616161; } -.tooltip::before{ +.menu::before{ content: ''; border: 10px solid transparent; border-top-color: #999; @@ -24,7 +24,7 @@ transform: rotate(180deg); } -.tooltip::after{ +.menu::after{ content: ''; border: 10px solid transparent; border-top-color: white; diff --git a/plugins/talk-plugin-moderation-actions/client/components/Tooltip.js b/plugins/talk-plugin-moderation-actions/client/components/Menu.js similarity index 77% rename from plugins/talk-plugin-moderation-actions/client/components/Tooltip.js rename to plugins/talk-plugin-moderation-actions/client/components/Menu.js index db7e56c1e..470349b69 100644 --- a/plugins/talk-plugin-moderation-actions/client/components/Tooltip.js +++ b/plugins/talk-plugin-moderation-actions/client/components/Menu.js @@ -1,10 +1,10 @@ import React from 'react'; import cn from 'classnames'; -import styles from './Tooltip.css'; +import styles from './Menu.css'; import {t} from 'plugin-api/beta/client/services'; export default ({className = '', children}) => ( -
+

{t('talk-plugin-moderation-actions.moderation_actions')}

diff --git a/plugins/talk-plugin-moderation-actions/client/components/ModerationActions.js b/plugins/talk-plugin-moderation-actions/client/components/ModerationActions.js index 3a3b54156..a4a753d60 100644 --- a/plugins/talk-plugin-moderation-actions/client/components/ModerationActions.js +++ b/plugins/talk-plugin-moderation-actions/client/components/ModerationActions.js @@ -1,6 +1,6 @@ import React from 'react'; import cn from 'classnames'; -import Tooltip from './Tooltip'; +import Menu from './Menu'; import styles from './ModerationActions.css'; import {Icon} from 'plugin-api/beta/client/components/ui'; import ClickOutside from 'coral-framework/components/ClickOutside'; @@ -9,51 +9,27 @@ import ApproveCommentAction from '../containers/ApproveCommentAction'; import {Slot} from 'plugin-api/beta/client/components'; export default class ModerationActions extends React.Component { - constructor() { - super(); - - this.state = { - tooltip: false - }; - } - - toogleTooltip = () => { - const {tooltip} = this.state; - this.setState({ - tooltip: !tooltip - }); - } - - hideTooltip = () => { - this.setState({ - tooltip: false - }); - } - render() { - const {tooltip} = this.state; - const {comment, asset, data} = this.props; + const {comment, asset, data, menuVisible, toogleMenu, hideMenu} = this.props; return( - +
- - {tooltip ? : + + {menuVisible ? : } - {tooltip && ( - - + {menuVisible && ( + - - - - + + + )}
diff --git a/plugins/talk-plugin-moderation-actions/client/components/styles.css b/plugins/talk-plugin-moderation-actions/client/components/styles.css index 12ae4bad5..d5278a462 100644 --- a/plugins/talk-plugin-moderation-actions/client/components/styles.css +++ b/plugins/talk-plugin-moderation-actions/client/components/styles.css @@ -12,10 +12,10 @@ width: 100%; text-align: left; letter-spacing: 0.3px; +} - &:hover { - background-color: #D8D8D8; - } +.button:not(.approved):hover { + background-color: #D8D8D8; } .icon { @@ -24,6 +24,8 @@ } .approved { + display: inline-block; color: #519954; font-weight: bold; + padding: 6px; } \ No newline at end of file diff --git a/plugins/talk-plugin-moderation-actions/client/constants.js b/plugins/talk-plugin-moderation-actions/client/constants.js new file mode 100644 index 000000000..de3d5c909 --- /dev/null +++ b/plugins/talk-plugin-moderation-actions/client/constants.js @@ -0,0 +1,4 @@ +const prefix = 'TALK_MODERATION_ACTIONS'; + +export const OPEN_MENU = `${prefix}_OPEN_MENU`; +export const CLOSE_MENU = `${prefix}_CLOSE_MENU`; diff --git a/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js b/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js index c75711a56..4c4a457a2 100644 --- a/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js +++ b/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js @@ -1,28 +1,27 @@ import React from 'react'; +import {compose} from 'react-apollo'; +import {bindActionCreators} from 'redux'; import {getErrorMessages} from 'plugin-api/beta/client/utils'; -import {withSetCommentStatus} from 'plugin-api/beta/client/hocs'; import {notify} from 'plugin-api/beta/client/actions/notification'; import ApproveCommentAction from '../components/ApproveCommentAction'; -import isNil from 'lodash/isNil'; +import {connect, withSetCommentStatus} from 'plugin-api/beta/client/hocs'; class ApproveCommentActionContainer extends React.Component { approveComment = async () => { - const {setCommentStatus, comment} = this.props; + const {setCommentStatus, comment, hideMenu, notify} = this.props; try { - const result = await setCommentStatus({ + await setCommentStatus({ commentId: comment.id, status: 'ACCEPTED' }); - - if (!isNil(result.data.setCommentStatus)) { - throw result.data.setCommentStatus.errors; - } - - } catch (err) { + } + catch(err) { notify('error', getErrorMessages(err)); } + + hideMenu(); } render() { @@ -30,4 +29,14 @@ class ApproveCommentActionContainer extends React.Component { } } -export default withSetCommentStatus(ApproveCommentActionContainer); +const mapDispatchToProps = (dispatch) => + bindActionCreators({ + notify + }, dispatch); + +const enhance = compose( + connect(null, mapDispatchToProps), + withSetCommentStatus +); + +export default enhance(ApproveCommentActionContainer); diff --git a/plugins/talk-plugin-moderation-actions/client/containers/ModerationActions.js b/plugins/talk-plugin-moderation-actions/client/containers/ModerationActions.js index ffb711e18..984c7f57a 100644 --- a/plugins/talk-plugin-moderation-actions/client/containers/ModerationActions.js +++ b/plugins/talk-plugin-moderation-actions/client/containers/ModerationActions.js @@ -1,14 +1,72 @@ +import React from 'react'; +import {bindActionCreators} from 'redux'; import {gql, compose} from 'react-apollo'; +import {openMenu, closeMenu} from '../actions'; import {can} from 'plugin-api/beta/client/services'; +import {getShallowChanges} from 'plugin-api/beta/client/utils'; import ModerationActions from '../components/ModerationActions'; import {connect, excludeIf, withFragments} from 'plugin-api/beta/client/hocs'; -const mapStateToProps = ({auth}) => ({ - user: auth.user +class ModerationActionsContainer extends React.Component { + + shouldComponentUpdate(nextProps) { + + // Specifically handle `showMenuForComment` if it is the only change. + const changes = getShallowChanges(this.props, nextProps); + if (changes.length === 1 && changes[0] === 'showMenuForComment') { + const commentId = this.props.comment.id; + if ( + commentId !== this.props.showMenuForComment && + commentId !== nextProps.showMenuForComment + ) { + return false; + } + } + + // Prevent Slot from rerendering when no props has shallowly changed. + return changes.length !== 0; + } + + toogleMenu = () => { + if (this.props.showMenuForComment === this.props.comment.id) { + this.props.closeMenu(); + } else { + this.props.openMenu(this.props.comment.id); + } + } + + hideMenu = () => { + if (this.props.showMenuForComment === this.props.comment.id) { + this.props.closeMenu(); + } + } + + render() { + return ; + } +} + +const mapStateToProps = ({auth, talkPluginModerationActions: state}) => ({ + user: auth.user, + showMenuForComment: state.showMenuForComment, }); +const mapDispatchToProps = (dispatch) => + bindActionCreators({ + openMenu, + closeMenu, + }, dispatch); + const enhance = compose( - connect(mapStateToProps), + connect(mapStateToProps, mapDispatchToProps), withFragments({ asset: gql` fragment TalkModerationActions_asset on Asset { @@ -29,4 +87,4 @@ const enhance = compose( excludeIf((props) => !can(props.user, 'MODERATE_COMMENTS')), ); -export default enhance(ModerationActions); +export default enhance(ModerationActionsContainer); diff --git a/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js b/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js index 5497d0e8e..e744eb1d7 100644 --- a/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js +++ b/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js @@ -1,28 +1,27 @@ import React from 'react'; +import {compose} from 'react-apollo'; +import {bindActionCreators} from 'redux'; import {getErrorMessages} from 'plugin-api/beta/client/utils'; -import {withSetCommentStatus} from 'plugin-api/beta/client/hocs'; import {notify} from 'plugin-api/beta/client/actions/notification'; import RejectCommentAction from '../components/RejectCommentAction'; -import isNil from 'lodash/isNil'; +import {connect, withSetCommentStatus} from 'plugin-api/beta/client/hocs'; class RejectCommentActionContainer extends React.Component { rejectComment = async () => { - const {setCommentStatus, comment} = this.props; + const {setCommentStatus, comment, hideMenu, notify} = this.props; try { - const result = await setCommentStatus({ + await setCommentStatus({ commentId: comment.id, status: 'REJECTED' }); - - if (!isNil(result.data.setCommentStatus)) { - throw result.data.setCommentStatus.errors; - } - - } catch (err) { + } + catch(err) { notify('error', getErrorMessages(err)); } + + hideMenu(); } render() { @@ -30,4 +29,14 @@ class RejectCommentActionContainer extends React.Component { } } -export default withSetCommentStatus(RejectCommentActionContainer); +const mapDispatchToProps = (dispatch) => + bindActionCreators({ + notify + }, dispatch); + +const enhance = compose( + connect(null, mapDispatchToProps), + withSetCommentStatus +); + +export default enhance(RejectCommentActionContainer); diff --git a/plugins/talk-plugin-moderation-actions/client/index.js b/plugins/talk-plugin-moderation-actions/client/index.js index 3343505f3..fefcac710 100644 --- a/plugins/talk-plugin-moderation-actions/client/index.js +++ b/plugins/talk-plugin-moderation-actions/client/index.js @@ -1,9 +1,11 @@ import ModerationActions from './containers/ModerationActions'; import translations from './translations.yml'; +import reducer from './reducer'; export default { slots: { commentInfoBar: [ModerationActions], }, + reducer, translations }; diff --git a/plugins/talk-plugin-moderation-actions/client/reducer.js b/plugins/talk-plugin-moderation-actions/client/reducer.js new file mode 100644 index 000000000..d8531708b --- /dev/null +++ b/plugins/talk-plugin-moderation-actions/client/reducer.js @@ -0,0 +1,22 @@ +import {OPEN_MENU, CLOSE_MENU} from './constants'; + +const initialState = { + showMenuForComment: null, +}; + +export default function reducer(state = initialState, action) { + switch (action.type) { + case OPEN_MENU: + return { + ...state, + showMenuForComment: action.id + }; + case CLOSE_MENU: + return { + ...state, + showMenuForComment: null + }; + default : + return state; + } +}