-
-
- {this.state.copied && 'Copied!'}
-
+
+ {t('configure.copy_and_paste')}
+
);
}
}
diff --git a/client/coral-admin/src/routes/Configure/components/ModerationSettings.js b/client/coral-admin/src/routes/Configure/components/ModerationSettings.js
index 92a5413e2..f5db719bb 100644
--- a/client/coral-admin/src/routes/Configure/components/ModerationSettings.js
+++ b/client/coral-admin/src/routes/Configure/components/ModerationSettings.js
@@ -1,90 +1,90 @@
import React from 'react';
import PropTypes from 'prop-types';
-import styles from './Configure.css';
-import {Card} from 'coral-ui';
-import {Checkbox} from 'react-mdl';
import Wordlist from './Wordlist';
+import Slot from 'coral-framework/components/Slot';
import t from 'coral-framework/services/i18n';
+import ConfigurePage from './ConfigurePage';
+import ConfigureCard from 'coral-framework/components/ConfigureCard';
-const updateModeration = (updateSettings, mod) => () => {
- const moderation = mod === 'PRE' ? 'POST' : 'PRE';
- updateSettings({moderation});
-};
+class ModerationSettings extends React.Component {
-const updateEmailConfirmation = (updateSettings, verify) => () => {
- updateSettings({requireEmailConfirmation: !verify});
-};
+ updateModeration = () => {
+ const updater = {moderation: {$set: this.props.settings.moderation === 'PRE' ? 'POST' : 'PRE'}};
+ this.props.updatePending({updater});
+ };
-const updatePremodLinksEnable = (updateSettings, premodLinks) => () => {
- const premodLinksEnable = !premodLinks;
- updateSettings({premodLinksEnable});
-};
+ updateEmailConfirmation = () => {
+ const updater = {requireEmailConfirmation: {$set: !this.props.settings.requireEmailConfirmation}};
+ this.props.updatePending({updater});
+ };
-const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => {
+ updatePremodLinksEnable = () => {
+ const updater = {premodLinksEnable: {$set: !this.props.settings.premodLinksEnable}};
+ this.props.updatePending({updater});
+ };
- // just putting this here for shorthand below
- const on = styles.enabledSetting;
- const off = styles.disabledSetting;
+ updateWordlist = (listName, list) => {
+ this.props.updatePending({updater: {
+ wordlist: {$apply: (wordlist) => {
+ const changeSet = {[listName]: list};
+ if (!wordlist) {
+ return changeSet;
+ }
+ return {
+ ...wordlist,
+ ...changeSet,
+ };
+ }},
+ }});
+ };
- return (
-
-
-
-
-
-
-
{t('configure.require_email_verification')}
-
- {t('configure.require_email_verification_text')}
-
-
-
-
-
-
-
-
-
{t('configure.enable_pre_moderation')}
-
- {t('configure.enable_pre_moderation_text')}
-
-
-
-
-
-
-
-
-
{t('configure.enable_premod_links')}
-
- {t('configure.enable_premod_links_text')}
-
-
-
-
-
- );
-};
+ render() {
+ const {settings, data, root} = this.props;
+
+ return (
+
+
+ {t('configure.require_email_verification_text')}
+
+
+ {t('configure.enable_pre_moderation_text')}
+
+
+ {t('configure.enable_premod_links_text')}
+
+
+
+
+ );
+ }
+}
ModerationSettings.propTypes = {
- onChangeWordlist: PropTypes.func.isRequired,
- settings: PropTypes.shape({
- moderation: PropTypes.string.isRequired,
- wordlist: PropTypes.shape({
- banned: PropTypes.array.isRequired,
- suspect: PropTypes.array.isRequired
- })
- }).isRequired,
- updateSettings: PropTypes.func.isRequired
+ updatePending: PropTypes.func.isRequired,
+ data: PropTypes.object.isRequired,
+ root: PropTypes.object.isRequired,
+ settings: PropTypes.object.isRequired,
};
export default ModerationSettings;
diff --git a/client/coral-admin/src/routes/Configure/components/StreamSettings.css b/client/coral-admin/src/routes/Configure/components/StreamSettings.css
new file mode 100644
index 000000000..5ddd46bd2
--- /dev/null
+++ b/client/coral-admin/src/routes/Configure/components/StreamSettings.css
@@ -0,0 +1,55 @@
+.configSettingInfoBox {
+ min-height: 100px;
+ margin-bottom: 20px;
+ width: auto;
+ height: auto;
+ text-align: left;
+ overflow: visible;
+}
+
+.descriptionBox {
+ margin-top: 15px;
+
+ input {
+ height: 150px;
+ }
+}
+
+.configTimeoutSelect {
+ display: inline-block;
+ margin-left: 20px;
+
+ i { /* fix for firefox and react-mdl-selectfield@0.2.0 */
+ padding: 20px 0;
+ vertical-align: top;
+ }
+}
+
+.hidden {
+ display: none;
+}
+
+.inlineTextfield {
+ border-color: #ccc;
+ border-style: solid;
+ border-width: 0px 0px 1px 0px;
+ text-align: center;
+ font-size: inherit;
+}
+
+.inlineTextfield:focus {
+ outline: none;
+}
+
+.charCountTexfield, .editCommentTimeframeTextfield {
+ width: 4em;
+ padding: 0px;
+}
+
+.charCountTexfieldEnabled {
+ border-color: #00796b;
+}
+
+
+
+
diff --git a/client/coral-admin/src/routes/Configure/components/StreamSettings.js b/client/coral-admin/src/routes/Configure/components/StreamSettings.js
index 1427f949e..9c05711cb 100644
--- a/client/coral-admin/src/routes/Configure/components/StreamSettings.js
+++ b/client/coral-admin/src/routes/Configure/components/StreamSettings.js
@@ -1,10 +1,15 @@
import React from 'react';
import {SelectField, Option} from 'react-mdl-selectfield';
import t from 'coral-framework/services/i18n';
-import styles from './Configure.css';
-import {Checkbox, Textfield} from 'react-mdl';
-import {Card, Icon, TextArea} from 'coral-ui';
+import styles from './StreamSettings.css';
+import {Textfield} from 'react-mdl';
+import {Icon, TextArea} from 'coral-ui';
+import PropTypes from 'prop-types';
+import Slot from 'coral-framework/components/Slot';
import MarkdownEditor from 'coral-framework/components/MarkdownEditor';
+import cn from 'classnames';
+import ConfigurePage from './ConfigurePage';
+import ConfigureCard from 'coral-framework/components/ConfigureCard';
const TIMESTAMPS = {
weeks: 60 * 60 * 24 * 7,
@@ -12,187 +17,6 @@ const TIMESTAMPS = {
hours: 60 * 60
};
-const updateCharCountEnable = (updateSettings, charCountChecked) => () => {
- const charCountEnable = !charCountChecked;
- updateSettings({charCountEnable});
-};
-
-const updateCharCount = (updateSettings, settingsError) => (event) => {
- const charCount = event.target.value;
- if (charCount.match(/[^0-9]/) || charCount.length === 0) {
- settingsError('charCount', true);
- } else {
- settingsError('charCount', false);
- }
- updateSettings({charCount: charCount});
-};
-
-const updateInfoBoxEnable = (updateSettings, infoBox) => () => {
- const infoBoxEnable = !infoBox;
- updateSettings({infoBoxEnable});
-};
-
-const updateInfoBoxContent = (updateSettings) => (value) => {
- const infoBoxContent = value;
- updateSettings({infoBoxContent});
-};
-
-const updateAutoClose = (updateSettings, autoCloseStream) => () => {
- updateSettings({autoCloseStream});
-};
-
-const updateClosedMessage = (updateSettings) => (event) => {
- const closedMessage = event.target.value;
- updateSettings({closedMessage});
-};
-
-// If we are changing the measure we need to recalculate using the old amount
-// Same thing if we are just changing the amount
-const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
- if (isMeasure) {
- const amount = getTimeoutAmount(ts);
- const closedTimeout = amount * TIMESTAMPS[event];
- updateSettings({closedTimeout});
- } else {
- const val = event.target.value;
- const measure = getTimeoutMeasure(ts);
- const closedTimeout = val * TIMESTAMPS[measure];
- updateSettings({closedTimeout});
- }
-};
-
-const updateEditCommentWindowLength = (updateSettings) => (e) => {
- const value = e.target.value;
- const valueAsNumber = parseFloat(value);
- const milliseconds = (!isNaN(valueAsNumber)) && (valueAsNumber * 1000);
- updateSettings({editCommentWindowLength: milliseconds || value});
-};
-
-const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
-
- // just putting this here for shorthand below
- const on = styles.enabledSetting;
- const off = styles.disabledSetting;
-
- return (
-
-
-
-
-
-
-
{t('configure.comment_count_header')}
-
- {t('configure.comment_count_text_pre')}
-
- {t('configure.comment_count_text_post')}
- {
- errors.charCount &&
-
-
-
- {t('configure.comment_count_error')}
-
- }
-
-
-
-
-
-
-
-
-
- {t('configure.include_comment_stream')}
-
-
- {t('configure.include_comment_stream_desc')}
-
-
-
-
-
-
-
-
-
{t('configure.closed_stream_settings')}
-
{t('configure.closed_comments_desc')}
-
-
-
-
-
- {/* Edit Comment Timeframe */}
-
- {t('configure.edit_comment_timeframe_heading')}
-
- {t('configure.edit_comment_timeframe_text_pre')}
-
-
-
- {t('configure.edit_comment_timeframe_text_post')}
-
-
-
-
-
-
-
-
{t('configure.close_after')}
-
-
-
-
-
-
-
-
-
-
-
- {/* the above card should be the last one if at all possible because of z-index issues with the selects */}
-
- );
-};
-
-export default StreamSettings;
-
// To see if we are talking about weeks, days or hours
// We talk the remainder of the division and see if it's 0
const getTimeoutMeasure = (ts) => {
@@ -208,3 +32,190 @@ const getTimeoutMeasure = (ts) => {
// Dividing the amount by it's measure (hours, days, weeks) we
// obtain the amount of time
const getTimeoutAmount = (ts) => ts / TIMESTAMPS[getTimeoutMeasure(ts)];
+
+class StreamSettings extends React.Component {
+
+ updateCharCountEnable = () => {
+ const updater = {charCountEnable: {$set: !this.props.settings.charCountEnable}};
+ this.props.updatePending({updater});
+ };
+
+ updateCharCount = (event) => {
+ let error = null;
+ const charCount = event.target.value;
+ if (charCount.match(/[^0-9]/) || charCount.length === 0) {
+ error = true;
+ }
+
+ const updater = {charCount: {$set: charCount}};
+ const errorUpdater = {charCount: {$set: error}};
+
+ this.props.updatePending({updater, errorUpdater});
+ };
+
+ updateInfoBoxEnable = () => {
+ const updater = {infoBoxEnable: {$set: !this.props.settings.infoBoxEnable}};
+ this.props.updatePending({updater});
+ };
+
+ updateInfoBoxContent = (value) => {
+ const updater = {infoBoxContent: {$set: value}};
+ this.props.updatePending({updater});
+ };
+
+ updateClosedMessage = (event) => {
+ const updater = {closedMessage: {$set: event.target.value}};
+ this.props.updatePending({updater});
+ };
+
+ updateEditCommentWindowLength = (e) => {
+ const value = e.target.value;
+ const valueAsNumber = parseFloat(value);
+ const milliseconds = (!isNaN(valueAsNumber)) && (valueAsNumber * 1000);
+
+ const updater = {editCommentWindowLength: {$set: milliseconds || value}};
+ this.props.updatePending({updater});
+ };
+
+ updateAutoClose = () => {
+ const updater = {autoCloseStream: {$set: !this.props.settings.autoCloseStream}};
+ this.props.updatePending({updater});
+ };
+
+ updateClosedTimeout = (event) => {
+ const val = event.target.value;
+ const measure = getTimeoutMeasure(this.props.settings.closedTimeout);
+
+ const updater = {closedTimeout: {$set: val * TIMESTAMPS[measure]}};
+ this.props.updatePending({updater});
+ };
+
+ // If we are changing the measure we need to recalculate using the old amount
+ // Same thing if we are just changing the amount
+ updateClosedTimeoutMeasure = (event) => {
+ const amount = getTimeoutAmount(this.props.settings.closedTimeout);
+
+ const updater = {closedTimeout: {$set: amount * TIMESTAMPS[event]}};
+ this.props.updatePending({updater});
+ };
+
+ render() {
+ const {settings, data, root, errors} = this.props;
+
+ return (
+
+
+ {t('configure.comment_count_text_pre')}
+
+ {t('configure.comment_count_text_post')}
+ {
+ errors.charCount &&
+
+
+
+ {t('configure.comment_count_error')}
+
+ }
+
+
+
+ {t('configure.include_comment_stream_desc')}
+
+
+
+
+
+
+ {t('configure.closed_comments_desc')}
+
+
+
+
+
+ {t('configure.edit_comment_timeframe_text_pre')}
+
+
+
+ {t('configure.edit_comment_timeframe_text_post')}
+
+
+
+
+
+
+
+
+
+
+
+ {/* the above card should be the last one if at all possible because of z-index issues with the selects */}
+
+
+ );
+ }
+}
+
+StreamSettings.propTypes = {
+ updatePending: PropTypes.func.isRequired,
+ errors: PropTypes.object.isRequired,
+ data: PropTypes.object.isRequired,
+ root: PropTypes.object.isRequired,
+ settings: PropTypes.object.isRequired,
+};
+
+export default StreamSettings;
+
diff --git a/client/coral-admin/src/routes/Configure/components/TechSettings.css b/client/coral-admin/src/routes/Configure/components/TechSettings.css
new file mode 100644
index 000000000..e3d6a071c
--- /dev/null
+++ b/client/coral-admin/src/routes/Configure/components/TechSettings.css
@@ -0,0 +1,10 @@
+.customCSSInput {
+ width: 100%;
+ font-size: 14px;
+ padding: 14px;
+ letter-spacing: 0.03em;
+ color: #555;
+ box-sizing: border-box;
+}
+
+
diff --git a/client/coral-admin/src/routes/Configure/components/TechSettings.js b/client/coral-admin/src/routes/Configure/components/TechSettings.js
index 4f1202a67..7f16b6064 100644
--- a/client/coral-admin/src/routes/Configure/components/TechSettings.js
+++ b/client/coral-admin/src/routes/Configure/components/TechSettings.js
@@ -1,44 +1,67 @@
import React from 'react';
import PropTypes from 'prop-types';
-import {Card} from 'coral-ui';
import Domainlist from './Domainlist';
import EmbedLink from './EmbedLink';
-import styles from './Configure.css';
+import styles from './TechSettings.css';
+import Slot from 'coral-framework/components/Slot';
import t from 'coral-framework/services/i18n';
+import ConfigurePage from './ConfigurePage';
+import ConfigureCard from 'coral-framework/components/ConfigureCard';
-const updateCustomCssUrl = (updateSettings) => (event) => {
- const customCssUrl = event.target.value;
- updateSettings({customCssUrl});
-};
+class TechSettings extends React.Component {
-const TechSettings = ({settings, onChangeDomainlist, updateSettings}) => {
- return (
-
-
-
-
-
-
{t('configure.custom_css_url')}
+ updateCustomCssUrl = (event) => {
+ const updater = {customCssUrl: {$set: event.target.value}};
+ this.props.updatePending({updater});
+ };
+
+ updateDomainlist = (listName, list) => {
+ this.props.updatePending({updater: {
+ domains: {$apply: (domains) => {
+ const changeSet = {[listName]: list};
+ if (!domains) {
+ return changeSet;
+ }
+ return {
+ ...domains,
+ ...changeSet,
+ };
+ }},
+ }});
+ };
+
+ render() {
+ const {settings, data, root} = this.props;
+ return (
+
+
+
+
{t('configure.custom_css_url_desc')}
-
-
-
- );
-};
+ onChange={this.updateCustomCssUrl} />
+
+
+
+ );
+ }
+}
TechSettings.propTypes = {
- settings: PropTypes.shape({
- domains: PropTypes.shape({
- whitelist: PropTypes.array.isRequired
- })
- }).isRequired,
- updateSettings: PropTypes.func.isRequired
+ updatePending: PropTypes.func.isRequired,
+ data: PropTypes.object.isRequired,
+ root: PropTypes.object.isRequired,
+ settings: PropTypes.object.isRequired,
};
export default TechSettings;
diff --git a/client/coral-admin/src/routes/Configure/components/Wordlist.js b/client/coral-admin/src/routes/Configure/components/Wordlist.js
index e96f37f9f..34a6ed825 100644
--- a/client/coral-admin/src/routes/Configure/components/Wordlist.js
+++ b/client/coral-admin/src/routes/Configure/components/Wordlist.js
@@ -1,33 +1,33 @@
import React from 'react';
import t from 'coral-framework/services/i18n';
import TagsInput from 'coral-admin/src/components/TagsInput';
-import styles from './Configure.css';
-import {Card} from 'coral-ui';
+import PropTypes from 'prop-types';
+import ConfigureCard from 'coral-framework/components/ConfigureCard';
const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
-
- {t('configure.banned_words_title')}
- {t('configure.banned_word_text')}
-
- onChangeWordlist('banned', tags)}
- />
-
-
-
- {t('configure.suspect_word_title')}
- {t('configure.suspect_word_text')}
-
- onChangeWordlist('suspect', tags)} />
-
-
+
+ {t('configure.banned_word_text')}
+ onChangeWordlist('banned', tags)}
+ />
+
+
+ {t('configure.suspect_word_text')}
+ onChangeWordlist('suspect', tags)} />
+
);
+Wordlist.propTypes = {
+ suspectWords: PropTypes.array.isRequired,
+ bannedWords: PropTypes.array.isRequired,
+ onChangeWordlist: PropTypes.func.isRequired,
+};
+
export default Wordlist;
diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js
index aff1e57b2..8f80a3e24 100644
--- a/client/coral-admin/src/routes/Configure/containers/Configure.js
+++ b/client/coral-admin/src/routes/Configure/containers/Configure.js
@@ -1,42 +1,128 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
-import {compose} from 'react-apollo';
-import {
- fetchSettings,
- updateSettings,
- saveSettingsToServer,
- updateWordlist,
- updateDomainlist
-} from '../../../actions/settings';
+import {compose, gql} from 'react-apollo';
+import withQuery from 'coral-framework/hocs/withQuery';
+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 {
- componentWillMount = () => {
- this.props.fetchSettings();
+
+ // 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);
+ this.props.clearPending();
+ }
+ catch(err) {
+ this.props.notify('error', getErrorMessages(err));
+ }
+ };
+
+ 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 () {
- return
;
+ if(this.props.data.loading) {
+ return
;
+ }
+
+ return
;
}
}
+const withConfigureQuery = withQuery(gql`
+ query TalkAdmin_Configure {
+ settings {
+ ...${getDefinitionName(StreamSettings.fragments.settings)}
+ ...${getDefinitionName(TechSettings.fragments.settings)}
+ ...${getDefinitionName(ModerationSettings.fragments.settings)}
+ }
+ ...${getDefinitionName(StreamSettings.fragments.root)}
+ ...${getDefinitionName(TechSettings.fragments.root)}
+ ...${getDefinitionName(ModerationSettings.fragments.root)}
+ }
+ ${StreamSettings.fragments.root}
+ ${StreamSettings.fragments.settings}
+ ${TechSettings.fragments.root}
+ ${TechSettings.fragments.settings}
+ ${ModerationSettings.fragments.root}
+ ${ModerationSettings.fragments.settings}
+ `, {
+ options: () => ({
+ variables: {},
+ }),
+});
+
const mapStateToProps = (state) => ({
auth: state.auth,
- settings: state.settings
+ pending: state.configure.pending,
+ canSave: state.configure.canSave,
+ activeSection: state.configure.activeSection,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
- fetchSettings,
- updateSettings,
- saveSettingsToServer,
- updateWordlist,
- updateDomainlist
+ notify,
+ clearPending,
+ setActiveSection,
}, dispatch);
export default compose(
+ withUpdateSettings,
+ withConfigureQuery,
connect(mapStateToProps, mapDispatchToProps),
)(ConfigureContainer);
+ConfigureContainer.propTypes = {
+ updateSettings: PropTypes.func.isRequired,
+ clearPending: PropTypes.func.isRequired,
+ setActiveSection: PropTypes.func.isRequired,
+ notify: PropTypes.func.isRequired,
+ auth: PropTypes.object.isRequired,
+ data: PropTypes.object.isRequired,
+ root: PropTypes.object.isRequired,
+ canSave: PropTypes.bool.isRequired,
+ pending: 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
new file mode 100644
index 000000000..0583378a9
--- /dev/null
+++ b/client/coral-admin/src/routes/Configure/containers/ModerationSettings.js
@@ -0,0 +1,40 @@
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {compose, gql} from 'react-apollo';
+import ModerationSettings from '../components/ModerationSettings';
+import withFragments from 'coral-framework/hocs/withFragments';
+import {getSlotFragmentSpreads} from 'coral-framework/utils';
+import {updatePending} from '../../../actions/configure';
+
+const slots = [
+ 'adminModerationSettings',
+];
+
+const mapDispatchToProps = (dispatch) =>
+ bindActionCreators({
+ updatePending,
+ }, dispatch);
+
+export default compose(
+ withFragments({
+ root: gql`
+ fragment TalkAdmin_ModerationSettings_root on RootQuery {
+ __typename
+ ${getSlotFragmentSpreads(slots, 'root')}
+ }
+ `,
+ settings: gql`
+ fragment TalkAdmin_ModerationSettings_settings on Settings {
+ requireEmailConfirmation
+ moderation
+ premodLinksEnable
+ wordlist {
+ suspect
+ banned
+ }
+ ${getSlotFragmentSpreads(slots, 'settings')}
+ }
+ `
+ }),
+ connect(null, mapDispatchToProps),
+)(ModerationSettings);
diff --git a/client/coral-admin/src/routes/Configure/containers/StreamSettings.js b/client/coral-admin/src/routes/Configure/containers/StreamSettings.js
new file mode 100644
index 000000000..9a7f79fa6
--- /dev/null
+++ b/client/coral-admin/src/routes/Configure/containers/StreamSettings.js
@@ -0,0 +1,45 @@
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {compose, gql} from 'react-apollo';
+import StreamSettings from '../components/StreamSettings';
+import withFragments from 'coral-framework/hocs/withFragments';
+import {getSlotFragmentSpreads} from 'coral-framework/utils';
+import {updatePending} from '../../../actions/configure';
+
+const slots = [
+ 'adminStreamSettings',
+];
+
+const mapStateToProps = (state) => ({
+ errors: state.configure.errors,
+});
+
+const mapDispatchToProps = (dispatch) =>
+ bindActionCreators({
+ updatePending,
+ }, dispatch);
+
+export default compose(
+ withFragments({
+ root: gql`
+ fragment TalkAdmin_StreamSettings_root on RootQuery {
+ __typename
+ ${getSlotFragmentSpreads(slots, 'root')}
+ }
+ `,
+ settings: gql`
+ fragment TalkAdmin_StreamSettings_settings on Settings {
+ infoBoxEnable
+ charCount
+ charCountEnable
+ infoBoxContent
+ editCommentWindowLength
+ autoCloseStream
+ closedTimeout
+ closedMessage
+ ${getSlotFragmentSpreads(slots, 'settings')}
+ }
+ `
+ }),
+ connect(mapStateToProps, mapDispatchToProps),
+)(StreamSettings);
diff --git a/client/coral-admin/src/routes/Configure/containers/TechSettings.js b/client/coral-admin/src/routes/Configure/containers/TechSettings.js
new file mode 100644
index 000000000..df33a3f9a
--- /dev/null
+++ b/client/coral-admin/src/routes/Configure/containers/TechSettings.js
@@ -0,0 +1,37 @@
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {compose, gql} from 'react-apollo';
+import TechSettings from '../components/TechSettings';
+import withFragments from 'coral-framework/hocs/withFragments';
+import {getSlotFragmentSpreads} from 'coral-framework/utils';
+import {updatePending} from '../../../actions/configure';
+
+const slots = [
+ 'adminTechSettings',
+];
+
+const mapDispatchToProps = (dispatch) =>
+ bindActionCreators({
+ updatePending,
+ }, dispatch);
+
+export default compose(
+ withFragments({
+ root: gql`
+ fragment TalkAdmin_TechSettings_root on RootQuery {
+ __typename
+ ${getSlotFragmentSpreads(slots, 'root')}
+ }
+ `,
+ settings: gql`
+ fragment TalkAdmin_TechSettings_settings on Settings {
+ customCssUrl
+ domains {
+ whitelist
+ }
+ ${getSlotFragmentSpreads(slots, 'settings')}
+ }
+ `
+ }),
+ connect(null, mapDispatchToProps),
+)(TechSettings);
diff --git a/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js b/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js
index 2bb192f2e..e2e9ef28c 100644
--- a/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js
+++ b/client/coral-admin/src/routes/Dashboard/containers/Dashboard.js
@@ -42,11 +42,11 @@ export const witDashboardQuery = withQuery(gql`
}
}
`, {
- options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
+ options: ({windowStart, windowEnd}) => {
return {
variables: {
- from: dashboardWindowStart,
- to: dashboardWindowEnd
+ from: windowStart,
+ to: windowEnd,
}
};
}
@@ -54,8 +54,8 @@ export const witDashboardQuery = withQuery(gql`
const mapStateToProps = (state) => {
return {
- settings: state.settings,
- moderation: state.moderation
+ windowStart: state.dashboard.windowStart,
+ windowEnd: state.dashboard.windowEnd,
};
};
diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js
index 42c9c2d13..aedc69739 100644
--- a/client/coral-admin/src/routes/Moderation/components/Comment.js
+++ b/client/coral-admin/src/routes/Moderation/components/Comment.js
@@ -58,12 +58,11 @@ class Comment extends React.Component {
render() {
const {
comment,
- suspectWords,
- bannedWords,
selected,
className,
data,
root,
+ root: {settings},
currentUserId,
currentAsset,
} = this.props;
@@ -130,8 +129,8 @@ class Comment extends React.Component {
{' '}
@@ -188,8 +187,6 @@ Comment.propTypes = {
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
className: PropTypes.string,
- suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
- bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
currentAsset: PropTypes.object,
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js
index 9b9673ddb..64ccb1857 100644
--- a/client/coral-admin/src/routes/Moderation/components/Moderation.js
+++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js
@@ -77,7 +77,7 @@ class Moderation extends Component {
const comments = this.getComments();
const commentIdx = comments.findIndex((comment) => comment.id === selectedCommentId);
const comment = comments[commentIdx];
-
+
if (accept) {
comment.status !== 'ACCEPTED' && acceptComment({commentId: comment.id});
} else {
@@ -220,7 +220,7 @@ class Moderation extends Component {
}
render () {
- const {root, data, moderation, settings, viewUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props;
+ const {root, data, moderation, viewUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props;
const {asset} = root;
const assetId = asset && asset.id;
@@ -262,15 +262,11 @@ class Moderation extends Component {
activeTab={activeTab}
singleView={moderation.singleView}
selectedCommentId={this.state.selectedCommentId}
- bannedWords={settings.wordlist.banned}
- suspectWords={settings.wordlist.suspect}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
loadMore={this.loadMore}
- assetId={assetId}
- sort={this.props.moderation.sortOrder}
commentCount={activeTabCount}
currentUserId={this.props.auth.user.id}
viewUserDetail={viewUserDetail}
@@ -308,7 +304,6 @@ Moderation.propTypes = {
storySearchChange: PropTypes.func.isRequired,
moderation: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
- settings: PropTypes.object.isRequired,
queueConfig: PropTypes.object.isRequired,
handleCommentChange: PropTypes.func.isRequired,
setSortOrder: PropTypes.func.isRequired,
@@ -321,6 +316,7 @@ Moderation.propTypes = {
activeTab: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
+ router: PropTypes.object.isRequired,
};
export default Moderation;
diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js
index 47ed56925..dc4a8c74c 100644
--- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js
+++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js
@@ -147,8 +147,6 @@ class ModerationQueue extends React.Component {
key={comment.id}
comment={comment}
selected={true}
- suspectWords={props.suspectWords}
- bannedWords={props.bannedWords}
viewUserDetail={viewUserDetail}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
@@ -193,8 +191,6 @@ class ModerationQueue extends React.Component {
key={comment.id}
comment={comment}
selected={comment.id === selectedCommentId}
- suspectWords={props.suspectWords}
- bannedWords={props.bannedWords}
viewUserDetail={viewUserDetail}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
@@ -218,8 +214,6 @@ class ModerationQueue extends React.Component {
ModerationQueue.propTypes = {
viewUserDetail: PropTypes.func.isRequired,
- bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
- suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
currentAsset: PropTypes.object,
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
diff --git a/client/coral-admin/src/routes/Moderation/containers/Comment.js b/client/coral-admin/src/routes/Moderation/containers/Comment.js
index b060b9f9e..9748359ae 100644
--- a/client/coral-admin/src/routes/Moderation/containers/Comment.js
+++ b/client/coral-admin/src/routes/Moderation/containers/Comment.js
@@ -15,7 +15,12 @@ const slots = [
export default withFragments({
root: gql`
fragment CoralAdmin_ModerationComment_root on RootQuery {
- __typename
+ settings {
+ wordlist {
+ banned
+ suspect
+ }
+ }
${getSlotFragmentSpreads(slots, 'root')}
...${getDefinitionName(CommentLabels.fragments.root)}
...${getDefinitionName(CommentDetails.fragments.root)}
diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js
index 7aa839292..8787d77cd 100644
--- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js
+++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js
@@ -13,7 +13,6 @@ import {isPremod, getModPath} from '../../../utils';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {handleCommentChange} from '../graphql';
-import {fetchSettings} from 'actions/settings';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {viewUserDetail} from '../../../actions/userDetail';
@@ -146,7 +145,6 @@ class ModerationContainer extends Component {
componentWillMount() {
this.props.clearState();
- this.props.fetchSettings();
this.subscribeToUpdates();
}
@@ -384,7 +382,6 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
const mapStateToProps = (state) => ({
moderation: state.moderation,
- settings: state.settings,
auth: state.auth,
});
@@ -392,7 +389,6 @@ const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
toggleModal,
singleView,
- fetchSettings,
showBanUserDialog,
hideShortcutsNote,
toggleStorySearch,
diff --git a/client/coral-framework/components/ConfigureCard.css b/client/coral-framework/components/ConfigureCard.css
new file mode 100644
index 000000000..0aafcf7f4
--- /dev/null
+++ b/client/coral-framework/components/ConfigureCard.css
@@ -0,0 +1,49 @@
+.card {
+ margin-bottom: 20px;
+ align-items: flex-start;
+ min-height: 100px;
+ max-width: 600px;
+}
+
+.header {
+ margin-top: 3px;
+ margin-bottom: 7px;
+ font-size: 18px;
+ font-weight: 500;
+}
+
+.wrapper {
+ width: 100%;
+ font-size: 14px;
+ letter-spacing: 0;
+}
+
+.action {
+ display: inline-block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ padding: 20px;
+}
+
+.content {
+ display: inline-block;
+ padding: 0px 30px;
+ box-sizing: border-box;
+}
+
+.enabledSetting {
+ border-left-color: #00796b;
+ border-left-style: solid;
+ border-left-width: 7px;
+}
+
+.disabledSetting {
+ padding-left: 22px;
+}
+
+.disabledSettingText {
+ color: #ccc;
+ pointer-events: none;
+}
+
diff --git a/client/coral-framework/components/ConfigureCard.js b/client/coral-framework/components/ConfigureCard.js
new file mode 100644
index 000000000..b7198a480
--- /dev/null
+++ b/client/coral-framework/components/ConfigureCard.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styles from './ConfigureCard.css';
+import {Card} from 'coral-ui';
+import {Checkbox} from 'react-mdl';
+import cn from 'classnames';
+
+const ConfigureCard = ({title, children, className, onCheckbox, checked, ...rest}) => (
+
+ {checked !== undefined &&
+
+
+
+ }
+
+
{title}
+
+ {children}
+
+
+
+);
+
+ConfigureCard.propTypes = {
+ title: PropTypes.string.isRequired,
+ className: PropTypes.string,
+ onCheckbox: PropTypes.func,
+ checked: PropTypes.bool,
+ children: PropTypes.node,
+};
+
+export default ConfigureCard;
diff --git a/client/coral-framework/graphql/fragments.js b/client/coral-framework/graphql/fragments.js
index 5b3fe5498..c2a03131b 100644
--- a/client/coral-framework/graphql/fragments.js
+++ b/client/coral-framework/graphql/fragments.js
@@ -16,6 +16,7 @@ export default {
'ModifyTagResponse',
'IgnoreUserResponse',
'StopIgnoringUserResponse',
+ 'UpdateSettingsResponse',
)
};
diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js
index fb004956b..447d297ab 100644
--- a/client/coral-framework/graphql/mutations.js
+++ b/client/coral-framework/graphql/mutations.js
@@ -340,3 +340,21 @@ export const withStopIgnoringUser = withMutation(
});
}}),
});
+
+export const withUpdateSettings = withMutation(
+ gql`
+ mutation UpdateSettings($input: UpdateSettingsInput!) {
+ updateSettings(input: $input) {
+ ...UpdateSettingsResponse
+ }
+ }
+ `, {
+ props: ({mutate}) => ({
+ updateSettings: (input) => {
+ return mutate({
+ variables: {
+ input,
+ },
+ });
+ }}),
+ });
diff --git a/graph/mutators/settings.js b/graph/mutators/settings.js
index f725d791d..59613ae6f 100644
--- a/graph/mutators/settings.js
+++ b/graph/mutators/settings.js
@@ -2,20 +2,16 @@ const errors = require('../../errors');
const {
UPDATE_SETTINGS,
- UPDATE_WORDLIST,
} = require('../../perms/constants');
const SettingsService = require('../../services/settings');
const update = async (ctx, settings) => SettingsService.update(settings);
-const updateWordlist = async (ctx, wordlist) => SettingsService.updateWordlist(wordlist);
-
module.exports = (ctx) => {
let mutators = {
Settings: {
update: () => Promise.reject(errors.ErrNotAuthorized),
- updateWordlist: () => Promise.reject(errors.ErrNotAuthorized)
}
};
@@ -23,10 +19,6 @@ module.exports = (ctx) => {
if (ctx.user.can(UPDATE_SETTINGS)) {
mutators.Settings.update = (id, settings) => update(ctx, id, settings);
}
-
- if (ctx.user.can(UPDATE_WORDLIST)) {
- mutators.Settings.updateWordlist = (id, status) => updateWordlist(ctx, id, status);
- }
}
return mutators;
diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js
index 17235a59e..c3bf6b7e9 100644
--- a/graph/resolvers/root_mutation.js
+++ b/graph/resolvers/root_mutation.js
@@ -61,9 +61,6 @@ const RootMutation = {
updateSettings: async (_, {input: settings}, {mutators: {Settings}}) => {
await Settings.update(settings);
},
- updateWordlist: async (_, {input: wordlist}, {mutators: {Settings}}) => {
- await Settings.updateWordlist(wordlist);
- },
createToken: async (_, {input}, {mutators: {Token}}) => ({
token: await Token.create(input),
}),
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index ca2a39871..3fbc1cec3 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -1217,6 +1217,12 @@ input UpdateSettingsInput {
# editCommentWindowLength is the length of time (in milliseconds) after a
# comment is posted that it can still be edited by the author.
editCommentWindowLength: Int
+
+ # wordlist allows chaninging the available wordlists.
+ wordlist: UpdateWordlistInput
+
+ # domains allows changing the available lists of domains.
+ domains: UpdateDomainsInput
}
# UpdateSettingsResponse contains any errors that were rendered as a result
@@ -1231,18 +1237,17 @@ type UpdateSettingsResponse implements Response {
input UpdateWordlistInput {
# banned words will by default reject the comment if it is found.
- banned: [String!]!
+ banned: [String!]
# suspect words will simply flag the comment.
- suspect: [String!]!
+ suspect: [String!]
}
-# UpdateWordlistResponse contains any errors that were rendered as a result
-# of the mutation.
-type UpdateWordlistResponse implements Response {
+# UpdateDomainsInput describes all the available lists of domains.
+input UpdateDomainsInput {
- # An array of errors relating to the mutation that occurred.
- errors: [UserError!]
+ # whitelist is the list of domains that the embed is allowed to render on.
+ whitelist: [String!]
}
# CreateTokenInput contains the input to create the token.
@@ -1329,10 +1334,6 @@ type RootMutation {
# Mutation is restricted.
updateSettings(input: UpdateSettingsInput!): UpdateSettingsResponse
- # updateWordlist will update the given Wordlist.
- # Mutation is restricted.
- updateWordlist(input: UpdateWordlistInput!): UpdateWordlistResponse
-
# Ignore comments by another user
ignoreUser(id: ID!): IgnoreUserResponse
diff --git a/perms/constants.js b/perms/constants.js
index 070fe80bf..4c85f9c26 100644
--- a/perms/constants.js
+++ b/perms/constants.js
@@ -19,7 +19,6 @@ module.exports = {
UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS',
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
- UPDATE_WORDLIST: 'UPDATE_WORDLIST',
// queries
SEARCH_ASSETS: 'SEARCH_ASSETS',
diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js
index 2e8718ed1..c44035372 100644
--- a/perms/mutationReducer.js
+++ b/perms/mutationReducer.js
@@ -19,7 +19,6 @@ module.exports = (user, perm) => {
case types.SET_COMMENT_STATUS:
case types.UPDATE_CONFIG:
case types.UPDATE_SETTINGS:
- case types.UPDATE_WORDLIST:
case types.UPDATE_ASSET_SETTINGS:
case types.UPDATE_ASSET_STATUS:
return check(user, ['ADMIN', 'MODERATOR']);
diff --git a/plugin-api/beta/client/components/index.js b/plugin-api/beta/client/components/index.js
index 17e8e7a13..21ae60a0d 100644
--- a/plugin-api/beta/client/components/index.js
+++ b/plugin-api/beta/client/components/index.js
@@ -6,3 +6,4 @@ export {default as CommentAuthorName} from 'coral-framework/components/CommentAu
export {default as CommentTimestamp} from 'coral-framework/components/CommentTimestamp';
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';
diff --git a/services/settings.js b/services/settings.js
index 3033701d8..b11b86d91 100644
--- a/services/settings.js
+++ b/services/settings.js
@@ -1,6 +1,32 @@
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;
+}
+
/**
* The selector used to uniquely identify the settings document.
*/
@@ -34,7 +60,7 @@ module.exports = class SettingsService {
*/
static update(settings) {
return SettingModel.findOneAndUpdate(selector, {
- $set: settings
+ $set: dotize(settings)
}, {
upsert: true,
new: true,
@@ -42,19 +68,6 @@ module.exports = class SettingsService {
});
}
- /**
- * updateWordlist will update the wordlists.
- *
- * @param {Object} wordlist the Wordlist object
- */
- static updateWordlist(wordlist) {
- return SettingModel.findOneAndUpdate(selector, {
- $set: {
- wordlist,
- },
- });
- }
-
/**
* This is run once when the app starts to ensure settings are populated.
*/
diff --git a/test/server/graph/mutations/updateSettings.js b/test/server/graph/mutations/updateSettings.js
index c1ec678d6..553a2be65 100644
--- a/test/server/graph/mutations/updateSettings.js
+++ b/test/server/graph/mutations/updateSettings.js
@@ -4,10 +4,12 @@ const schema = require('../../../../graph/schema');
const Context = require('../../../../graph/context');
const UserModel = require('../../../../models/user');
const SettingsService = require('../../../../services/settings');
+const isEqual = require('lodash/isEqual');
const {expect} = require('chai');
describe('graph.mutations.updateSettings', () => {
+
beforeEach(async () => {
await SettingsService.init();
});
@@ -71,4 +73,86 @@ describe('graph.mutations.updateSettings', () => {
});
});
});
+
+ describe('nested objects', () => {
+ const user = new UserModel({roles: ['ADMIN']});
+ const ctx = new Context({user});
+
+ it('should handle nested objects', async () => {
+ const initSettings = {
+ wordlist: {
+ banned: ['fuck'],
+ suspect: ['idiot', 'nazis'],
+ },
+ domains: {
+ whitelist: ['localhost:3000'],
+ },
+ };
+
+ let res = await graphql(schema, QUERY, {}, ctx, {
+ settings: initSettings,
+ });
+ if (res.errors) {
+ console.error(res.errors);
+ }
+
+ if (res.data.updateSettings && res.data.updateSettings.errors) {
+ console.error(res.data.updateSettings.errors);
+ }
+
+ expect(res.errors).to.be.empty;
+ expect(res.data.updateSettings).to.be.null;
+
+ let retrievedSettings = await SettingsService.retrieve();
+ Object.keys(initSettings).forEach((key) => {
+ Object.keys(initSettings[key]).forEach((nestedKey) => {
+ expect(retrievedSettings).to.have.property(key);
+ expect(retrievedSettings[key]).to.have.property(nestedKey);
+ expect(isEqual(retrievedSettings[key][nestedKey], initSettings[key][nestedKey])).to.be.true;
+ });
+ });
+
+ const change = {
+ wordlist: {
+ suspect: ['idiot'],
+ },
+ domains: {
+ whitelist: ['coralproject.org'],
+ },
+ };
+
+ const changedSettings = Object.assign({}, initSettings, {
+ wordlist: {
+ suspect: change.wordlist.suspect,
+ },
+ domains: {
+ whitelist: change.domains.whitelist,
+ }
+ });
+
+ res = await graphql(schema, QUERY, {}, ctx, {
+ settings: change,
+ });
+
+ if (res.errors) {
+ console.error(res.errors);
+ }
+
+ if (res.data.updateSettings && res.data.updateSettings.errors) {
+ console.error(res.data.updateSettings.errors);
+ }
+
+ expect(res.errors).to.be.empty;
+ expect(res.data.updateSettings).to.be.null;
+
+ retrievedSettings = await SettingsService.retrieve();
+ Object.keys(changedSettings).forEach((key) => {
+ Object.keys(changedSettings[key]).forEach((nestedKey) => {
+ expect(retrievedSettings).to.have.property(key);
+ expect(retrievedSettings[key]).to.have.property(nestedKey);
+ expect(isEqual(retrievedSettings[key][nestedKey], changedSettings[key][nestedKey])).to.be.true;
+ });
+ });
+ });
+ });
});
diff --git a/test/server/graph/mutations/updateWordlist.js b/test/server/graph/mutations/updateWordlist.js
deleted file mode 100644
index 3a392a074..000000000
--- a/test/server/graph/mutations/updateWordlist.js
+++ /dev/null
@@ -1,82 +0,0 @@
-const {graphql} = require('graphql');
-
-const schema = require('../../../../graph/schema');
-const Context = require('../../../../graph/context');
-const UserModel = require('../../../../models/user');
-const SettingsService = require('../../../../services/settings');
-
-const {expect} = require('chai');
-
-describe('graph.mutations.updateWordlist', () => {
- beforeEach(async () => {
- await SettingsService.init();
- });
-
- const QUERY = `
- mutation UpdateWordlist($wordlist: UpdateWordlistInput!) {
- updateWordlist(input: $wordlist) {
- errors {
- translation_key
- }
- }
- }
-`;
-
- describe('context with different user roles', () => {
-
- [
- {error: 'NOT_AUTHORIZED'},
- {error: 'NOT_AUTHORIZED', roles: []},
- {roles: ['ADMIN']},
- {roles: ['ADMIN', 'MODERATOR']},
- {roles: ['MODERATOR']},
- ].forEach(({roles, error}) => {
- it(roles && roles.length > 0 ? roles.join(', ') : '
', async () => {
- let user;
- if (roles != null) {
- user = new UserModel({roles});
- }
- const ctx = new Context({user});
-
- const wordlist = {
- banned: [
- 'happy',
- ],
- suspect: [
- 'sad',
- ],
- };
-
- const res = await graphql(schema, QUERY, {}, ctx, {
- wordlist,
- });
- if (res.errors) {
- console.error(res.errors);
- }
- expect(res.errors).to.be.empty;
-
- if (error) {
- expect(res.data.updateWordlist.errors).to.not.be.empty;
- expect(res.data.updateWordlist.errors[0]).to.have.property('translation_key', error);
-
- const {wordlist: retrievedWordlist} = await SettingsService.retrieve();
- expect(retrievedWordlist).to.have.property('banned');
- expect(retrievedWordlist.banned).to.have.members([]);
- expect(retrievedWordlist).to.have.property('suspect');
- expect(retrievedWordlist.suspect).to.have.members([]);
- } else {
- if (res.data.updateWordlist && res.data.updateWordlist.errors) {
- console.error(res.data.updateWordlist.errors);
- }
- expect(res.data.updateWordlist).to.be.null;
-
- const {wordlist: retrievedWordlist} = await SettingsService.retrieve();
- expect(retrievedWordlist).to.have.property('banned');
- expect(retrievedWordlist.banned).to.have.members(wordlist.banned);
- expect(retrievedWordlist).to.have.property('suspect');
- expect(retrievedWordlist.suspect).to.have.members(wordlist.suspect);
- }
- });
- });
- });
-});