mirror of
https://github.com/wassname/talk.git
synced 2026-07-10 07:33:13 +08:00
Merge branch 'master' into firefox-hover-tab
This commit is contained in:
@@ -1,15 +1,5 @@
|
||||
import update from 'immutability-helper';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
|
||||
// Map nested object leaves. Array objects are considered leaves.
|
||||
function mapLeaves(o, mapper) {
|
||||
return mapValues(o, (val) => {
|
||||
if (typeof val === 'object' && !Array.isArray(val)) {
|
||||
return mapLeaves(val, mapper);
|
||||
}
|
||||
return mapper(val);
|
||||
});
|
||||
}
|
||||
import {mapLeaves} from 'coral-framework/utils';
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
|
||||
@@ -39,7 +39,7 @@ class ModerationSettings extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {settings, data, root} = this.props;
|
||||
const {settings, data, root, updatePending, errors} = this.props;
|
||||
|
||||
return (
|
||||
<ConfigurePage
|
||||
@@ -74,6 +74,8 @@ class ModerationSettings extends React.Component {
|
||||
fill="adminModerationSettings"
|
||||
data={data}
|
||||
queryData={{root, settings}}
|
||||
updatePending={updatePending}
|
||||
errors={errors}
|
||||
/>
|
||||
</ConfigurePage>
|
||||
);
|
||||
@@ -82,6 +84,7 @@ class ModerationSettings extends React.Component {
|
||||
|
||||
ModerationSettings.propTypes = {
|
||||
updatePending: PropTypes.func.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
|
||||
@@ -100,7 +100,7 @@ class StreamSettings extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {settings, data, root, errors} = this.props;
|
||||
const {settings, data, root, errors, updatePending} = this.props;
|
||||
|
||||
return (
|
||||
<ConfigurePage
|
||||
@@ -205,6 +205,8 @@ class StreamSettings extends React.Component {
|
||||
fill="adminStreamSettings"
|
||||
data={data}
|
||||
queryData={{root, settings}}
|
||||
updatePending={updatePending}
|
||||
errors={errors}
|
||||
/>
|
||||
</ConfigurePage>
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ class TechSettings extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {settings, data, root} = this.props;
|
||||
const {settings, data, root, errors, updatePending} = this.props;
|
||||
return (
|
||||
<ConfigurePage
|
||||
title={t('configure.tech_settings')}
|
||||
@@ -51,6 +51,8 @@ class TechSettings extends React.Component {
|
||||
fill="adminTechSettings"
|
||||
data={data}
|
||||
queryData={{root, settings}}
|
||||
updatePending={updatePending}
|
||||
errors={errors}
|
||||
/>
|
||||
</ConfigurePage>
|
||||
);
|
||||
@@ -59,6 +61,7 @@ class TechSettings extends React.Component {
|
||||
|
||||
TechSettings.propTypes = {
|
||||
updatePending: PropTypes.func.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,10 @@ const slots = [
|
||||
'adminModerationSettings',
|
||||
];
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
errors: state.configure.errors,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
updatePending,
|
||||
@@ -36,5 +40,5 @@ export default compose(
|
||||
}
|
||||
`
|
||||
}),
|
||||
connect(null, mapDispatchToProps),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
)(ModerationSettings);
|
||||
|
||||
@@ -10,6 +10,10 @@ const slots = [
|
||||
'adminTechSettings',
|
||||
];
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
errors: state.configure.errors,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
updatePending,
|
||||
@@ -33,5 +37,5 @@ export default compose(
|
||||
}
|
||||
`
|
||||
}),
|
||||
connect(null, mapDispatchToProps),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
)(TechSettings);
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Button} from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const CloseCommentsInfo = ({status, onClick}) => (
|
||||
status === 'open' ? (
|
||||
<div className="close-comments-intro-wrapper">
|
||||
<p>
|
||||
{t('configure.open_stream_configuration')}
|
||||
</p>
|
||||
<Button onClick={onClick}>{t('configure.close_stream')}</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="close-comments-intro-wrapper">
|
||||
<p>
|
||||
{t('configure.close_stream_configuration')}
|
||||
</p>
|
||||
<Button onClick={onClick}>{t('configure.open_stream')}</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
CloseCommentsInfo.propTypes = {
|
||||
status: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default CloseCommentsInfo;
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Button, Checkbox} from 'coral-ui';
|
||||
import QuestionBoxBuilder from './QuestionBoxBuilder';
|
||||
import cn from 'classnames';
|
||||
|
||||
import styles from './ConfigureCommentStream.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export default ({handleChange, handleApply, changed, ...props}) => (
|
||||
<form onSubmit={handleApply}>
|
||||
<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')}
|
||||
onChange={handleChange}
|
||||
cStyle={changed ? 'green' : 'darkGrey'} >
|
||||
{t('configure.apply')}
|
||||
</Button>
|
||||
<p>{t('configure.description')}</p>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
cStyle={changed ? 'green' : 'darkGrey'}
|
||||
name="premod"
|
||||
onChange={handleChange}
|
||||
defaultChecked={props.premod}
|
||||
info={{
|
||||
title: t('configure.enable_premod'),
|
||||
description: t('configure.enable_premod_description')
|
||||
}} />
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
cStyle={changed ? 'green' : 'darkGrey'}
|
||||
name="plinksenable"
|
||||
onChange={handleChange}
|
||||
defaultChecked={props.premodLinksEnable}
|
||||
info={{
|
||||
title: t('configure.enable_premod_links'),
|
||||
description: t('configure.enable_premod_links_description')
|
||||
}} />
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
cStyle={changed ? 'green' : 'darkGrey'}
|
||||
name="qboxenable"
|
||||
onChange={handleChange}
|
||||
defaultChecked={props.questionBoxEnable}
|
||||
info={{
|
||||
title: t('configure.enable_questionbox'),
|
||||
description: t('configure.enable_questionbox_description')
|
||||
}} />
|
||||
{
|
||||
props.questionBoxEnable && <QuestionBoxBuilder
|
||||
questionBoxIcon={props.questionBoxIcon}
|
||||
questionBoxContent={props.questionBoxContent}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
@@ -1,24 +0,0 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import marked from 'marked';
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
// Set link target to `_parent` to work properly in an embed.
|
||||
renderer.link = (href, title, text) =>
|
||||
`<a target="_parent" href="${href}" ${title ? `title="${title}"` : ''}>${text}</a>`;
|
||||
|
||||
marked.setOptions({renderer});
|
||||
|
||||
export default class Markdown extends PureComponent {
|
||||
render() {
|
||||
const {content, ...rest} = this.props;
|
||||
const __html = marked(content);
|
||||
return <div {...rest} dangerouslySetInnerHTML={{__html}} />;
|
||||
}
|
||||
}
|
||||
|
||||
Markdown.propTypes = {
|
||||
content: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
.qbBuilder {
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
.qbItemIconList {
|
||||
padding: 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.qbItemIcon {
|
||||
background: #F0F0F0;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
line-height: 45px;
|
||||
color: #252525;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
border: solid 2px #F0F0F0;
|
||||
transition: border 0.3s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
.qbItemIcon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.qbItemIconActive {
|
||||
border: solid 2px #00796B;
|
||||
}
|
||||
|
||||
.defaultIcon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.qb {
|
||||
margin: 10px 0;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import React from 'react';
|
||||
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
|
||||
import {Icon, Spinner} from 'coral-ui';
|
||||
import DefaultIcon from './DefaultIcon';
|
||||
import cn from 'classnames';
|
||||
import styles from './QuestionBoxBuilder.css';
|
||||
|
||||
class QuestionBoxBuilder extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.loadEditor();
|
||||
}
|
||||
|
||||
async loadEditor() {
|
||||
const {default: MarkdownEditor} = await import('coral-framework/components/MarkdownEditor');
|
||||
|
||||
return this.setState({
|
||||
loading : false,
|
||||
MarkdownEditor
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {handleChange, questionBoxIcon, questionBoxContent} = this.props;
|
||||
const {loading, MarkdownEditor} = this.state;
|
||||
|
||||
if (loading) {
|
||||
return <Spinner/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.qbBuilder}>
|
||||
<h4>Include an Icon</h4>
|
||||
|
||||
<ul className={styles.qbItemIconList}>
|
||||
<li className={cn(
|
||||
styles.qbItemIcon,
|
||||
{[styles.qbItemIconActive]: questionBoxIcon === 'default'}
|
||||
)}
|
||||
id="qboxicon"
|
||||
onClick={handleChange}
|
||||
data-icon="default" >
|
||||
<DefaultIcon className={styles.defaultIcon} />
|
||||
</li>
|
||||
<li className={cn(
|
||||
styles.qbItemIcon,
|
||||
{[styles.qbItemIconActive]: questionBoxIcon === 'forum'}
|
||||
)}
|
||||
id="qboxicon"
|
||||
onClick={handleChange}
|
||||
data-icon="forum" >
|
||||
<Icon name="forum" />
|
||||
</li>
|
||||
<li className={cn(
|
||||
styles.qbItemIcon,
|
||||
{[styles.qbItemIconActive]: questionBoxIcon === 'build'}
|
||||
)}
|
||||
id="qboxicon"
|
||||
onClick={handleChange}
|
||||
data-icon="build" >
|
||||
<Icon name="build" />
|
||||
</li>
|
||||
<li className={cn(
|
||||
styles.qbItemIcon,
|
||||
{[styles.qbItemIconActive]: questionBoxIcon === 'format_quote'}
|
||||
)}
|
||||
id="qboxicon"
|
||||
onClick={handleChange}
|
||||
data-icon="format_quote" >
|
||||
<Icon name="format_quote" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<QuestionBox
|
||||
className={styles.qb}
|
||||
enable={true}
|
||||
icon={questionBoxIcon}
|
||||
content={questionBoxContent}
|
||||
/>
|
||||
|
||||
<MarkdownEditor
|
||||
value={questionBoxContent}
|
||||
onChange={(value) => handleChange({}, {questionBoxContent: value})}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default QuestionBoxBuilder;
|
||||
@@ -1,145 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {compose} from 'react-apollo';
|
||||
import PropTypes from 'prop-types';
|
||||
import {updateOpenStatus, updateConfiguration} from 'coral-embed-stream/src/actions/asset';
|
||||
import CloseCommentsInfo from '../components/CloseCommentsInfo';
|
||||
import ConfigureCommentStream from '../components/ConfigureCommentStream';
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
|
||||
class ConfigureStreamContainer extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
changed: false,
|
||||
dirtySettings: {...props.asset.settings},
|
||||
closedAt: !props.asset.isClosed ? 'open' : 'closed'
|
||||
};
|
||||
|
||||
this.toggleStatus = this.toggleStatus.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleApply = this.handleApply.bind(this);
|
||||
}
|
||||
|
||||
handleApply (e) {
|
||||
e.preventDefault();
|
||||
const {elements} = e.target;
|
||||
const {questionBoxIcon, questionBoxContent} = this.state.dirtySettings;
|
||||
const premod = elements.premod.checked;
|
||||
const questionBoxEnable = elements.qboxenable.checked;
|
||||
const premodLinksEnable = elements.plinksenable.checked;
|
||||
const {changed} = this.state;
|
||||
|
||||
const newConfig = {
|
||||
moderation: premod ? 'PRE' : 'POST',
|
||||
questionBoxEnable,
|
||||
questionBoxContent,
|
||||
questionBoxIcon,
|
||||
premodLinksEnable
|
||||
};
|
||||
|
||||
if (changed) {
|
||||
this.props.updateConfiguration(newConfig);
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
changed: false
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange (e, newChanges) {
|
||||
let changes = {};
|
||||
|
||||
if (changes) {
|
||||
changes = {...newChanges};
|
||||
}
|
||||
|
||||
if (e.target && e.target.id === 'qboxenable') {
|
||||
changes.questionBoxEnable = e.target.checked;
|
||||
}
|
||||
if (e.currentTarget && e.currentTarget.id === 'qboxicon') {
|
||||
changes.questionBoxIcon = e.currentTarget.dataset.icon;
|
||||
}
|
||||
if (e.target && e.target.id === 'plinksenable') {
|
||||
changes.premodLinksEnable = e.target.value;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
changed: true,
|
||||
dirtySettings: {
|
||||
...this.state.dirtySettings,
|
||||
...changes,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
toggleStatus () {
|
||||
|
||||
// update the closedAt status for the asset
|
||||
this.props.updateStatus(
|
||||
this.state.closedAt === 'open' ? 'closed' : 'open'
|
||||
);
|
||||
this.setState({
|
||||
closedAt: (this.state.closedAt === 'open' ? 'closed' : 'open')
|
||||
});
|
||||
}
|
||||
|
||||
getClosedIn () {
|
||||
const {closedTimeout} = this.props.asset.settings;
|
||||
const {created_at} = this.props.asset;
|
||||
|
||||
return timeago(new Date(created_at).getTime() + (1000 * closedTimeout));
|
||||
}
|
||||
|
||||
render () {
|
||||
const {dirtySettings} = this.state;
|
||||
const premod = dirtySettings.moderation === 'PRE';
|
||||
const {closedAt} = this.state;
|
||||
const closedTimeout = dirtySettings.closedTimeout;
|
||||
|
||||
return (
|
||||
<div className='talk-embed-stream-configuration-container'>
|
||||
<ConfigureCommentStream
|
||||
handleChange={this.handleChange}
|
||||
handleApply={this.handleApply}
|
||||
changed={this.state.changed}
|
||||
premodLinksEnable={dirtySettings.premodLinksEnable}
|
||||
premod={premod}
|
||||
questionBoxIcon={dirtySettings.questionBoxIcon}
|
||||
questionBoxEnable={dirtySettings.questionBoxEnable}
|
||||
questionBoxContent={dirtySettings.questionBoxContent}
|
||||
/>
|
||||
<hr />
|
||||
<h3>{closedAt === 'open' ? t('configure.close') : t('configure.open')} {t('configure.comment_stream')}</h3>
|
||||
{(closedAt === 'open' && closedTimeout) ? <p>{t('configure.comment_stream_will_close')} {this.getClosedIn()}.</p> : ''}
|
||||
<CloseCommentsInfo
|
||||
onClick={this.toggleStatus}
|
||||
status={closedAt}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
asset: state.asset
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
updateStatus: (status) => dispatch(updateOpenStatus(status)),
|
||||
updateConfiguration: (newConfig) => dispatch(updateConfiguration(newConfig)),
|
||||
});
|
||||
|
||||
ConfigureStreamContainer.propTypes = {
|
||||
updateStatus: PropTypes.func,
|
||||
closedTimeout: PropTypes.string,
|
||||
created_at: PropTypes.string,
|
||||
updateConfiguration: PropTypes.func,
|
||||
asset: PropTypes.object,
|
||||
};
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(ConfigureStreamContainer);
|
||||
@@ -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};
|
||||
};
|
||||
+2
-9
@@ -16,12 +16,5 @@
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.qbIconContainer {
|
||||
position: relative;
|
||||
border: 0;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
padding: 5px 20px;
|
||||
vertical-align: middle;
|
||||
width: 10px;
|
||||
}
|
||||
.root {
|
||||
}
|
||||
+4
-4
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './DefaultIcon.css';
|
||||
import styles from './DefaultQuestionBoxIcon.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
const DefaultIcon = ({className}) => (
|
||||
<div className={cn(styles.qbIconContainer, className)}>
|
||||
const DefaultQuestionBoxIcon = ({className}) => (
|
||||
<div className={cn(styles.root, className)}>
|
||||
<Icon name="chat_bubble" className={cn(styles.iconBubble)} />
|
||||
<Icon name="person" className={cn(styles.iconPerson)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default DefaultIcon;
|
||||
export default DefaultQuestionBoxIcon;
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Stream from '../containers/Stream';
|
||||
import Stream from '../tabs/stream/containers/Stream';
|
||||
import Configure from '../tabs/configure/containers/Configure';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import AutomaticAssetClosure from '../containers/AutomaticAssetClosure';
|
||||
|
||||
import ExtendableTabPanel from '../containers/ExtendableTabPanel';
|
||||
import {Tab, 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';
|
||||
|
||||
export default class Embed extends React.Component {
|
||||
@@ -22,9 +22,6 @@ export default class Embed extends React.Component {
|
||||
case 'profile':
|
||||
this.props.data.refetch();
|
||||
break;
|
||||
case 'config':
|
||||
this.props.data.refetch();
|
||||
break;
|
||||
}
|
||||
this.props.setActiveTab(tab);
|
||||
};
|
||||
@@ -41,7 +38,7 @@ export default class Embed extends React.Component {
|
||||
];
|
||||
if (can(user, 'UPDATE_CONFIG')) {
|
||||
tabs.push(
|
||||
<Tab key='config' tabId='config' className='talk-embed-stream-configuration-tab'>
|
||||
<Tab key='config' tabId='config'>
|
||||
{t('framework.configure_stream')}
|
||||
</Tab>
|
||||
);
|
||||
@@ -50,11 +47,12 @@ export default class Embed extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {activeTab, commentId, root, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog, router: {location: {query: {parentUrl}}}} = this.props;
|
||||
const {activeTab, commentId, root, root: {asset}, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog, router: {location: {query: {parentUrl}}}} = this.props;
|
||||
const hasHighlightedComment = !!commentId;
|
||||
|
||||
return (
|
||||
<div className={cn('talk-embed-stream', {'talk-embed-stream-highlight-comment': hasHighlightedComment})}>
|
||||
<AutomaticAssetClosure asset={asset} />
|
||||
<IfSlotIsNotEmpty slot="login">
|
||||
<Popup
|
||||
href={`embed/stream/login?parentUrl=${encodeURIComponent(parentUrl)}`}
|
||||
@@ -87,13 +85,13 @@ export default class Embed extends React.Component {
|
||||
tabs={this.getTabs()}
|
||||
tabPanes={[
|
||||
<TabPane key='stream' tabId='stream' className='talk-embed-stream-comments-tab-pane'>
|
||||
<Stream data={data} root={root} />
|
||||
<Stream data={data} root={root} asset={root.asset} />
|
||||
</TabPane>,
|
||||
<TabPane key='profile' tabId='profile' className='talk-embed-stream-profile-tab-pane'>
|
||||
<ProfileContainer />
|
||||
</TabPane>,
|
||||
<TabPane key='config' tabId='config' className='talk-embed-stream-configuration-tab-pane'>
|
||||
<ConfigureStreamContainer />
|
||||
<Configure data={data} root={root} asset={root.asset} />
|
||||
</TabPane>,
|
||||
]}
|
||||
/>
|
||||
|
||||
-5
@@ -65,8 +65,3 @@
|
||||
justify-content: center;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
+5
-8
@@ -3,16 +3,14 @@ import cn from 'classnames';
|
||||
import styles from './QuestionBox.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
import Markdown from 'coral-framework/components/Markdown';
|
||||
import DefaultQuestionBoxIcon from './DefaultQuestionBoxIcon';
|
||||
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
const QuestionBox = ({content, enable, icon = '', className = ''}) => (
|
||||
<div className={cn(styles.qbInfo, {[styles.hidden]: !enable}, 'questionbox-info', className)}>
|
||||
const QuestionBox = ({content, icon, className, children}) => (
|
||||
<div className={cn(styles.qbInfo, 'questionbox-info', className)}>
|
||||
{
|
||||
icon === 'default' ? (
|
||||
<div className={cn(styles.qbIconContainer)}>
|
||||
<Icon name="chat_bubble" className={cn(styles.iconBubble)} />
|
||||
<Icon name="person" className={cn(styles.iconPerson)} />
|
||||
<DefaultQuestionBoxIcon />
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn(styles.qbIconContainer)}>
|
||||
@@ -23,8 +21,7 @@ const QuestionBox = ({content, enable, icon = '', className = ''}) => (
|
||||
<div className={cn(styles.qbContent, 'questionbox-content')}>
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
|
||||
<Slot fill="streamQuestionArea" />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
const prefix = 'TALK_EMBED_STREAM_CONFIGURE';
|
||||
|
||||
export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`;
|
||||
export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`;
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {gql} from 'react-apollo';
|
||||
import {gql, compose} from 'react-apollo';
|
||||
import {withFragments} from 'coral-framework/hocs';
|
||||
|
||||
const FRAGMENT = gql`
|
||||
fragment CoralEmbedStream_AutomaticAssetClosure_Fragment on Asset {
|
||||
id
|
||||
isClosed
|
||||
}
|
||||
`;
|
||||
@@ -26,15 +26,15 @@ class AutomaticAssetClosure extends React.Component {
|
||||
timer = null;
|
||||
|
||||
componentWillMount() {
|
||||
this.setupTimer(this.props.assetId, this.props.closedAt);
|
||||
this.setupTimer(this.props.asset.id, this.props.asset.closedAt);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
if (
|
||||
this.props.assetId !== next.assetId ||
|
||||
this.props.closedAt !== next.closedAt
|
||||
this.props.asset.id !== next.asset.id ||
|
||||
this.props.asset.closedAt !== next.asset.closedAt
|
||||
) {
|
||||
this.setupTimer(next.assetId, next.closedAt);
|
||||
this.setupTimer(next.asset.id, next.asset.closedAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class AutomaticAssetClosure extends React.Component {
|
||||
fragment: FRAGMENT,
|
||||
id: getFragmentId(assetId),
|
||||
data: {
|
||||
__typename: 'Asset',
|
||||
isClosed: true,
|
||||
},
|
||||
});
|
||||
@@ -74,9 +75,21 @@ class AutomaticAssetClosure extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
AutomaticAssetClosure.PropTypes = {
|
||||
assetId: PropTypes.string,
|
||||
closedAt: PropTypes.string,
|
||||
AutomaticAssetClosure.propTypes = {
|
||||
asset: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default AutomaticAssetClosure;
|
||||
const withAutomaticAssetClosureFragments = withFragments({
|
||||
asset: gql`
|
||||
fragment CoralEmbedStream_AutomaticAssetClosure_asset on Asset {
|
||||
id
|
||||
closedAt
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const enhance = compose(
|
||||
withAutomaticAssetClosureFragments,
|
||||
);
|
||||
|
||||
export default enhance(AutomaticAssetClosure);
|
||||
|
||||
@@ -13,7 +13,9 @@ import * as assetActions from '../actions/asset';
|
||||
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
import {withQuery} from 'coral-framework/hocs';
|
||||
import Embed from '../components/Embed';
|
||||
import Stream from './Stream';
|
||||
import Stream from '../tabs/stream/containers/Stream';
|
||||
import AutomaticAssetClosure from './AutomaticAssetClosure';
|
||||
import Configure from '../tabs/configure/containers/Configure';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -170,10 +172,20 @@ const EMBED_QUERY = gql`
|
||||
id
|
||||
status
|
||||
}
|
||||
asset(id: $assetId, url: $assetUrl) {
|
||||
...${getDefinitionName(Configure.fragments.asset)}
|
||||
...${getDefinitionName(Stream.fragments.asset)}
|
||||
...${getDefinitionName(AutomaticAssetClosure.fragments.asset)}
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
...${getDefinitionName(Stream.fragments.root)}
|
||||
...${getDefinitionName(Configure.fragments.root)}
|
||||
}
|
||||
${Stream.fragments.root}
|
||||
${Stream.fragments.asset}
|
||||
${Configure.fragments.root}
|
||||
${Configure.fragments.asset}
|
||||
${AutomaticAssetClosure.fragments.asset}
|
||||
`;
|
||||
|
||||
export const withEmbedQuery = withQuery(EMBED_QUERY, {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {gql} from 'react-apollo';
|
||||
import update from 'immutability-helper';
|
||||
import uuid from 'uuid/v4';
|
||||
import {insertCommentIntoEmbedQuery, removeCommentFromEmbedQuery} from './utils';
|
||||
import {mapLeaves} from 'coral-framework/utils';
|
||||
|
||||
export default {
|
||||
fragments: {
|
||||
@@ -218,6 +219,18 @@ export default {
|
||||
},
|
||||
},
|
||||
}),
|
||||
UpdateAssetSettings: ({variables: {input}}) => ({
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (prev) => {
|
||||
const updated = update(prev, {
|
||||
asset: {
|
||||
settings: mapLeaves(input, (leaf) => ({$set: leaf})),
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,10 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 300px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import {Button} from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import cn from 'classnames';
|
||||
import styles from './AssetStatusInfo.css';
|
||||
|
||||
class AssetStatusInfo extends React.Component {
|
||||
|
||||
timer = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.setupTimer(props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setupTimer(nextProps);
|
||||
}
|
||||
|
||||
// Rerendering interval. If remaining time > 1min, rerender every minute, otherwise evey second.
|
||||
interval(closedAt) {
|
||||
const diff = new Date(closedAt).getTime() - new Date().getTime();
|
||||
return diff > 60000 ? 60000 : 1000;
|
||||
}
|
||||
|
||||
// Timer that counts down the remaining time.
|
||||
setupTimer({closedAt, isClosed} = this.props) {
|
||||
if (this.timer && (isClosed || !closedAt)) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
if (isClosed || !closedAt) {
|
||||
this.timer = null;
|
||||
return;
|
||||
}
|
||||
if (!this.timer) {
|
||||
this.timer = setTimeout(() => {
|
||||
this.timer = null;
|
||||
this.forceUpdate();
|
||||
this.setupTimer();
|
||||
}, this.interval(closedAt));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {isClosed, closedAt, onClose, onOpen} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<h3>{!isClosed ? t('configure.close') : t('configure.open')} {t('configure.comment_stream')}</h3>
|
||||
{(!isClosed && closedAt) ? <p>{t('configure.comment_stream_will_close')} {timeago(new Date(closedAt))}.</p> : ''}
|
||||
<div className={cn('close-comments-intro-wrapper', styles.wrapper)}>
|
||||
<p>
|
||||
{!isClosed ? t('configure.open_stream_configuration') : t('configure.close_stream_configuration')}
|
||||
</p>
|
||||
<Button className={styles.button} onClick={!isClosed ? onClose : onOpen}>
|
||||
{!isClosed ? t('configure.close_stream') : t('configure.open_stream')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AssetStatusInfo.propTypes = {
|
||||
isClosed: PropTypes.bool.isRequired,
|
||||
closedAt: PropTypes.string,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AssetStatusInfo;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import AssetStatusInfo from '../containers/AssetStatusInfo';
|
||||
import Settings from '../containers/Settings';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Configure extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className='talk-embed-stream-configuration-container'>
|
||||
<Settings
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
asset={this.props.asset}
|
||||
/>
|
||||
<hr />
|
||||
<AssetStatusInfo
|
||||
asset={this.props.asset}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Configure.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Configure;
|
||||
@@ -0,0 +1,59 @@
|
||||
.root {
|
||||
}
|
||||
|
||||
.iconList {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: buttonReset from "coral-framework/styles/reset.css";
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
background: #F0F0F0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
line-height: 48px;
|
||||
color: #252525;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
border: solid 2px #F0F0F0;
|
||||
outline: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.button:focus {
|
||||
border: solid 2px #00c96B;
|
||||
}
|
||||
|
||||
.buttonActive {
|
||||
border: solid 2px #00796B;
|
||||
}
|
||||
|
||||
.defaultIcon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.questionBox {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import QuestionBox from '../../../components/QuestionBox';
|
||||
import {Icon, Spinner} from 'coral-ui';
|
||||
import DefaultQuestionBoxIcon from '../../../components/DefaultQuestionBoxIcon';
|
||||
import cn from 'classnames';
|
||||
import styles from './QuestionBoxBuilder.css';
|
||||
|
||||
const DefaultIcon = <DefaultQuestionBoxIcon className={styles.defaultIcon} />;
|
||||
|
||||
const icons = [
|
||||
{'default': DefaultIcon},
|
||||
'forum',
|
||||
'build',
|
||||
'format_quote',
|
||||
];
|
||||
|
||||
class QuestionBoxBuilder extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.loadEditor();
|
||||
}
|
||||
|
||||
async loadEditor() {
|
||||
const {default: MarkdownEditor} = await import('coral-framework/components/MarkdownEditor');
|
||||
|
||||
return this.setState({
|
||||
loading : false,
|
||||
MarkdownEditor
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {questionBoxIcon, questionBoxContent, onContentChange, onIconChange} = this.props;
|
||||
const {loading, MarkdownEditor} = this.state;
|
||||
|
||||
if (loading) {
|
||||
return <Spinner/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<h4>Include an Icon</h4>
|
||||
|
||||
<ul className={styles.iconList}>
|
||||
{icons.map((item) => {
|
||||
const name = typeof item === 'object' ? Object.keys(item)[0] : item;
|
||||
const icon = typeof item === 'object' ? item[name] : item;
|
||||
return (
|
||||
<li
|
||||
className={styles.item}
|
||||
key={name}
|
||||
>
|
||||
<button
|
||||
className={cn(
|
||||
styles.button,
|
||||
{[styles.buttonActive]: questionBoxIcon === name},
|
||||
)}
|
||||
onClick={() => onIconChange(name)}
|
||||
>
|
||||
{typeof icon === 'string'
|
||||
? <Icon name={icon} />
|
||||
: icon
|
||||
}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<QuestionBox
|
||||
className={styles.questionBox}
|
||||
icon={questionBoxIcon}
|
||||
content={questionBoxContent}
|
||||
/>
|
||||
|
||||
<MarkdownEditor
|
||||
value={questionBoxContent}
|
||||
onChange={onContentChange}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default QuestionBoxBuilder;
|
||||
+7
-13
@@ -7,9 +7,8 @@
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.wrapper ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
.description {
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@@ -17,19 +16,14 @@
|
||||
margin: 12px 12px 12px 0;
|
||||
}
|
||||
|
||||
.wrapper h4 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.wrapper p {
|
||||
max-width: 380px;
|
||||
.list {
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.questionBoxContainer {
|
||||
margin-top: 24px;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
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 'coral-framework/components/StreamConfiguration';
|
||||
import QuestionBoxBuilder from './QuestionBoxBuilder';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
class Settings extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
settings: {
|
||||
moderation,
|
||||
premodLinksEnable,
|
||||
questionBoxEnable,
|
||||
questionBoxContent,
|
||||
questionBoxIcon,
|
||||
},
|
||||
onToggleModeration,
|
||||
onTogglePremodLinks,
|
||||
onToggleQuestionBox,
|
||||
onQuestionBoxIconChange,
|
||||
onQuestionBoxContentChange,
|
||||
canSave,
|
||||
onApply,
|
||||
slotProps,
|
||||
queryData,
|
||||
} = this.props;
|
||||
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={canSave}
|
||||
cStyle={canSave ? 'green' : 'darkGrey'}
|
||||
onClick={onApply}
|
||||
disabled={!canSave}
|
||||
>
|
||||
{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')}
|
||||
description={t('configure.enable_premod_description')}
|
||||
onCheckbox={onToggleModeration}
|
||||
/>
|
||||
<Configuration
|
||||
checked={premodLinksEnable}
|
||||
title={t('configure.enable_premod_links')}
|
||||
description={t('configure.enable_premod_description')}
|
||||
onCheckbox={onTogglePremodLinks}
|
||||
/>
|
||||
<Configuration
|
||||
checked={questionBoxEnable}
|
||||
title={t('configure.enable_questionbox')}
|
||||
description={t('configure.enable_questionbox_description')}
|
||||
onCheckbox={onToggleQuestionBox}
|
||||
>
|
||||
{
|
||||
questionBoxEnable &&
|
||||
<div className={styles.questionBoxContainer}>
|
||||
<QuestionBoxBuilder
|
||||
questionBoxIcon={questionBoxIcon}
|
||||
questionBoxContent={questionBoxContent}
|
||||
onIconChange={onQuestionBoxIconChange}
|
||||
onContentChange={onQuestionBoxContentChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</Configuration>
|
||||
<Slot
|
||||
fill="streamSettings"
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Settings.propTypes = {
|
||||
queryData: PropTypes.object.isRequired,
|
||||
slotProps: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
onToggleModeration: PropTypes.func.isRequired,
|
||||
onTogglePremodLinks: PropTypes.func.isRequired,
|
||||
onToggleQuestionBox: PropTypes.func.isRequired,
|
||||
onQuestionBoxContentChange: PropTypes.func.isRequired,
|
||||
onQuestionBoxIconChange: PropTypes.func.isRequired,
|
||||
onApply: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import {gql, compose} from 'react-apollo';
|
||||
import {withFragments} from 'coral-framework/hocs';
|
||||
import AssetStatusInfo from '../components/AssetStatusInfo';
|
||||
import PropTypes from 'prop-types';
|
||||
import {withUpdateAssetStatus} from 'coral-framework/graphql/mutations';
|
||||
|
||||
class AssetStatusInfoContainer extends React.Component {
|
||||
|
||||
openAsset = () => this.props.updateAssetStatus(this.props.asset.id, {closedAt: null});
|
||||
closeAsset = () => this.props.updateAssetStatus(this.props.asset.id, {closedAt: new Date().toISOString()});
|
||||
|
||||
render() {
|
||||
return <AssetStatusInfo
|
||||
settings={this.props.asset.settings}
|
||||
isClosed={this.props.asset.isClosed}
|
||||
closedAt={this.props.asset.closedAt}
|
||||
onOpen={this.openAsset}
|
||||
onClose={this.closeAsset}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
AssetStatusInfoContainer.propTypes = {
|
||||
asset: PropTypes.object.isRequired,
|
||||
updateAssetStatus: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const withAssetStatusInfoFragments = withFragments({
|
||||
asset: gql`
|
||||
fragment CoralEmbedStream_AssetStatusInfo_asset on Asset {
|
||||
id
|
||||
closedAt
|
||||
isClosed
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const enhance = compose(
|
||||
withAssetStatusInfoFragments,
|
||||
withUpdateAssetStatus,
|
||||
);
|
||||
|
||||
export default enhance(AssetStatusInfoContainer);
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import {gql, compose} from 'react-apollo';
|
||||
import {withFragments} from 'coral-framework/hocs';
|
||||
import Configure from '../components/Configure';
|
||||
import AssetStatusInfo from './AssetStatusInfo';
|
||||
import Settings from './Settings';
|
||||
import PropTypes from 'prop-types';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
|
||||
class ConfigureContainer extends React.Component {
|
||||
|
||||
render() {
|
||||
return <Configure
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
asset={this.props.asset}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
ConfigureContainer.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const withConfigureFragments = withFragments({
|
||||
root: gql`
|
||||
fragment CoralEmbedStream_Configure_root on RootQuery {
|
||||
__typename
|
||||
...${getDefinitionName(Settings.fragments.root)}
|
||||
}
|
||||
${Settings.fragments.root}
|
||||
`,
|
||||
asset: gql`
|
||||
fragment CoralEmbedStream_Configure_asset on Asset {
|
||||
__typename
|
||||
...${getDefinitionName(AssetStatusInfo.fragments.asset)}
|
||||
...${getDefinitionName(Settings.fragments.asset)}
|
||||
}
|
||||
${AssetStatusInfo.fragments.asset}
|
||||
${Settings.fragments.asset}
|
||||
`,
|
||||
});
|
||||
|
||||
const enhance = compose(
|
||||
withConfigureFragments,
|
||||
);
|
||||
|
||||
export default enhance(ConfigureContainer);
|
||||
@@ -0,0 +1,129 @@
|
||||
import React from 'react';
|
||||
import {gql, compose} from 'react-apollo';
|
||||
import {withFragments, withMergedSettings} from 'coral-framework/hocs';
|
||||
import {getErrorMessages, getSlotFragmentSpreads} 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';
|
||||
|
||||
const slots = [
|
||||
'streamSettings',
|
||||
];
|
||||
|
||||
class SettingsContainer extends React.Component {
|
||||
|
||||
toggleModeration = () => {
|
||||
const updater = {moderation: {$set: this.props.mergedSettings.moderation === 'PRE' ? 'POST' : 'PRE'}};
|
||||
this.props.updatePending({updater});
|
||||
};
|
||||
|
||||
togglePremodLinks = () => {
|
||||
const updater = {premodLinksEnable: {$set: !this.props.mergedSettings.premodLinksEnable}};
|
||||
this.props.updatePending({updater});
|
||||
};
|
||||
|
||||
toggleQuestionBox = () => {
|
||||
const updater = {questionBoxEnable: {$set: !this.props.mergedSettings.questionBoxEnable}};
|
||||
this.props.updatePending({updater});
|
||||
};
|
||||
|
||||
setQuestionBoxIcon = (icon) => {
|
||||
const updater = {questionBoxIcon: {$set: icon}};
|
||||
this.props.updatePending({updater});
|
||||
};
|
||||
|
||||
setQuestionBoxContent = (content) => {
|
||||
const updater = {questionBoxContent: {$set: content}};
|
||||
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() {
|
||||
const {mergedSettings, canSave, data, root, asset, errors, updatePending} = this.props;
|
||||
return <Settings
|
||||
settings={mergedSettings}
|
||||
queryData={{root, asset, settings: mergedSettings}}
|
||||
slotProps={{data, updatePending, errors}}
|
||||
savePending={this.savePending}
|
||||
onToggleModeration={this.toggleModeration}
|
||||
onTogglePremodLinks={this.togglePremodLinks}
|
||||
onToggleQuestionBox={this.toggleQuestionBox}
|
||||
onQuestionBoxIconChange={this.setQuestionBoxIcon}
|
||||
onQuestionBoxContentChange={this.setQuestionBoxContent}
|
||||
onApply={this.savePending}
|
||||
canSave={canSave}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
SettingsContainer.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
pending: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
mergedSettings: PropTypes.object.isRequired,
|
||||
updateAssetSettings: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
updatePending: PropTypes.func.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const withSettingsFragments = withFragments({
|
||||
root: gql`
|
||||
fragment CoralEmbedStream_Settings_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
`,
|
||||
asset: gql`
|
||||
fragment CoralEmbedStream_Settings_asset on Asset {
|
||||
id
|
||||
settings {
|
||||
moderation
|
||||
premodLinksEnable
|
||||
questionBoxEnable
|
||||
questionBoxIcon
|
||||
questionBoxContent
|
||||
${getSlotFragmentSpreads(slots, 'settings')}
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'asset')}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
pending: state.configure.pending,
|
||||
canSave: state.configure.canSave,
|
||||
errors: state.configure.errors,
|
||||
});
|
||||
|
||||
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);
|
||||
+2
-2
@@ -14,7 +14,7 @@ const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
|
||||
// comments to show. The spare cursor functions as a backup in case one
|
||||
// of the comments gets deleted.
|
||||
function resetCursors(state, props) {
|
||||
const comments = props.root.asset.comments;
|
||||
const comments = props.asset.comments;
|
||||
if (comments && comments.nodes.length) {
|
||||
const idCursors = [comments.nodes[0].id];
|
||||
if (comments.nodes[1]) {
|
||||
@@ -30,7 +30,7 @@ function resetCursors(state, props) {
|
||||
// using the help of the backup cursor.
|
||||
function invalidateCursor(invalidated, state, props) {
|
||||
const alt = invalidated === 1 ? 0 : 1;
|
||||
const comments = props.root.asset.comments;
|
||||
const comments = props.asset.comments;
|
||||
const idCursors = [];
|
||||
if (state.idCursors[alt]) {
|
||||
idCursors.push(state.idCursors[alt]);
|
||||
+2
-2
@@ -9,13 +9,13 @@ import {can} from 'coral-framework/services/perms';
|
||||
import {TransitionGroup} from 'react-transition-group';
|
||||
import cn from 'classnames';
|
||||
import styles from './Comment.css';
|
||||
import {THREADING_LEVEL} from '../constants/stream';
|
||||
import {THREADING_LEVEL} from '../../../constants/stream';
|
||||
import merge from 'lodash/merge';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
|
||||
import LoadMore from './LoadMore';
|
||||
import {getEditableUntilDate} from './util';
|
||||
import {findCommentWithId} from '../graphql/utils';
|
||||
import {findCommentWithId} from '../../../graphql/utils';
|
||||
import CommentContent from 'coral-framework/components/CommentContent';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentTombstone from './CommentTombstone';
|
||||
+18
-10
@@ -11,15 +11,14 @@ import RestrictedMessageBox
|
||||
from 'coral-framework/components/RestrictedMessageBox';
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import CommentBox from 'talk-plugin-commentbox/CommentBox';
|
||||
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
|
||||
import QuestionBox from '../../../components/QuestionBox';
|
||||
import {isCommentActive} from 'coral-framework/utils';
|
||||
import {Tab, TabCount, TabPane} from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
|
||||
import {getTopLevelParent, attachCommentToParent} from '../graphql/utils';
|
||||
import {getTopLevelParent, attachCommentToParent} from '../../../graphql/utils';
|
||||
import AllCommentsPane from './AllCommentsPane';
|
||||
import AutomaticAssetClosure from '../containers/AutomaticAssetClosure';
|
||||
import ExtendableTabPanel from '../containers/ExtendableTabPanel';
|
||||
import ExtendableTabPanel from '../../../containers/ExtendableTabPanel';
|
||||
|
||||
import styles from './Stream.css';
|
||||
|
||||
@@ -47,7 +46,8 @@ class Stream extends React.Component {
|
||||
activeReplyBox,
|
||||
setActiveReplyBox,
|
||||
commentClassNames,
|
||||
root: {asset, asset: {comment}},
|
||||
asset,
|
||||
asset: {comment},
|
||||
postComment,
|
||||
notify,
|
||||
editComment,
|
||||
@@ -122,7 +122,8 @@ class Stream extends React.Component {
|
||||
activeReplyBox,
|
||||
setActiveReplyBox,
|
||||
commentClassNames,
|
||||
root: {asset, asset: {comments, totalCommentCount}},
|
||||
asset,
|
||||
asset: {comments, totalCommentCount},
|
||||
postComment,
|
||||
notify,
|
||||
editComment,
|
||||
@@ -178,6 +179,7 @@ class Stream extends React.Component {
|
||||
<AllCommentsPane
|
||||
data={data}
|
||||
root={root}
|
||||
asset={asset}
|
||||
comments={comments}
|
||||
commentClassNames={commentClassNames}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
@@ -185,7 +187,6 @@ class Stream extends React.Component {
|
||||
notify={notify}
|
||||
disableReply={!open}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
@@ -211,7 +212,8 @@ class Stream extends React.Component {
|
||||
data,
|
||||
root,
|
||||
appendItemArray,
|
||||
root: {asset, asset: {comment: highlightedComment}},
|
||||
asset,
|
||||
asset: {comment: highlightedComment},
|
||||
postComment,
|
||||
notify,
|
||||
updateItem,
|
||||
@@ -239,7 +241,6 @@ class Stream extends React.Component {
|
||||
|
||||
return (
|
||||
<div id="stream" className={styles.root}>
|
||||
<AutomaticAssetClosure assetId={asset.id} closedAt={asset.closedAt}/>
|
||||
{open
|
||||
? <div id="commentBox">
|
||||
<InfoBox
|
||||
@@ -250,7 +251,14 @@ class Stream extends React.Component {
|
||||
content={asset.settings.questionBoxContent}
|
||||
enable={asset.settings.questionBoxEnable}
|
||||
icon={asset.settings.questionBoxIcon}
|
||||
/>
|
||||
>
|
||||
<Slot
|
||||
fill="streamQuestionArea"
|
||||
queryData={slotQueryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
</QuestionBox>
|
||||
|
||||
{!banned &&
|
||||
temporarilySuspended &&
|
||||
<RestrictedMessageBox>
|
||||
+1
-1
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './SuspendAccount.css';
|
||||
import styles from './SuspendedAccount.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox';
|
||||
+2
-2
@@ -4,9 +4,9 @@ import Comment from '../components/Comment';
|
||||
import {withFragments} from 'coral-framework/hocs';
|
||||
import {getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import {THREADING_LEVEL} from '../constants/stream';
|
||||
import {THREADING_LEVEL} from '../../../constants/stream';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import {nest} from '../graphql/utils';
|
||||
import {nest} from '../../../graphql/utils';
|
||||
|
||||
const slots = [
|
||||
'streamQuestionArea',
|
||||
+56
-55
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import {gql, compose} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {ADDTL_COMMENTS_ON_LOAD_MORE, THREADING_LEVEL} from '../constants/stream';
|
||||
import {ADDTL_COMMENTS_ON_LOAD_MORE, THREADING_LEVEL} from '../../../constants/stream';
|
||||
import {
|
||||
withPostComment, withPostFlag, withPostDontAgree,
|
||||
withDeleteAction, withEditComment
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
import * as authActions from 'coral-embed-stream/src/actions/auth';
|
||||
import * as notificationActions from 'coral-framework/actions/notification';
|
||||
import {setActiveReplyBox, setActiveTab, viewAllComments} from '../actions/stream';
|
||||
import {setActiveReplyBox, setActiveTab, viewAllComments} from '../../../actions/stream';
|
||||
import Stream from '../components/Stream';
|
||||
import Comment from './Comment';
|
||||
import {withFragments, withEmit} from 'coral-framework/hocs';
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
removeCommentFromEmbedQuery,
|
||||
insertFetchedCommentsIntoEmbedQuery,
|
||||
nest,
|
||||
} from '../graphql/utils';
|
||||
} from '../../../graphql/utils';
|
||||
|
||||
const {showSignInDialog, editName} = authActions;
|
||||
const {notify} = notificationActions;
|
||||
@@ -36,7 +36,7 @@ class StreamContainer extends React.Component {
|
||||
this.commentsEditedSubscription = this.props.data.subscribeToMore({
|
||||
document: COMMENTS_EDITED_SUBSCRIPTION,
|
||||
variables: {
|
||||
assetId: this.props.root.asset.id,
|
||||
assetId: this.props.asset.id,
|
||||
},
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentEdited}}}) => {
|
||||
|
||||
@@ -62,7 +62,7 @@ class StreamContainer extends React.Component {
|
||||
this.commentsAddedSubscription = this.props.data.subscribeToMore({
|
||||
document: COMMENTS_ADDED_SUBSCRIPTION,
|
||||
variables: {
|
||||
assetId: this.props.root.asset.id,
|
||||
assetId: this.props.asset.id,
|
||||
},
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAdded}}}) => {
|
||||
|
||||
@@ -116,7 +116,7 @@ class StreamContainer extends React.Component {
|
||||
limit: parent_id ? 999999 : ADDTL_COMMENTS_ON_LOAD_MORE,
|
||||
cursor: comment.replies.endCursor,
|
||||
parent_id,
|
||||
asset_id: this.props.root.asset.id,
|
||||
asset_id: this.props.asset.id,
|
||||
sortOrder: 'ASC',
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
},
|
||||
@@ -131,9 +131,9 @@ class StreamContainer extends React.Component {
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables: {
|
||||
limit: ADDTL_COMMENTS_ON_LOAD_MORE,
|
||||
cursor: this.props.root.asset.comments.endCursor,
|
||||
cursor: this.props.asset.comments.endCursor,
|
||||
parent_id: null,
|
||||
asset_id: this.props.root.asset.id,
|
||||
asset_id: this.props.asset.id,
|
||||
sortOrder: this.props.data.variables.sortOrder,
|
||||
sortBy: this.props.data.variables.sortBy,
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
@@ -177,9 +177,9 @@ class StreamContainer extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.root.asset
|
||||
|| this.props.root.asset.comment === undefined
|
||||
&& !this.props.root.asset.comments
|
||||
if (!this.props.asset
|
||||
|| this.props.asset.comment === undefined
|
||||
&& !this.props.asset.comments
|
||||
) {
|
||||
return <Spinner />;
|
||||
}
|
||||
@@ -279,49 +279,6 @@ const slots = [
|
||||
const fragments = {
|
||||
root: gql`
|
||||
fragment CoralEmbedStream_Stream_root on RootQuery {
|
||||
asset(id: $assetId, url: $assetUrl) {
|
||||
comment(id: $commentId) @include(if: $hasComment) {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
${nest(`
|
||||
parent {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
...nest
|
||||
}
|
||||
`, THREADING_LEVEL)}
|
||||
}
|
||||
id
|
||||
title
|
||||
url
|
||||
closedAt
|
||||
isClosed
|
||||
created_at
|
||||
settings {
|
||||
moderation
|
||||
infoBoxEnable
|
||||
infoBoxContent
|
||||
premodLinksEnable
|
||||
questionBoxEnable
|
||||
questionBoxContent
|
||||
questionBoxIcon
|
||||
closedTimeout
|
||||
closedMessage
|
||||
charCountEnable
|
||||
charCount
|
||||
requireEmailConfirmation
|
||||
}
|
||||
commentCount @skip(if: $hasComment)
|
||||
totalCommentCount @skip(if: $hasComment)
|
||||
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
|
||||
nodes {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
}
|
||||
hasNextPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'asset')}
|
||||
...${getDefinitionName(Comment.fragments.asset)}
|
||||
}
|
||||
me {
|
||||
status
|
||||
ignoredUsers {
|
||||
@@ -334,8 +291,52 @@ const fragments = {
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
...${getDefinitionName(Comment.fragments.root)}
|
||||
}
|
||||
${Comment.fragments.asset}
|
||||
${Comment.fragments.root}
|
||||
`,
|
||||
asset: gql`
|
||||
fragment CoralEmbedStream_Stream_asset on Asset {
|
||||
comment(id: $commentId) @include(if: $hasComment) {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
${nest(`
|
||||
parent {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
...nest
|
||||
}
|
||||
`, THREADING_LEVEL)}
|
||||
}
|
||||
id
|
||||
title
|
||||
url
|
||||
isClosed
|
||||
created_at
|
||||
settings {
|
||||
moderation
|
||||
infoBoxEnable
|
||||
infoBoxContent
|
||||
premodLinksEnable
|
||||
questionBoxEnable
|
||||
questionBoxContent
|
||||
questionBoxIcon
|
||||
closedTimeout
|
||||
closedMessage
|
||||
charCountEnable
|
||||
charCount
|
||||
requireEmailConfirmation
|
||||
}
|
||||
commentCount @skip(if: $hasComment)
|
||||
totalCommentCount @skip(if: $hasComment)
|
||||
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
|
||||
nodes {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
}
|
||||
hasNextPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'asset')}
|
||||
...${getDefinitionName(Comment.fragments.asset)}
|
||||
}
|
||||
${Comment.fragments.asset}
|
||||
${commentFragment}
|
||||
`,
|
||||
};
|
||||
@@ -275,22 +275,6 @@ button.comment__action-button[disabled],
|
||||
|
||||
/* Close comments */
|
||||
|
||||
.close-comments-intro-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.close-comments-intro-wrapper button {
|
||||
width: 300px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.close-comments-intro-wrapper button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.close-comments-message {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
@@ -107,6 +107,11 @@ export default class MarkdownEditor extends Component {
|
||||
...config,
|
||||
element: this.textarea,
|
||||
});
|
||||
|
||||
// Don't trap the key, to stay accessible.
|
||||
this.editor.codemirror.options.extraKeys['Tab'] = false;
|
||||
this.editor.codemirror.options.extraKeys['Shift-Tab'] = false;
|
||||
|
||||
this.editor.codemirror.on('change', this.onChange);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
.root {
|
||||
position: relative;
|
||||
margin: 12px 12px 24px 0;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.description{
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import Checkbox from 'coral-ui/components/Checkbox';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './StreamConfiguration.css';
|
||||
import uuid from 'uuid/v4';
|
||||
|
||||
class Configuration extends React.Component {
|
||||
|
||||
id = uuid();
|
||||
|
||||
render() {
|
||||
const {title, description, 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 className={styles.description}>{description}</div>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Configuration.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
onCheckbox: PropTypes.func,
|
||||
checked: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Configuration;
|
||||
@@ -17,6 +17,8 @@ export default {
|
||||
'IgnoreUserResponse',
|
||||
'StopIgnoringUserResponse',
|
||||
'UpdateSettingsResponse',
|
||||
'UpdateAssetSettingsResponse',
|
||||
'UpdateAssetStatusResponse',
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ export const withRemoveTag = withMutation(
|
||||
asset_id: assetId,
|
||||
item_type: itemType,
|
||||
},
|
||||
optimisticResponse: {
|
||||
o3timisticResponse: {
|
||||
removeTag: {
|
||||
__typename: 'ModifyTagResponse',
|
||||
errors: null,
|
||||
@@ -363,3 +363,70 @@ export const withUpdateSettings = withMutation(
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
export const withUpdateAssetSettings = withMutation(
|
||||
gql`
|
||||
mutation UpdateAssetSettings($id: ID!, $input: AssetSettingsInput!) {
|
||||
updateAssetSettings(id: $id, input: $input) {
|
||||
...UpdateAssetSettingsResponse
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
updateAssetSettings: (id, input) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
id,
|
||||
input,
|
||||
},
|
||||
optimisticResponse: {
|
||||
updateAssetSettings: {
|
||||
__typename: 'UpdateAssetSettingsResponse',
|
||||
errors: null,
|
||||
}
|
||||
},
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
export const withUpdateAssetStatus = withMutation(
|
||||
gql`
|
||||
mutation UpdateAssetStatus($id: ID!, $input: UpdateAssetStatusInput!) {
|
||||
updateAssetStatus(id: $id, input: $input) {
|
||||
...UpdateAssetStatusResponse
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
updateAssetStatus: (id, input) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
id,
|
||||
input,
|
||||
},
|
||||
optimisticResponse: {
|
||||
updateAssetStatus: {
|
||||
__typename: 'UpdateAssetStatusResponse',
|
||||
errors: null,
|
||||
}
|
||||
},
|
||||
update: (proxy) => {
|
||||
if (input.closedAt !== undefined) {
|
||||
const fragment = gql`
|
||||
fragment Talk_UpdateAssetStatusResponse on Asset {
|
||||
closedAt
|
||||
isClosed
|
||||
}`;
|
||||
|
||||
const fragmentId = `Asset_${id}`;
|
||||
const data = {
|
||||
__typename: 'Asset',
|
||||
closedAt: input.closedAt,
|
||||
isClosed: !!input.closedAt && new Date(input.closedAt).getTime() <= new Date().getTime(),
|
||||
};
|
||||
proxy.writeFragment({fragment, id: fragmentId, data});
|
||||
}
|
||||
}
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
@@ -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,25 @@
|
||||
import {mergeExcludingArrays} from 'coral-framework/utils';
|
||||
import assignWith from 'lodash/assignWith';
|
||||
import get from 'lodash/get';
|
||||
import {withPropsOnChange} from 'recompose';
|
||||
|
||||
/**
|
||||
* Exports a HOC that applies props at `pending` to
|
||||
* props at `settings` and writes into `result` prop name.
|
||||
* `Settings`, and `pending` can have a dotnotation like
|
||||
* "asset.settings".
|
||||
*
|
||||
* Example:
|
||||
* withMergedSettings('asset.settings', 'pending', 'mergedSettings')
|
||||
*/
|
||||
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,8 @@ 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';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
export * from 'coral-framework/helpers/strings';
|
||||
|
||||
export const getTotalActionCount = (type, comment) => {
|
||||
@@ -197,3 +199,21 @@ 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;
|
||||
}
|
||||
|
||||
// Map nested object leaves. Array objects are considered leaves.
|
||||
export function mapLeaves(o, mapper) {
|
||||
return mapValues(o, (val) => {
|
||||
if (typeof val === 'object' && !Array.isArray(val)) {
|
||||
return mapLeaves(val, mapper);
|
||||
}
|
||||
return mapper(val);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
.label {
|
||||
.root {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.label input {
|
||||
visibility: hidden;
|
||||
.input {
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
bottom: 7px;
|
||||
@@ -12,6 +11,7 @@
|
||||
padding: 0;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -19,21 +19,9 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label input[type="checkbox"]:checked + .checkbox:before {
|
||||
content: "\e834";
|
||||
}
|
||||
|
||||
.label input[type="checkbox"] + .checkbox:before {
|
||||
.checkbox:before {
|
||||
content: "\e835";
|
||||
color: #717171;
|
||||
}
|
||||
|
||||
.label.type--green input[type="checkbox"] + .checkbox:before {
|
||||
color: #00a291;
|
||||
}
|
||||
|
||||
.label input[type="checkbox"] + .checkbox:before {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 0px;
|
||||
width: 18px;
|
||||
@@ -49,22 +37,23 @@
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
vertical-align: -6px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-feature-settings: 'liga';
|
||||
font-feature-settings: 'liga';
|
||||
-webkit-transition: all .2s ease;
|
||||
transition: all .2s ease;
|
||||
z-index: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.checkboxInfo {
|
||||
display: inline-block;
|
||||
max-width: 360px;
|
||||
margin-left: 50px;
|
||||
.checkboxChecked:before {
|
||||
content: "\e834";
|
||||
color: #00a291;
|
||||
}
|
||||
|
||||
.checkboxInfo h4 {
|
||||
margin: 0 0 5px;
|
||||
.input:focus + .checkbox:before {
|
||||
color: #00a291;
|
||||
}
|
||||
|
||||
.input:focus + .checkboxChecked:before {
|
||||
color: #00e291;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import React from 'react';
|
||||
import styles from './Checkbox.css';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default ({name, cStyle = 'base', onChange, label, className, info, ...attrs}) => (
|
||||
<label className={`${styles.label} ${styles[`type--${cStyle}`]} ${className}`} htmlFor={name}>
|
||||
<input type="checkbox" id={name} name={name} onChange={onChange} {...attrs} />
|
||||
<span className={styles.checkbox}></span>
|
||||
{label && <span>{label}</span>}
|
||||
{info && (
|
||||
<div className={styles.checkboxInfo}>
|
||||
<h4>{info.title}</h4>
|
||||
<span>{info.description}</span>
|
||||
</div>
|
||||
)}
|
||||
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;
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ const AssetSchema = new Schema({
|
||||
// always after running `rectifySettings` against it.
|
||||
settings: {
|
||||
type: Schema.Types.Mixed,
|
||||
default: null
|
||||
default: {},
|
||||
},
|
||||
|
||||
// Tags are added by the self or by administrators.
|
||||
|
||||
@@ -7,3 +7,4 @@ export {default as CommentTimestamp} from 'coral-framework/components/CommentTim
|
||||
export {default as CommentDetail} from 'coral-framework/components/CommentDetail';
|
||||
export {default as CommentContent} from 'coral-framework/components/CommentContent';
|
||||
export {default as ConfigureCard} from 'coral-framework/components/ConfigureCard';
|
||||
export {default as StreamConfiguration} from 'coral-framework/components/StreamConfiguration';
|
||||
|
||||
+33
-11
@@ -4,6 +4,8 @@ const SettingsService = require('./settings');
|
||||
const domainlist = require('./domainlist');
|
||||
const errors = require('../errors');
|
||||
const merge = require('lodash/merge');
|
||||
const isEmpty = require('lodash/isEmpty');
|
||||
const {dotize} = require('./utils');
|
||||
|
||||
module.exports = class AssetsService {
|
||||
|
||||
@@ -39,7 +41,7 @@ module.exports = class AssetsService {
|
||||
]);
|
||||
|
||||
// If the asset exists and has settings then return the merged object.
|
||||
if (asset && asset.settings) {
|
||||
if (asset && asset.settings && !isEmpty(asset.settings)) {
|
||||
settings = merge({}, globalSettings, asset.settings);
|
||||
} else {
|
||||
settings = globalSettings;
|
||||
@@ -94,18 +96,38 @@ module.exports = class AssetsService {
|
||||
|
||||
/**
|
||||
* Updates the settings for the asset.
|
||||
* @param {[type]} id [description]
|
||||
* @param {[type]} settings [description]
|
||||
* @return {[type]} [description]
|
||||
* @param {String} id id of asset
|
||||
* @param {Object} settings new settings values
|
||||
* @return {Promise}
|
||||
*/
|
||||
static overrideSettings(id, settings) {
|
||||
return AssetModel.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
settings
|
||||
static async overrideSettings(id, settings) {
|
||||
try {
|
||||
const result = await AssetModel.findOneAndUpdate({id}, {
|
||||
|
||||
// The effect of dotize is that only the provided setting values are overwritten
|
||||
// and does not replace the whole object.
|
||||
$set: dotize({settings})
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
return result;
|
||||
} catch (e) {
|
||||
|
||||
// Legacy data models contains `settings=null` as a default which cannot be traversed.
|
||||
// New data models uses `settings={}`.
|
||||
if (e.code === 16837) {
|
||||
|
||||
// Overwrite it fully in this case.
|
||||
const result = await AssetModel.findOneAndUpdate({id}, {
|
||||
$set: {settings}
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
return result;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-26
@@ -1,31 +1,6 @@
|
||||
const SettingModel = require('../models/setting');
|
||||
const errors = require('../errors');
|
||||
|
||||
function dotizeRecurse(result, object, path = '') {
|
||||
for (const key in object) {
|
||||
const newPath = path ? `${path}.${key}` : key;
|
||||
if (typeof object[key] === 'object' && !Array.isArray(object[key])) {
|
||||
dotizeRecurse(result, object[key], newPath);
|
||||
continue;
|
||||
}
|
||||
result[newPath] = object[key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dotize turns a nested object into flattened object with
|
||||
* dotized key notation. Arrays do not become dotized.
|
||||
*
|
||||
* e.g. {a: {b: 'c'}} becomes {'a.b': 'c}
|
||||
*
|
||||
* @param {Object} object
|
||||
* @return {Object} dotized object
|
||||
*/
|
||||
function dotize(object) {
|
||||
const result = {};
|
||||
dotizeRecurse(result, object);
|
||||
return result;
|
||||
}
|
||||
const {dotize} = require('./utils');
|
||||
|
||||
/**
|
||||
* The selector used to uniquely identify the settings document.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
function dotizeRecurse(result, object, path = '') {
|
||||
for (const key in object) {
|
||||
const newPath = path ? `${path}.${key}` : key;
|
||||
if (typeof object[key] === 'object' && !Array.isArray(object[key])) {
|
||||
dotizeRecurse(result, object[key], newPath);
|
||||
continue;
|
||||
}
|
||||
result[newPath] = object[key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dotize turns a nested object into flattened object with
|
||||
* dotized key notation. Arrays do not become dotized.
|
||||
*
|
||||
* e.g. {a: {b: 'c'}} becomes {'a.b': 'c}
|
||||
*
|
||||
* @param {Object} object
|
||||
* @return {Object} dotized object
|
||||
*/
|
||||
function dotize(object) {
|
||||
const result = {};
|
||||
dotizeRecurse(result, object);
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dotize,
|
||||
};
|
||||
@@ -91,7 +91,7 @@ describe('services.AssetsService', () => {
|
||||
.findOrCreateByUrl('https://override.test.com/asset')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('settings');
|
||||
expect(asset.settings).to.be.null;
|
||||
expect(asset.settings).to.be.empty;
|
||||
|
||||
return AssetsService.overrideSettings(asset.id, {moderation: 'PRE'});
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user