diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js
index d7656d9a5..42e96f382 100644
--- a/client/coral-admin/src/graphql/index.js
+++ b/client/coral-admin/src/graphql/index.js
@@ -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: {
diff --git a/client/coral-admin/src/routes/Configure/components/ModerationSettings.js b/client/coral-admin/src/routes/Configure/components/ModerationSettings.js
index f5db719bb..94aa7d923 100644
--- a/client/coral-admin/src/routes/Configure/components/ModerationSettings.js
+++ b/client/coral-admin/src/routes/Configure/components/ModerationSettings.js
@@ -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 (
);
@@ -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,
diff --git a/client/coral-admin/src/routes/Configure/components/StreamSettings.js b/client/coral-admin/src/routes/Configure/components/StreamSettings.js
index ffaeed63b..82441632d 100644
--- a/client/coral-admin/src/routes/Configure/components/StreamSettings.js
+++ b/client/coral-admin/src/routes/Configure/components/StreamSettings.js
@@ -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 (
);
diff --git a/client/coral-admin/src/routes/Configure/components/TechSettings.js b/client/coral-admin/src/routes/Configure/components/TechSettings.js
index 7f16b6064..1b783c9ec 100644
--- a/client/coral-admin/src/routes/Configure/components/TechSettings.js
+++ b/client/coral-admin/src/routes/Configure/components/TechSettings.js
@@ -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 (
);
@@ -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,
diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js
index 8f80a3e24..dfa8df876 100644
--- a/client/coral-admin/src/routes/Configure/containers/Configure.js
+++ b/client/coral-admin/src/routes/Configure/containers/Configure.js
@@ -2,38 +2,20 @@ import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
-import withQuery from 'coral-framework/hocs/withQuery';
+import {withQuery, withMergedSettings} from 'coral-framework/hocs';
import {Spinner} from 'coral-ui';
import {notify} from 'coral-framework/actions/notification';
import PropTypes from 'prop-types';
-import assignWith from 'lodash/assignWith';
import {withUpdateSettings} from 'coral-framework/graphql/mutations';
import {getErrorMessages, getDefinitionName} from 'coral-framework/utils';
import StreamSettings from './StreamSettings';
import TechSettings from './TechSettings';
import ModerationSettings from './ModerationSettings';
import {clearPending, setActiveSection} from '../../../actions/configure';
-
import Configure from '../components/Configure';
-// Like lodash merge but does not recurse into arrays.
-const mergeExcludingArrays = (objValue, srcValue) => {
- if (typeof srcValue === 'object' && !Array.isArray(srcValue)) {
- return assignWith({}, objValue, srcValue, mergeExcludingArrays);
- }
- return srcValue;
-};
-
class ConfigureContainer extends Component {
- // Merge current settings with pending settings.
- getMergedSettings = (props = this.props) => {
- return assignWith({}, props.root.settings, props.pending, mergeExcludingArrays);
- }
-
- // Cached merged settings.
- mergedSettings = this.getMergedSettings();
-
savePending = async () => {
try {
await this.props.updateSettings(this.props.pending);
@@ -44,14 +26,6 @@ class ConfigureContainer extends Component {
}
};
- componentWillReceiveProps(nextProps) {
-
- // Recalculate merged settings when necessary.
- if (this.props.root.settings !== nextProps.root.settings || this.props.pending !== nextProps.pending) {
- this.mergedSettings = this.getMergedSettings(nextProps);
- }
- }
-
render () {
if(this.props.data.loading) {
return ;
@@ -62,7 +36,7 @@ class ConfigureContainer extends Component {
auth={this.props.auth}
data={this.props.data}
root={this.props.root}
- settings={this.mergedSettings}
+ settings={this.props.mergedSettings}
canSave={this.props.canSave}
savePending={this.savePending}
setActiveSection={this.props.setActiveSection}
@@ -112,6 +86,7 @@ export default compose(
withUpdateSettings,
withConfigureQuery,
connect(mapStateToProps, mapDispatchToProps),
+ withMergedSettings('root.settings', 'pending', 'mergedSettings'),
)(ConfigureContainer);
ConfigureContainer.propTypes = {
@@ -124,5 +99,6 @@ ConfigureContainer.propTypes = {
root: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
pending: PropTypes.object.isRequired,
+ mergedSettings: PropTypes.object.isRequired,
activeSection: PropTypes.string.isRequired,
};
diff --git a/client/coral-admin/src/routes/Configure/containers/ModerationSettings.js b/client/coral-admin/src/routes/Configure/containers/ModerationSettings.js
index 0583378a9..61538f4c0 100644
--- a/client/coral-admin/src/routes/Configure/containers/ModerationSettings.js
+++ b/client/coral-admin/src/routes/Configure/containers/ModerationSettings.js
@@ -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);
diff --git a/client/coral-admin/src/routes/Configure/containers/TechSettings.js b/client/coral-admin/src/routes/Configure/containers/TechSettings.js
index df33a3f9a..1f5ac20b8 100644
--- a/client/coral-admin/src/routes/Configure/containers/TechSettings.js
+++ b/client/coral-admin/src/routes/Configure/containers/TechSettings.js
@@ -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);
diff --git a/client/coral-configure/components/CloseCommentsInfo.js b/client/coral-configure/components/CloseCommentsInfo.js
deleted file mode 100644
index d10419f3f..000000000
--- a/client/coral-configure/components/CloseCommentsInfo.js
+++ /dev/null
@@ -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' ? (
-
-
- {t('configure.open_stream_configuration')}
-
-
-
- ) : (
-
-
- {t('configure.close_stream_configuration')}
-
-
-
- )
-);
-
-CloseCommentsInfo.propTypes = {
- status: PropTypes.string,
- onClick: PropTypes.func,
-};
-
-export default CloseCommentsInfo;
\ No newline at end of file
diff --git a/client/coral-configure/components/ConfigureCommentStream.js b/client/coral-configure/components/ConfigureCommentStream.js
deleted file mode 100644
index 20250ee86..000000000
--- a/client/coral-configure/components/ConfigureCommentStream.js
+++ /dev/null
@@ -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}) => (
-
-);
diff --git a/client/coral-configure/components/Markdown.js b/client/coral-configure/components/Markdown.js
deleted file mode 100644
index d9d1bc4a7..000000000
--- a/client/coral-configure/components/Markdown.js
+++ /dev/null
@@ -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) =>
- `${text}`;
-
-marked.setOptions({renderer});
-
-export default class Markdown extends PureComponent {
- render() {
- const {content, ...rest} = this.props;
- const __html = marked(content);
- return ;
- }
-}
-
-Markdown.propTypes = {
- content: PropTypes.string,
-};
-
diff --git a/client/coral-configure/components/QuestionBoxBuilder.css b/client/coral-configure/components/QuestionBoxBuilder.css
deleted file mode 100644
index 758f350b9..000000000
--- a/client/coral-configure/components/QuestionBoxBuilder.css
+++ /dev/null
@@ -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;
-}
\ No newline at end of file
diff --git a/client/coral-configure/components/QuestionBoxBuilder.js b/client/coral-configure/components/QuestionBoxBuilder.js
deleted file mode 100644
index 28dfc30b3..000000000
--- a/client/coral-configure/components/QuestionBoxBuilder.js
+++ /dev/null
@@ -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 ;
- }
-
- return (
-
-
Include an Icon
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
-
-
handleChange({}, {questionBoxContent: value})}
- />
-
-
- );
- }
-}
-
-export default QuestionBoxBuilder;
diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js
deleted file mode 100644
index 0ab71c1d7..000000000
--- a/client/coral-configure/containers/ConfigureStreamContainer.js
+++ /dev/null
@@ -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 (
-
-
-
-
{closedAt === 'open' ? t('configure.close') : t('configure.open')} {t('configure.comment_stream')}
- {(closedAt === 'open' && closedTimeout) ?
{t('configure.comment_stream_will_close')} {this.getClosedIn()}.
: ''}
-
-
- );
- }
-}
-
-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);
diff --git a/client/coral-embed-stream/src/actions/configure.js b/client/coral-embed-stream/src/actions/configure.js
new file mode 100644
index 000000000..bfc738fe0
--- /dev/null
+++ b/client/coral-embed-stream/src/actions/configure.js
@@ -0,0 +1,9 @@
+import * as actions from '../constants/configure';
+
+export const updatePending = ({updater, errorUpdater}) => {
+ return {type: actions.UPDATE_PENDING, updater, errorUpdater};
+};
+
+export const clearPending = () => {
+ return {type: actions.CLEAR_PENDING};
+};
diff --git a/client/coral-configure/components/DefaultIcon.css b/client/coral-embed-stream/src/components/DefaultQuestionBoxIcon.css
similarity index 59%
rename from client/coral-configure/components/DefaultIcon.css
rename to client/coral-embed-stream/src/components/DefaultQuestionBoxIcon.css
index 6125d2f6a..414a48e7a 100644
--- a/client/coral-configure/components/DefaultIcon.css
+++ b/client/coral-embed-stream/src/components/DefaultQuestionBoxIcon.css
@@ -16,12 +16,5 @@
color: #262626;
}
-.qbIconContainer {
- position: relative;
- border: 0;
- color: white;
- display: inline-block;
- padding: 5px 20px;
- vertical-align: middle;
- width: 10px;
-}
\ No newline at end of file
+.root {
+}
diff --git a/client/coral-configure/components/DefaultIcon.js b/client/coral-embed-stream/src/components/DefaultQuestionBoxIcon.js
similarity index 54%
rename from client/coral-configure/components/DefaultIcon.js
rename to client/coral-embed-stream/src/components/DefaultQuestionBoxIcon.js
index 5f6f4876c..acb3f2e64 100644
--- a/client/coral-configure/components/DefaultIcon.js
+++ b/client/coral-embed-stream/src/components/DefaultQuestionBoxIcon.js
@@ -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}) => (
-
+const DefaultQuestionBoxIcon = ({className}) => (
+
);
-export default DefaultIcon;
+export default DefaultQuestionBoxIcon;
diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js
index 5ce7021fb..7445bf300 100644
--- a/client/coral-embed-stream/src/components/Embed.js
+++ b/client/coral-embed-stream/src/components/Embed.js
@@ -1,16 +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 {TabBar, Tab, TabContent, TabPane} from 'coral-ui';
+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 {
@@ -21,20 +22,37 @@ 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);
};
- render() {
- const {activeTab, commentId, root, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog, router: {location: {query: {parentUrl}}}} = this.props;
+ getTabs() {
const {user} = this.props.auth;
+ const tabs = [
+
+ {t('embed_comments_tab')}
+ ,
+
+ {t('framework.my_profile')}
+ ,
+ ];
+ if (can(user, 'UPDATE_CONFIG')) {
+ tabs.push(
+
+ {t('framework.configure_stream')}
+
+ );
+ }
+ return tabs;
+ }
+
+ render() {
+ const {activeTab, commentId, root, root: {asset}, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog, router: {location: {query: {parentUrl}}}} = this.props;
const hasHighlightedComment = !!commentId;
return (
+
-
-
- {t('embed_comments_tab')}
-
-
- {t('framework.my_profile')}
-
- {can(user, 'UPDATE_CONFIG') &&
-
- {t('framework.configure_stream')}
-
- }
-
+
-
-
-
-
-
-
-
-
-
-
-
+ setActiveTab={this.changeTab}
+ fallbackTab='stream'
+ tabSlot='embedStreamTabs'
+ tabSlotPrepend='embedStreamTabsPrepend'
+ tabPaneSlot='embedStreamTabPanes'
+ slotProps={{data}}
+ queryData={{root}}
+ tabs={this.getTabs()}
+ tabPanes={[
+
+
+ ,
+
+
+ ,
+
+
+ ,
+ ]}
+ />
);
}
diff --git a/client/coral-embed-stream/src/components/StreamTabPanel.css b/client/coral-embed-stream/src/components/ExtendableTabPanel.css
similarity index 100%
rename from client/coral-embed-stream/src/components/StreamTabPanel.css
rename to client/coral-embed-stream/src/components/ExtendableTabPanel.css
diff --git a/client/coral-embed-stream/src/components/StreamTabPanel.js b/client/coral-embed-stream/src/components/ExtendableTabPanel.js
similarity index 85%
rename from client/coral-embed-stream/src/components/StreamTabPanel.js
rename to client/coral-embed-stream/src/components/ExtendableTabPanel.js
index 35c10d5d9..ec582eb84 100644
--- a/client/coral-embed-stream/src/components/StreamTabPanel.js
+++ b/client/coral-embed-stream/src/components/ExtendableTabPanel.js
@@ -1,9 +1,9 @@
import React from 'react';
import {Spinner, TabBar, TabContent} from 'coral-ui';
import PropTypes from 'prop-types';
-import styles from './StreamTabPanel.css';
+import styles from './ExtendableTabPanel.css';
-class StreamTabPanel extends React.Component {
+class ExtendableTabPanel extends React.Component {
render() {
const {activeTab, setActiveTab, tabs, tabPanes, sub, loading, ...rest} = this.props;
@@ -23,7 +23,7 @@ class StreamTabPanel extends React.Component {
}
}
-StreamTabPanel.propTypes = {
+ExtendableTabPanel.propTypes = {
activeTab: PropTypes.string.isRequired,
setActiveTab: PropTypes.func.isRequired,
loading: PropTypes.bool,
@@ -39,4 +39,4 @@ StreamTabPanel.propTypes = {
sub: PropTypes.bool,
};
-export default StreamTabPanel;
+export default ExtendableTabPanel;
diff --git a/client/talk-plugin-questionbox/QuestionBox.css b/client/coral-embed-stream/src/components/QuestionBox.css
similarity index 95%
rename from client/talk-plugin-questionbox/QuestionBox.css
rename to client/coral-embed-stream/src/components/QuestionBox.css
index b8df1f228..f8c8e0eb2 100644
--- a/client/talk-plugin-questionbox/QuestionBox.css
+++ b/client/coral-embed-stream/src/components/QuestionBox.css
@@ -65,8 +65,3 @@
justify-content: center;
font-weight: 400;
}
-
-.hidden {
- visibility: hidden;
- display: none;
-}
diff --git a/client/talk-plugin-questionbox/QuestionBox.js b/client/coral-embed-stream/src/components/QuestionBox.js
similarity index 59%
rename from client/talk-plugin-questionbox/QuestionBox.js
rename to client/coral-embed-stream/src/components/QuestionBox.js
index f15549b7c..1f34afcd7 100644
--- a/client/talk-plugin-questionbox/QuestionBox.js
+++ b/client/coral-embed-stream/src/components/QuestionBox.js
@@ -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 = ''}) => (
-
+const QuestionBox = ({content, icon, className, children}) => (
+
{
icon === 'default' ? (
-
-
+
) : (
@@ -23,8 +21,7 @@ const QuestionBox = ({content, enable, icon = '', className = ''}) => (
-
-
+ {children}
);
diff --git a/client/coral-embed-stream/src/constants/configure.js b/client/coral-embed-stream/src/constants/configure.js
new file mode 100644
index 000000000..b29cf6280
--- /dev/null
+++ b/client/coral-embed-stream/src/constants/configure.js
@@ -0,0 +1,4 @@
+const prefix = 'TALK_EMBED_STREAM_CONFIGURE';
+
+export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`;
+export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`;
diff --git a/client/coral-embed-stream/src/containers/AutomaticAssetClosure.js b/client/coral-embed-stream/src/containers/AutomaticAssetClosure.js
index 487e927a8..f6ade1de0 100644
--- a/client/coral-embed-stream/src/containers/AutomaticAssetClosure.js
+++ b/client/coral-embed-stream/src/containers/AutomaticAssetClosure.js
@@ -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);
diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js
index 132e7f5e7..7a8df6e7c 100644
--- a/client/coral-embed-stream/src/containers/Embed.js
+++ b/client/coral-embed-stream/src/containers/Embed.js
@@ -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';
@@ -151,6 +153,9 @@ const USERNAME_REJECTED_SUBSCRIPTION = gql`
const slots = [
'embed',
+ 'embedStreamTabs',
+ 'embedStreamTabsPrepend',
+ 'embedStreamTabPanes',
];
const EMBED_QUERY = gql`
@@ -167,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, {
diff --git a/client/coral-embed-stream/src/containers/StreamTabPanel.js b/client/coral-embed-stream/src/containers/ExtendableTabPanel.js
similarity index 66%
rename from client/coral-embed-stream/src/containers/StreamTabPanel.js
rename to client/coral-embed-stream/src/containers/ExtendableTabPanel.js
index 9a154512d..b582f7b66 100644
--- a/client/coral-embed-stream/src/containers/StreamTabPanel.js
+++ b/client/coral-embed-stream/src/containers/ExtendableTabPanel.js
@@ -1,5 +1,5 @@
import React from 'react';
-import StreamTabPanel from '../components/StreamTabPanel';
+import ExtendableTabPanel from '../components/ExtendableTabPanel';
import {connect} from 'react-redux';
import omit from 'lodash/omit';
import {Tab, TabPane} from 'coral-ui';
@@ -7,17 +7,17 @@ import {getShallowChanges} from 'coral-framework/utils';
import isEqual from 'lodash/isEqual';
import PropTypes from 'prop-types';
-class StreamTabPanelContainer extends React.Component {
+class ExtendableTabPanelContainer extends React.Component {
static contextTypes = {
plugins: PropTypes.object,
};
componentDidMount() {
- this.fallbackAllTab();
+ this.handleFallback();
}
componentWillReceiveProps(next) {
- this.fallbackAllTab(next);
+ this.handleFallback(next);
}
shouldComponentUpdate(next) {
@@ -35,22 +35,31 @@ class StreamTabPanelContainer extends React.Component {
return changes.length !== 0;
}
- fallbackAllTab(props = this.props) {
- if (props.activeTab !== props.fallbackTab) {
- const slotPlugins = this.getSlotElements(props.tabSlot, props).map((el) => el.type.talkPluginName);
- if (slotPlugins.indexOf(props.activeTab) === -1) {
- props.setActiveTab(props.fallbackTab);
- }
+ handleFallback(props = this.props) {
+ if (this.getTabNames(props).indexOf(props.activeTab) === -1) {
+ props.setActiveTab(props.fallbackTab);
}
}
+ getTabNames(props = this.props) {
+ return this.getTabElements(props).map((el) => el.props.tabId);
+ }
+
getSlotElements(slot, props = this.props) {
const {plugins} = this.context;
return plugins.getSlotElements(slot, props.reduxState, props.slotProps, props.queryData);
}
getPluginTabElements(props = this.props) {
- return this.getSlotElements(props.tabSlot).map((el) => {
+ return this.getSlotTabElements(props.tabSlot);
+ }
+
+ getPluginTabElementsPrepend(props = this.props) {
+ return this.getSlotTabElements(props.tabSlotPrepend);
+ }
+
+ getSlotTabElements(slot) {
+ return this.getSlotElements(slot).map((el) => {
return (
{React.cloneElement(el, {active: this.props.activeTab === el.type.talkPluginName})}
@@ -59,6 +68,17 @@ class StreamTabPanelContainer extends React.Component {
});
}
+ getTabElements(props = this.props) {
+ const elements = [...this.getPluginTabElementsPrepend(props)];
+ if (Array.isArray(props.tabs)) {
+ elements.push(...props.tabs);
+ } else {
+ elements.push(props.tabs);
+ }
+ elements.push(...this.getPluginTabElements(props));
+ return elements;
+ }
+
getPluginTabPaneElements(props = this.props) {
return this.getSlotElements(props.tabPaneSlot).map((el) => {
return (
@@ -71,12 +91,12 @@ class StreamTabPanelContainer extends React.Component {
render() {
return (
-
@@ -84,19 +104,20 @@ class StreamTabPanelContainer extends React.Component {
}
}
-StreamTabPanelContainer.propTypes = {
+ExtendableTabPanelContainer.propTypes = {
activeTab: PropTypes.string.isRequired,
setActiveTab: PropTypes.func.isRequired,
- appendTabs: PropTypes.oneOfType([
+ tabs: PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element)
]),
- appendTabPanes: PropTypes.oneOfType([
+ tabPanes: PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element)
]),
fallbackTab: PropTypes.string.isRequired,
tabSlot: PropTypes.string.isRequired,
+ tabSlotPrepend: PropTypes.string.isRequired,
tabPaneSlot: PropTypes.string.isRequired,
slotProps: PropTypes.object.isRequired,
queryData: PropTypes.object,
@@ -109,4 +130,4 @@ const mapStateToProps = (state) => ({
reduxState: omit(state, 'apollo'),
});
-export default connect(mapStateToProps, null)(StreamTabPanelContainer);
+export default connect(mapStateToProps, null)(ExtendableTabPanelContainer);
diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js
index d18cdc435..d77e6356a 100644
--- a/client/coral-embed-stream/src/graphql/index.js
+++ b/client/coral-embed-stream/src/graphql/index.js
@@ -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;
+ }
+ }
+ }),
},
};
diff --git a/client/coral-embed-stream/src/reducers/configure.js b/client/coral-embed-stream/src/reducers/configure.js
new file mode 100644
index 000000000..0a94d5a54
--- /dev/null
+++ b/client/coral-embed-stream/src/reducers/configure.js
@@ -0,0 +1,42 @@
+import * as actions from '../constants/configure';
+import isEmpty from 'lodash/isEmpty';
+import update from 'immutability-helper';
+
+const initialState = {
+ canSave: false,
+ pending: {},
+ errors: {},
+};
+
+export default function config(state = initialState, action) {
+ switch (action.type) {
+ case actions.UPDATE_PENDING: {
+ let next = state;
+ if (action.updater) {
+ next = update(next, {
+ pending: action.updater,
+ });
+ }
+ if (action.errorUpdater) {
+ next = update(next, {
+ errors: action.errorUpdater,
+ });
+ }
+ const noErrors = Object.keys(next.errors).reduce((res, error) => res && !next.errors[error], true);
+ const canSave = !isEmpty(next.pending) && noErrors;
+ next = update(next, {
+ canSave: {$set: canSave},
+ });
+
+ return next;
+ }
+ case actions.CLEAR_PENDING:
+ return {
+ ...state,
+ pending: {},
+ canSave: false,
+ };
+ default:
+ return state;
+ }
+}
diff --git a/client/coral-embed-stream/src/reducers/index.js b/client/coral-embed-stream/src/reducers/index.js
index 5c553b04f..d6d0c7693 100644
--- a/client/coral-embed-stream/src/reducers/index.js
+++ b/client/coral-embed-stream/src/reducers/index.js
@@ -2,6 +2,7 @@ import auth from './auth';
import asset from './asset';
import embed from './embed';
import config from './config';
+import configure from './configure';
import stream from './stream';
import {reducer as commentBox} from '../../../talk-plugin-commentbox';
@@ -11,5 +12,6 @@ export default {
commentBox,
embed,
config,
+ configure,
stream,
};
diff --git a/client/coral-embed-stream/src/tabs/configure/components/AssetStatusInfo.css b/client/coral-embed-stream/src/tabs/configure/components/AssetStatusInfo.css
new file mode 100644
index 000000000..3dfd98ccc
--- /dev/null
+++ b/client/coral-embed-stream/src/tabs/configure/components/AssetStatusInfo.css
@@ -0,0 +1,10 @@
+.wrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.button {
+ width: 300px;
+ margin-left: 20px;
+}
diff --git a/client/coral-embed-stream/src/tabs/configure/components/AssetStatusInfo.js b/client/coral-embed-stream/src/tabs/configure/components/AssetStatusInfo.js
new file mode 100644
index 000000000..d3c2f14d7
--- /dev/null
+++ b/client/coral-embed-stream/src/tabs/configure/components/AssetStatusInfo.js
@@ -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 (
+
+
{!isClosed ? t('configure.close') : t('configure.open')} {t('configure.comment_stream')}
+ {(!isClosed && closedAt) ?
{t('configure.comment_stream_will_close')} {timeago(new Date(closedAt))}.
: ''}
+
+
+ {!isClosed ? t('configure.open_stream_configuration') : t('configure.close_stream_configuration')}
+
+
+
+
+ );
+ }
+}
+
+AssetStatusInfo.propTypes = {
+ isClosed: PropTypes.bool.isRequired,
+ closedAt: PropTypes.string,
+ onClose: PropTypes.func.isRequired,
+ onOpen: PropTypes.func.isRequired,
+};
+
+export default AssetStatusInfo;
diff --git a/client/coral-embed-stream/src/tabs/configure/components/Configure.js b/client/coral-embed-stream/src/tabs/configure/components/Configure.js
new file mode 100644
index 000000000..a7000864e
--- /dev/null
+++ b/client/coral-embed-stream/src/tabs/configure/components/Configure.js
@@ -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 (
+
+ );
+ }
+}
+
+Configure.propTypes = {
+ data: PropTypes.object.isRequired,
+ root: PropTypes.object.isRequired,
+ asset: PropTypes.object.isRequired,
+};
+
+export default Configure;
diff --git a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.css b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.css
new file mode 100644
index 000000000..8dc6091fa
--- /dev/null
+++ b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.css
@@ -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;
+}
+
diff --git a/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js
new file mode 100644
index 000000000..0d44b4a3a
--- /dev/null
+++ b/client/coral-embed-stream/src/tabs/configure/components/QuestionBoxBuilder.js
@@ -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 = ;
+
+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 ;
+ }
+
+ return (
+
+
Include an Icon
+
+
+ {icons.map((item) => {
+ const name = typeof item === 'object' ? Object.keys(item)[0] : item;
+ const icon = typeof item === 'object' ? item[name] : item;
+ return (
+ -
+
+
+ );
+ })}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default QuestionBoxBuilder;
diff --git a/client/coral-configure/components/ConfigureCommentStream.css b/client/coral-embed-stream/src/tabs/configure/components/Settings.css
similarity index 59%
rename from client/coral-configure/components/ConfigureCommentStream.css
rename to client/coral-embed-stream/src/tabs/configure/components/Settings.css
index 5575e073b..1a97c3fc1 100644
--- a/client/coral-configure/components/ConfigureCommentStream.css
+++ b/client/coral-embed-stream/src/tabs/configure/components/Settings.css
@@ -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;
-}
\ No newline at end of file
+.questionBoxContainer {
+ margin-top: 24px;
+}
diff --git a/client/coral-embed-stream/src/tabs/configure/components/Settings.js b/client/coral-embed-stream/src/tabs/configure/components/Settings.js
new file mode 100644
index 000000000..4e89172d2
--- /dev/null
+++ b/client/coral-embed-stream/src/tabs/configure/components/Settings.js
@@ -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 (
+
+
+
{t('configure.title')}
+
+
{t('configure.description')}
+
+
+
+
+
+ {
+ questionBoxEnable &&
+
+
+
+ }
+
+
+
+
+ );
+ }
+}
+
+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;
diff --git a/client/coral-embed-stream/src/tabs/configure/containers/AssetStatusInfo.js b/client/coral-embed-stream/src/tabs/configure/containers/AssetStatusInfo.js
new file mode 100644
index 000000000..3d4248e24
--- /dev/null
+++ b/client/coral-embed-stream/src/tabs/configure/containers/AssetStatusInfo.js
@@ -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 ;
+ }
+}
+
+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);
diff --git a/client/coral-embed-stream/src/tabs/configure/containers/Configure.js b/client/coral-embed-stream/src/tabs/configure/containers/Configure.js
new file mode 100644
index 000000000..b88306262
--- /dev/null
+++ b/client/coral-embed-stream/src/tabs/configure/containers/Configure.js
@@ -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 ;
+ }
+}
+
+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);
diff --git a/client/coral-embed-stream/src/tabs/configure/containers/Settings.js b/client/coral-embed-stream/src/tabs/configure/containers/Settings.js
new file mode 100644
index 000000000..afad3482d
--- /dev/null
+++ b/client/coral-embed-stream/src/tabs/configure/containers/Settings.js
@@ -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 ;
+ }
+}
+
+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);
diff --git a/client/coral-embed-stream/src/components/AllCommentsPane.js b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js
similarity index 98%
rename from client/coral-embed-stream/src/components/AllCommentsPane.js
rename to client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js
index 2967b9972..3a64820c7 100644
--- a/client/coral-embed-stream/src/components/AllCommentsPane.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js
@@ -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]);
diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/tabs/stream/components/Comment.css
similarity index 100%
rename from client/coral-embed-stream/src/components/Comment.css
rename to client/coral-embed-stream/src/tabs/stream/components/Comment.css
diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/tabs/stream/components/Comment.js
similarity index 99%
rename from client/coral-embed-stream/src/components/Comment.js
rename to client/coral-embed-stream/src/tabs/stream/components/Comment.js
index 7ae36b5fa..3da59afdc 100644
--- a/client/coral-embed-stream/src/components/Comment.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.js
@@ -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';
diff --git a/client/coral-embed-stream/src/components/CommentTombstone.css b/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.css
similarity index 100%
rename from client/coral-embed-stream/src/components/CommentTombstone.css
rename to client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.css
diff --git a/client/coral-embed-stream/src/components/CommentTombstone.js b/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js
similarity index 100%
rename from client/coral-embed-stream/src/components/CommentTombstone.js
rename to client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js
diff --git a/client/coral-embed-stream/src/components/CountdownSeconds.js b/client/coral-embed-stream/src/tabs/stream/components/CountdownSeconds.js
similarity index 100%
rename from client/coral-embed-stream/src/components/CountdownSeconds.js
rename to client/coral-embed-stream/src/tabs/stream/components/CountdownSeconds.js
diff --git a/client/coral-embed-stream/src/components/EditableCommentContent.js b/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js
similarity index 100%
rename from client/coral-embed-stream/src/components/EditableCommentContent.js
rename to client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js
diff --git a/client/coral-embed-stream/src/components/IgnoreUserWizard.js b/client/coral-embed-stream/src/tabs/stream/components/IgnoreUserWizard.js
similarity index 100%
rename from client/coral-embed-stream/src/components/IgnoreUserWizard.js
rename to client/coral-embed-stream/src/tabs/stream/components/IgnoreUserWizard.js
diff --git a/client/coral-embed-stream/src/components/InactiveCommentLabel.css b/client/coral-embed-stream/src/tabs/stream/components/InactiveCommentLabel.css
similarity index 100%
rename from client/coral-embed-stream/src/components/InactiveCommentLabel.css
rename to client/coral-embed-stream/src/tabs/stream/components/InactiveCommentLabel.css
diff --git a/client/coral-embed-stream/src/components/InactiveCommentLabel.js b/client/coral-embed-stream/src/tabs/stream/components/InactiveCommentLabel.js
similarity index 100%
rename from client/coral-embed-stream/src/components/InactiveCommentLabel.js
rename to client/coral-embed-stream/src/tabs/stream/components/InactiveCommentLabel.js
diff --git a/client/coral-embed-stream/src/components/LoadMore.js b/client/coral-embed-stream/src/tabs/stream/components/LoadMore.js
similarity index 100%
rename from client/coral-embed-stream/src/components/LoadMore.js
rename to client/coral-embed-stream/src/tabs/stream/components/LoadMore.js
diff --git a/client/coral-embed-stream/src/components/NewCount.js b/client/coral-embed-stream/src/tabs/stream/components/NewCount.js
similarity index 100%
rename from client/coral-embed-stream/src/components/NewCount.js
rename to client/coral-embed-stream/src/tabs/stream/components/NewCount.js
diff --git a/client/coral-embed-stream/src/components/NoComments.css b/client/coral-embed-stream/src/tabs/stream/components/NoComments.css
similarity index 100%
rename from client/coral-embed-stream/src/components/NoComments.css
rename to client/coral-embed-stream/src/tabs/stream/components/NoComments.css
diff --git a/client/coral-embed-stream/src/components/NoComments.js b/client/coral-embed-stream/src/tabs/stream/components/NoComments.js
similarity index 100%
rename from client/coral-embed-stream/src/components/NoComments.js
rename to client/coral-embed-stream/src/tabs/stream/components/NoComments.js
diff --git a/client/coral-embed-stream/src/components/Stream.css b/client/coral-embed-stream/src/tabs/stream/components/Stream.css
similarity index 100%
rename from client/coral-embed-stream/src/components/Stream.css
rename to client/coral-embed-stream/src/tabs/stream/components/Stream.css
diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/tabs/stream/components/Stream.js
similarity index 90%
rename from client/coral-embed-stream/src/components/Stream.js
rename to client/coral-embed-stream/src/tabs/stream/components/Stream.js
index 67d27f903..b5f45b8e5 100644
--- a/client/coral-embed-stream/src/components/Stream.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/Stream.js
@@ -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 StreamTabPanel from '../containers/StreamTabPanel';
+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,
@@ -115,14 +115,15 @@ class Stream extends React.Component {
);
}
- renderTabPanel() {
+ renderExtendableTabPanel() {
const {
data,
root,
activeReplyBox,
setActiveReplyBox,
commentClassNames,
- root: {asset, asset: {comments, totalCommentCount}},
+ asset,
+ asset: {comments, totalCommentCount},
postComment,
notify,
editComment,
@@ -144,7 +145,7 @@ class Stream extends React.Component {
const slotProps = {data};
const slotQueryData = {root, asset};
- // `key` of `StreamTabPanel` depends on sorting so that we always reset
+ // `key` of `ExtendableTabPanel` depends on sorting so that we always reset
// the state when changing sorting.
return (
@@ -157,26 +158,28 @@ class Stream extends React.Component {
{...slotProps}
/>
-
{t('stream.all_comments')} {totalCommentCount}
}
- appendTabPanes={
-
+ tabPanes={
+
-
{open
?
);
diff --git a/client/coral-embed-stream/src/components/StreamError.css b/client/coral-embed-stream/src/tabs/stream/components/StreamError.css
similarity index 100%
rename from client/coral-embed-stream/src/components/StreamError.css
rename to client/coral-embed-stream/src/tabs/stream/components/StreamError.css
diff --git a/client/coral-embed-stream/src/components/StreamError.js b/client/coral-embed-stream/src/tabs/stream/components/StreamError.js
similarity index 100%
rename from client/coral-embed-stream/src/components/StreamError.js
rename to client/coral-embed-stream/src/tabs/stream/components/StreamError.js
diff --git a/client/coral-embed-stream/src/components/SuspendAccount.css b/client/coral-embed-stream/src/tabs/stream/components/SuspendedAccount.css
similarity index 100%
rename from client/coral-embed-stream/src/components/SuspendAccount.css
rename to client/coral-embed-stream/src/tabs/stream/components/SuspendedAccount.css
diff --git a/client/coral-embed-stream/src/components/SuspendedAccount.js b/client/coral-embed-stream/src/tabs/stream/components/SuspendedAccount.js
similarity index 98%
rename from client/coral-embed-stream/src/components/SuspendedAccount.js
rename to client/coral-embed-stream/src/tabs/stream/components/SuspendedAccount.js
index ce9a1c23c..0369ef559 100644
--- a/client/coral-embed-stream/src/components/SuspendedAccount.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/SuspendedAccount.js
@@ -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';
diff --git a/client/coral-embed-stream/src/components/util.js b/client/coral-embed-stream/src/tabs/stream/components/util.js
similarity index 100%
rename from client/coral-embed-stream/src/components/util.js
rename to client/coral-embed-stream/src/tabs/stream/components/util.js
diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/tabs/stream/containers/Comment.js
similarity index 96%
rename from client/coral-embed-stream/src/containers/Comment.js
rename to client/coral-embed-stream/src/tabs/stream/containers/Comment.js
index 26fc872d5..eb27517f7 100644
--- a/client/coral-embed-stream/src/containers/Comment.js
+++ b/client/coral-embed-stream/src/tabs/stream/containers/Comment.js
@@ -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',
diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
similarity index 84%
rename from client/coral-embed-stream/src/containers/Stream.js
rename to client/coral-embed-stream/src/tabs/stream/containers/Stream.js
index 6c3d7b02c..dad69b960 100644
--- a/client/coral-embed-stream/src/containers/Stream.js
+++ b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
@@ -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 ;
}
@@ -271,6 +271,7 @@ const LOAD_MORE_QUERY = gql`
const slots = [
'streamTabs',
+ 'streamTabsPrepend',
'streamTabPanes',
'streamFilter',
];
@@ -278,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 {
@@ -333,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}
`,
};
diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css
index 0dcbf856e..63f1edf67 100644
--- a/client/coral-embed-stream/style/default.css
+++ b/client/coral-embed-stream/style/default.css
@@ -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%;
diff --git a/client/coral-framework/components/MarkdownEditor.js b/client/coral-framework/components/MarkdownEditor.js
index 65f603b0c..451a06019 100644
--- a/client/coral-framework/components/MarkdownEditor.js
+++ b/client/coral-framework/components/MarkdownEditor.js
@@ -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);
}
diff --git a/client/coral-framework/components/StreamConfiguration.css b/client/coral-framework/components/StreamConfiguration.css
new file mode 100644
index 000000000..c6863fc6a
--- /dev/null
+++ b/client/coral-framework/components/StreamConfiguration.css
@@ -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;
+}
+
+
diff --git a/client/coral-framework/components/StreamConfiguration.js b/client/coral-framework/components/StreamConfiguration.js
new file mode 100644
index 000000000..6331e8230
--- /dev/null
+++ b/client/coral-framework/components/StreamConfiguration.js
@@ -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 (
+
+ {checked !== undefined &&
+
+
+
+ }
+
+
+
{description}
+
+ {children}
+
+
+
+ );
+ }
+}
+
+Configuration.propTypes = {
+ title: PropTypes.string.isRequired,
+ description: PropTypes.string,
+ className: PropTypes.string,
+ onCheckbox: PropTypes.func,
+ checked: PropTypes.bool,
+ children: PropTypes.node,
+};
+
+export default Configuration;
diff --git a/client/coral-framework/graphql/fragments.js b/client/coral-framework/graphql/fragments.js
index c2a03131b..8b4c19e6f 100644
--- a/client/coral-framework/graphql/fragments.js
+++ b/client/coral-framework/graphql/fragments.js
@@ -17,6 +17,8 @@ export default {
'IgnoreUserResponse',
'StopIgnoringUserResponse',
'UpdateSettingsResponse',
+ 'UpdateAssetSettingsResponse',
+ 'UpdateAssetStatusResponse',
)
};
diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js
index 3a543a738..15d24a9ce 100644
--- a/client/coral-framework/graphql/mutations.js
+++ b/client/coral-framework/graphql/mutations.js
@@ -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});
+ }
+ }
+ });
+ }}),
+ });
diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js
index 67b47482e..b3c0219e3 100644
--- a/client/coral-framework/hocs/index.js
+++ b/client/coral-framework/hocs/index.js
@@ -5,3 +5,4 @@ export {default as withCopyToClipboard} from './withCopyToClipboard';
export {default as withEmit} from './withEmit';
export {default as excludeIf} from './excludeIf';
export {default as connect} from './connect';
+export {default as withMergedSettings} from './withMergedSettings';
diff --git a/client/coral-framework/hocs/withMergedSettings.js b/client/coral-framework/hocs/withMergedSettings.js
new file mode 100644
index 000000000..c90e360bc
--- /dev/null
+++ b/client/coral-framework/hocs/withMergedSettings.js
@@ -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;
diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js
index 037c1b99f..86e9e1915 100644
--- a/client/coral-framework/utils/index.js
+++ b/client/coral-framework/utils/index.js
@@ -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);
+ });
+}
diff --git a/client/coral-ui/components/Checkbox.css b/client/coral-ui/components/Checkbox.css
index d20886689..a25ea150f 100644
--- a/client/coral-ui/components/Checkbox.css
+++ b/client/coral-ui/components/Checkbox.css
@@ -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;
}
diff --git a/client/coral-ui/components/Checkbox.js b/client/coral-ui/components/Checkbox.js
index f80666c65..98804fe60 100644
--- a/client/coral-ui/components/Checkbox.js
+++ b/client/coral-ui/components/Checkbox.js
@@ -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}) => (
-