From 8f9724ed90b93efc1f924986e434642a0c068846 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 20 Nov 2017 12:29:23 +0100 Subject: [PATCH] Implement configure and redux state --- .../routes/Configure/containers/Configure.js | 32 +------- .../src/actions/configure.js | 9 +++ .../src/constants/configure.js | 4 + .../src/reducers/configure.js | 42 ++++++++++ .../coral-embed-stream/src/reducers/index.js | 2 + .../tabs/configure/components/Checkbox.css | 59 ++++++++++++++ .../src/tabs/configure/components/Checkbox.js | 28 +++++++ .../configure/components/Configuration.css | 27 +++++++ .../configure/components/Configuration.js | 46 +++++++++++ .../tabs/configure/components/Settings.css | 26 ++++++ .../src/tabs/configure/components/Settings.js | 46 +++++++++++ .../src/tabs/configure/containers/Settings.js | 79 +++++++++++++++++++ client/coral-framework/hocs/index.js | 1 + .../hocs/withMergedSettings.js | 16 ++++ client/coral-framework/utils/index.js | 10 +++ 15 files changed, 399 insertions(+), 28 deletions(-) create mode 100644 client/coral-embed-stream/src/constants/configure.js create mode 100644 client/coral-embed-stream/src/reducers/configure.js create mode 100644 client/coral-embed-stream/src/tabs/configure/components/Checkbox.css create mode 100644 client/coral-embed-stream/src/tabs/configure/components/Checkbox.js create mode 100644 client/coral-embed-stream/src/tabs/configure/components/Configuration.css create mode 100644 client/coral-embed-stream/src/tabs/configure/components/Configuration.js create mode 100644 client/coral-embed-stream/src/tabs/configure/components/Settings.css create mode 100644 client/coral-embed-stream/src/tabs/configure/components/Settings.js create mode 100644 client/coral-embed-stream/src/tabs/configure/containers/Settings.js create mode 100644 client/coral-framework/hocs/withMergedSettings.js diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js index 8f80a3e24..dfa8df876 100644 --- a/client/coral-admin/src/routes/Configure/containers/Configure.js +++ b/client/coral-admin/src/routes/Configure/containers/Configure.js @@ -2,38 +2,20 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import {compose, gql} from 'react-apollo'; -import withQuery from 'coral-framework/hocs/withQuery'; +import {withQuery, withMergedSettings} from 'coral-framework/hocs'; import {Spinner} from 'coral-ui'; import {notify} from 'coral-framework/actions/notification'; import PropTypes from 'prop-types'; -import assignWith from 'lodash/assignWith'; import {withUpdateSettings} from 'coral-framework/graphql/mutations'; import {getErrorMessages, getDefinitionName} from 'coral-framework/utils'; import StreamSettings from './StreamSettings'; import TechSettings from './TechSettings'; import ModerationSettings from './ModerationSettings'; import {clearPending, setActiveSection} from '../../../actions/configure'; - import Configure from '../components/Configure'; -// Like lodash merge but does not recurse into arrays. -const mergeExcludingArrays = (objValue, srcValue) => { - if (typeof srcValue === 'object' && !Array.isArray(srcValue)) { - return assignWith({}, objValue, srcValue, mergeExcludingArrays); - } - return srcValue; -}; - class ConfigureContainer extends Component { - // Merge current settings with pending settings. - getMergedSettings = (props = this.props) => { - return assignWith({}, props.root.settings, props.pending, mergeExcludingArrays); - } - - // Cached merged settings. - mergedSettings = this.getMergedSettings(); - savePending = async () => { try { await this.props.updateSettings(this.props.pending); @@ -44,14 +26,6 @@ class ConfigureContainer extends Component { } }; - componentWillReceiveProps(nextProps) { - - // Recalculate merged settings when necessary. - if (this.props.root.settings !== nextProps.root.settings || this.props.pending !== nextProps.pending) { - this.mergedSettings = this.getMergedSettings(nextProps); - } - } - render () { if(this.props.data.loading) { return ; @@ -62,7 +36,7 @@ class ConfigureContainer extends Component { auth={this.props.auth} data={this.props.data} root={this.props.root} - settings={this.mergedSettings} + settings={this.props.mergedSettings} canSave={this.props.canSave} savePending={this.savePending} setActiveSection={this.props.setActiveSection} @@ -112,6 +86,7 @@ export default compose( withUpdateSettings, withConfigureQuery, connect(mapStateToProps, mapDispatchToProps), + withMergedSettings('root.settings', 'pending', 'mergedSettings'), )(ConfigureContainer); ConfigureContainer.propTypes = { @@ -124,5 +99,6 @@ ConfigureContainer.propTypes = { root: PropTypes.object.isRequired, canSave: PropTypes.bool.isRequired, pending: PropTypes.object.isRequired, + mergedSettings: PropTypes.object.isRequired, activeSection: PropTypes.string.isRequired, }; diff --git a/client/coral-embed-stream/src/actions/configure.js b/client/coral-embed-stream/src/actions/configure.js index e69de29bb..bfc738fe0 100644 --- a/client/coral-embed-stream/src/actions/configure.js +++ b/client/coral-embed-stream/src/actions/configure.js @@ -0,0 +1,9 @@ +import * as actions from '../constants/configure'; + +export const updatePending = ({updater, errorUpdater}) => { + return {type: actions.UPDATE_PENDING, updater, errorUpdater}; +}; + +export const clearPending = () => { + return {type: actions.CLEAR_PENDING}; +}; diff --git a/client/coral-embed-stream/src/constants/configure.js b/client/coral-embed-stream/src/constants/configure.js new file mode 100644 index 000000000..b29cf6280 --- /dev/null +++ b/client/coral-embed-stream/src/constants/configure.js @@ -0,0 +1,4 @@ +const prefix = 'TALK_EMBED_STREAM_CONFIGURE'; + +export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`; +export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`; diff --git a/client/coral-embed-stream/src/reducers/configure.js b/client/coral-embed-stream/src/reducers/configure.js new file mode 100644 index 000000000..0a94d5a54 --- /dev/null +++ b/client/coral-embed-stream/src/reducers/configure.js @@ -0,0 +1,42 @@ +import * as actions from '../constants/configure'; +import isEmpty from 'lodash/isEmpty'; +import update from 'immutability-helper'; + +const initialState = { + canSave: false, + pending: {}, + errors: {}, +}; + +export default function config(state = initialState, action) { + switch (action.type) { + case actions.UPDATE_PENDING: { + let next = state; + if (action.updater) { + next = update(next, { + pending: action.updater, + }); + } + if (action.errorUpdater) { + next = update(next, { + errors: action.errorUpdater, + }); + } + const noErrors = Object.keys(next.errors).reduce((res, error) => res && !next.errors[error], true); + const canSave = !isEmpty(next.pending) && noErrors; + next = update(next, { + canSave: {$set: canSave}, + }); + + return next; + } + case actions.CLEAR_PENDING: + return { + ...state, + pending: {}, + canSave: false, + }; + default: + return state; + } +} diff --git a/client/coral-embed-stream/src/reducers/index.js b/client/coral-embed-stream/src/reducers/index.js index 5c553b04f..d6d0c7693 100644 --- a/client/coral-embed-stream/src/reducers/index.js +++ b/client/coral-embed-stream/src/reducers/index.js @@ -2,6 +2,7 @@ import auth from './auth'; import asset from './asset'; import embed from './embed'; import config from './config'; +import configure from './configure'; import stream from './stream'; import {reducer as commentBox} from '../../../talk-plugin-commentbox'; @@ -11,5 +12,6 @@ export default { commentBox, embed, config, + configure, stream, }; diff --git a/client/coral-embed-stream/src/tabs/configure/components/Checkbox.css b/client/coral-embed-stream/src/tabs/configure/components/Checkbox.css new file mode 100644 index 000000000..a25ea150f --- /dev/null +++ b/client/coral-embed-stream/src/tabs/configure/components/Checkbox.css @@ -0,0 +1,59 @@ +.root { + position: relative; + display: inline-block; +} + +.input { + position: absolute; + left: 7px; + bottom: 7px; + margin: 0; + padding: 0; + outline: none; + cursor: pointer; + pointer-events: none; + opacity: 0; +} + +.checkbox { + cursor: pointer; +} + +.checkbox:before { + content: "\e835"; + color: #717171; + left: 4px; + top: 0px; + width: 18px; + height: 18px; + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + vertical-align: -6px; + text-rendering: optimizeLegibility; + font-feature-settings: 'liga'; + transition: all .2s ease; + z-index: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.checkboxChecked:before { + content: "\e834"; + color: #00a291; +} + +.input:focus + .checkbox:before { + color: #00a291; +} + +.input:focus + .checkboxChecked:before { + color: #00e291; +} diff --git a/client/coral-embed-stream/src/tabs/configure/components/Checkbox.js b/client/coral-embed-stream/src/tabs/configure/components/Checkbox.js new file mode 100644 index 000000000..98804fe60 --- /dev/null +++ b/client/coral-embed-stream/src/tabs/configure/components/Checkbox.js @@ -0,0 +1,28 @@ +import React from 'react'; +import styles from './Checkbox.css'; +import cn from 'classnames'; +import PropTypes from 'prop-types'; + +const Checkbox = ({onChange, checked, className, ...rest}) => ( + +); + +Checkbox.propTypes = { + className: PropTypes.string, + onChange: PropTypes.func, + checked: PropTypes.bool, +}; + +export default Checkbox; diff --git a/client/coral-embed-stream/src/tabs/configure/components/Configuration.css b/client/coral-embed-stream/src/tabs/configure/components/Configuration.css new file mode 100644 index 000000000..b18328a94 --- /dev/null +++ b/client/coral-embed-stream/src/tabs/configure/components/Configuration.css @@ -0,0 +1,27 @@ +.root { + position: relative; + margin: 12px 12px 12px 0; +} + +.action { + display: inline-block; + position: absolute; + top: 0; + left: 0; + padding-left: 4px; +} + +.title { + font-size: 14px; + margin-bottom: 5px; + font-weight: bold; + cursor: pointer; +} + +.content { + display: inline-block; + padding: 0px 50px; + box-sizing: border-box; +} + + diff --git a/client/coral-embed-stream/src/tabs/configure/components/Configuration.js b/client/coral-embed-stream/src/tabs/configure/components/Configuration.js new file mode 100644 index 000000000..025517be6 --- /dev/null +++ b/client/coral-embed-stream/src/tabs/configure/components/Configuration.js @@ -0,0 +1,46 @@ +import React from 'react'; +import Checkbox from './Checkbox'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; +import styles from './Configuration.css'; +import uuid from 'uuid/v4'; + +class Configuration extends React.Component { + + id = uuid(); + + render() { + const {title, children, className, onCheckbox, checked, ...rest} = this.props; + return ( +
+ {checked !== undefined && +
+ +
+ } +
+ +
+ {children} +
+
+
+ ); + } +} + +Configuration.propTypes = { + title: PropTypes.string.isRequired, + className: PropTypes.string, + onCheckbox: PropTypes.func, + checked: PropTypes.bool, + children: PropTypes.node, +}; + +export default Configuration; diff --git a/client/coral-embed-stream/src/tabs/configure/components/Settings.css b/client/coral-embed-stream/src/tabs/configure/components/Settings.css new file mode 100644 index 000000000..914b7332b --- /dev/null +++ b/client/coral-embed-stream/src/tabs/configure/components/Settings.css @@ -0,0 +1,26 @@ +.container { + position: relative; +} + +.apply { + float: right; + margin: 0 10px; +} + +.description { + max-width: 380px; +} + +.checkbox { + vertical-align: top; + margin: 12px 12px 12px 0; +} + +.list { + margin-top: 26px; +} + +.wrapper { + margin-bottom: 20px; +} + diff --git a/client/coral-embed-stream/src/tabs/configure/components/Settings.js b/client/coral-embed-stream/src/tabs/configure/components/Settings.js new file mode 100644 index 000000000..2235369fe --- /dev/null +++ b/client/coral-embed-stream/src/tabs/configure/components/Settings.js @@ -0,0 +1,46 @@ +import React from 'react'; +import {Button} from 'coral-ui'; +import PropTypes from 'prop-types'; +import t from 'coral-framework/services/i18n'; +import cn from 'classnames'; +import styles from './Settings.css'; +import Configuration from './Configuration'; + +class Settings extends React.Component { + render() { + const {settings: {moderation}, toggleModeration} = this.props; + const changed = false; + return ( +
+
+

{t('configure.title')}

+ +

{t('configure.description')}

+
+
+ + {t('configure.enable_premod_description')} + +
+
+ ); + } +} + +Settings.propTypes = { + settings: PropTypes.object.isRequired, + toggleModeration: PropTypes.func.isRequired, +}; + +export default Settings; diff --git a/client/coral-embed-stream/src/tabs/configure/containers/Settings.js b/client/coral-embed-stream/src/tabs/configure/containers/Settings.js new file mode 100644 index 000000000..66625715d --- /dev/null +++ b/client/coral-embed-stream/src/tabs/configure/containers/Settings.js @@ -0,0 +1,79 @@ +import React from 'react'; +import {gql, compose} from 'react-apollo'; +import {withFragments, withMergedSettings} from 'coral-framework/hocs'; +import {getErrorMessages} from 'coral-framework/utils'; +import Settings from '../components/Settings.js'; +import PropTypes from 'prop-types'; +import {withUpdateAssetSettings} from 'coral-framework/graphql/mutations'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {notify} from 'coral-framework/actions/notification'; +import {clearPending, updatePending} from '../../../actions/configure'; + +class SettingsContainer extends React.Component { + + toggleModeration = () => { + const updater = {moderation: {$set: this.props.mergedSettings.moderation === 'PRE' ? 'POST' : 'PRE'}}; + this.props.updatePending({updater}); + }; + + savePending = async () => { + try { + await this.props.updateAssetSettings(this.props.asset.id, this.props.pending); + this.props.clearPending(); + } + catch(err) { + this.props.notify('error', getErrorMessages(err)); + } + }; + + render() { + return ; + } +} + +SettingsContainer.propTypes = { + asset: PropTypes.object.isRequired, + pending: PropTypes.object.isRequired, + mergedSettings: PropTypes.object.isRequired, + updateAssetSettings: PropTypes.func.isRequired, + clearPending: PropTypes.func.isRequired, + notify: PropTypes.func.isRequired, + updatePending: PropTypes.func.isRequired, +}; + +const withSettingsFragments = withFragments({ + asset: gql` + fragment CoralEmbedStream_Settings_asset on Asset { + id + settings { + moderation + } + } + `, +}); + +const mapStateToProps = (state) => ({ + pending: state.configure.pending, + canSave: state.configure.canSave, +}); + +const mapDispatchToProps = (dispatch) => + bindActionCreators({ + notify, + clearPending, + updatePending, + }, dispatch); + +const enhance = compose( + withSettingsFragments, + withUpdateAssetSettings, + connect(mapStateToProps, mapDispatchToProps), + withMergedSettings('asset.settings', 'pending', 'mergedSettings'), +); + +export default enhance(SettingsContainer); diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js index 67b47482e..b3c0219e3 100644 --- a/client/coral-framework/hocs/index.js +++ b/client/coral-framework/hocs/index.js @@ -5,3 +5,4 @@ export {default as withCopyToClipboard} from './withCopyToClipboard'; export {default as withEmit} from './withEmit'; export {default as excludeIf} from './excludeIf'; export {default as connect} from './connect'; +export {default as withMergedSettings} from './withMergedSettings'; diff --git a/client/coral-framework/hocs/withMergedSettings.js b/client/coral-framework/hocs/withMergedSettings.js new file mode 100644 index 000000000..8368475e6 --- /dev/null +++ b/client/coral-framework/hocs/withMergedSettings.js @@ -0,0 +1,16 @@ +import {mergeExcludingArrays} from 'coral-framework/utils'; +import assignWith from 'lodash/assignWith'; +import get from 'lodash/get'; +import {withPropsOnChange} from 'recompose'; + +const withMergedSettings = (settings, pending, result) => + withPropsOnChange( + (props, nextProps) => + get(props, settings) !== get(nextProps, settings) || + get(props, pending) !== get(nextProps, pending), + (props) => ({ + [result]: assignWith({}, get(props, settings), get(props, pending), mergeExcludingArrays) + }) + ); + +export default withMergedSettings; diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 037c1b99f..9f71b0913 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -2,6 +2,7 @@ import {gql} from 'react-apollo'; import t from 'coral-framework/services/i18n'; import union from 'lodash/union'; import {capitalize} from 'coral-framework/helpers/strings'; +import assignWith from 'lodash/assignWith'; export * from 'coral-framework/helpers/strings'; export const getTotalActionCount = (type, comment) => { @@ -197,3 +198,12 @@ export function getTotalReactionsCount(actionSummaries) { .filter(({__typename}) => !NOT_REACTION_TYPES.includes(__typename)) .reduce((total, {count}) => total + count, 0); } + +// Like lodash merge but does not recurse into arrays. +export function mergeExcludingArrays(objValue, srcValue) { + if (typeof srcValue === 'object' && !Array.isArray(srcValue)) { + return assignWith({}, objValue, srcValue, mergeExcludingArrays); + } + return srcValue; +} +