Implement configure and redux state

This commit is contained in:
Chi Vinh Le
2017-11-20 12:29:23 +01:00
parent 9fb7aa2657
commit 8f9724ed90
15 changed files with 399 additions and 28 deletions
@@ -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 <Spinner/>;
@@ -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,
};
@@ -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};
};
@@ -0,0 +1,4 @@
const prefix = 'TALK_EMBED_STREAM_CONFIGURE';
export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`;
export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`;
@@ -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;
}
}
@@ -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,
};
@@ -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;
}
@@ -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}) => (
<label className={cn(styles.root, className)}>
<input
type="checkbox"
className={cn(styles.input, {[styles.inputChecked]: checked})}
onChange={onChange}
checked={checked}
{...rest}
/>
<span
className={cn(styles.checkbox, {[styles.checkboxChecked]: checked})}
aria-hidden='true'
></span>
</label>
);
Checkbox.propTypes = {
className: PropTypes.string,
onChange: PropTypes.func,
checked: PropTypes.bool,
};
export default Checkbox;
@@ -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;
}
@@ -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 (
<div {...rest} className={cn(styles.root, className)}>
{checked !== undefined &&
<div className={styles.action}>
<Checkbox
id={this.id}
className={styles.checkbox}
onChange={onCheckbox}
checked={checked} />
</div>
}
<div className={cn(styles.wrapper, {
[styles.content]: checked !== undefined,
})}>
<label htmlFor={this.id} className={styles.title}>{title}</label>
<div>
{children}
</div>
</div>
</div>
);
}
}
Configuration.propTypes = {
title: PropTypes.string.isRequired,
className: PropTypes.string,
onCheckbox: PropTypes.func,
checked: PropTypes.bool,
children: PropTypes.node,
};
export default Configuration;
@@ -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;
}
@@ -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 (
<div className={styles.wrapper}>
<div className={styles.container}>
<h3>{t('configure.title')}</h3>
<Button
type="submit"
className={cn(styles.apply, 'talk-embed-stream-configuration-submit-button')}
checked={changed}
cStyle={changed ? 'green' : 'darkGrey'}
>
{t('configure.apply')}
</Button>
<p className={styles.description}>{t('configure.description')}</p>
</div>
<div className={styles.list}>
<Configuration
checked={moderation === 'PRE'}
title={t('configure.enable_premod')}
onCheckbox={toggleModeration}
>
{t('configure.enable_premod_description')}
</Configuration>
</div>
</div>
);
}
}
Settings.propTypes = {
settings: PropTypes.object.isRequired,
toggleModeration: PropTypes.func.isRequired,
};
export default Settings;
@@ -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 <Settings
settings={this.props.mergedSettings}
savePending={this.savePending}
toggleModeration={this.toggleModeration}
/>;
}
}
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);
+1
View File
@@ -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';
@@ -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;
+10
View File
@@ -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;
}