mirror of
https://github.com/wassname/talk.git
synced 2026-09-10 12:43:11 +08:00
Implement configure and redux state
This commit is contained in:
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user