mirror of
https://github.com/wassname/talk.git
synced 2026-07-06 05:17:19 +08:00
Merge pull request #1083 from coralproject/refactor-configure
Refactor Configure, Add Slots
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
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};
|
||||
};
|
||||
|
||||
export const setActiveSection = (section) => {
|
||||
return {type: actions.SET_ACTIVE_SECTION, section};
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
|
||||
export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED';
|
||||
export const SETTINGS_FETCH_ERROR = 'SETTINGS_FETCH_ERROR';
|
||||
|
||||
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
|
||||
|
||||
export const SAVE_SETTINGS_LOADING = 'SAVE_SETTINGS_LOADING';
|
||||
export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS';
|
||||
export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
|
||||
|
||||
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
|
||||
export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED';
|
||||
|
||||
export const fetchSettings = () => (dispatch, _, {rest}) => {
|
||||
dispatch({type: SETTINGS_LOADING});
|
||||
rest('/settings')
|
||||
.then((settings) => {
|
||||
dispatch({type: SETTINGS_RECEIVED, settings});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: SETTINGS_FETCH_ERROR, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
// for updating top-level settings
|
||||
export const updateSettings = (settings) => {
|
||||
return {type: SETTINGS_UPDATED, settings};
|
||||
};
|
||||
|
||||
// this is a nested property, so it needs a special action.
|
||||
export const updateWordlist = (listName, list) => {
|
||||
return {type: WORDLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const updateDomainlist = (listName, list) => {
|
||||
return {type: DOMAINLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const saveSettingsToServer = () => (dispatch, getState, {rest}) => {
|
||||
let settings = getState().settings;
|
||||
if (settings.charCount) {
|
||||
settings.charCount = parseInt(settings.charCount);
|
||||
}
|
||||
dispatch({type: SAVE_SETTINGS_LOADING});
|
||||
rest('/settings', {method: 'PUT', body: settings})
|
||||
.then(() => {
|
||||
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: SAVE_SETTINGS_FAILED, error: errorMessage});
|
||||
});
|
||||
};
|
||||
@@ -17,8 +17,6 @@ export default class UserDetail extends React.Component {
|
||||
userId: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
bannedWords: PropTypes.array.isRequired,
|
||||
suspectWords: PropTypes.array.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
changeStatus: PropTypes.func.isRequired,
|
||||
@@ -79,8 +77,6 @@ export default class UserDetail extends React.Component {
|
||||
},
|
||||
activeTab,
|
||||
selectedCommentIds,
|
||||
bannedWords,
|
||||
suspectWords,
|
||||
toggleSelect,
|
||||
bulkAccept,
|
||||
bulkReject,
|
||||
@@ -184,8 +180,6 @@ export default class UserDetail extends React.Component {
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selected={selected}
|
||||
|
||||
@@ -30,12 +30,11 @@ class UserDetailComment extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
comment,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
selected,
|
||||
toggleSelect,
|
||||
className,
|
||||
data,
|
||||
root: {settings: {wordlist: {banned, suspect}}},
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -72,8 +71,8 @@ class UserDetailComment extends React.Component {
|
||||
<div className={styles.bodyContainer}>
|
||||
<div className={styles.body}>
|
||||
<CommentBodyHighlighter
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
suspectWords={suspect}
|
||||
bannedWords={banned}
|
||||
body={comment.body}
|
||||
/>
|
||||
{' '}
|
||||
@@ -123,9 +122,15 @@ UserDetailComment.propTypes = {
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
toggleSelect: PropTypes.func,
|
||||
root: PropTypes.shape({
|
||||
settings: PropTypes.shape({
|
||||
wordlist: PropTypes.shape({
|
||||
suspect: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
banned: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
@@ -136,8 +141,8 @@ UserDetailComment.propTypes = {
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
})
|
||||
})
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
export default UserDetailComment;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const prefix = 'TALK_ADMIN_CONFIGURE';
|
||||
|
||||
export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`;
|
||||
export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`;
|
||||
export const SET_ACTIVE_SECTION = `${prefix}_SET_ACTIVE_SECTION`;
|
||||
@@ -179,8 +179,6 @@ const mapStateToProps = (state) => ({
|
||||
selectedCommentIds: state.userDetail.selectedCommentIds,
|
||||
statuses: state.userDetail.statuses,
|
||||
activeTab: state.userDetail.activeTab,
|
||||
bannedWords: state.settings.wordlist.banned,
|
||||
suspectWords: state.settings.wordlist.suspect,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
@@ -8,7 +8,12 @@ import CommentDetails from './CommentDetails';
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
fragment CoralAdmin_UserDetailComment_root on RootQuery {
|
||||
__typename
|
||||
settings {
|
||||
wordlist {
|
||||
banned
|
||||
suspect
|
||||
}
|
||||
}
|
||||
...${getDefinitionName(CommentLabels.fragments.root)}
|
||||
...${getDefinitionName(CommentDetails.fragments.root)}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
@@ -29,6 +40,16 @@ export default {
|
||||
}
|
||||
}
|
||||
}),
|
||||
UpdateSettings: ({variables: {input}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_Configure: (prev) => {
|
||||
const updated = update(prev, {
|
||||
settings: mapLeaves(input, (leaf) => ({$set: leaf})),
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as actions from '../constants/configure';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
const initialState = {
|
||||
canSave: false,
|
||||
pending: {},
|
||||
errors: {},
|
||||
activeSection: 'stream',
|
||||
};
|
||||
|
||||
export default function configure(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,
|
||||
};
|
||||
case actions.SET_ACTIVE_SECTION:
|
||||
return {
|
||||
...state,
|
||||
activeSection: action.section,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// this is initialized here because
|
||||
// currently you have to reload the dashboard to get new stats
|
||||
// cleaner updates are planned in the future.
|
||||
const DASHBOARD_WINDOW_MINUTES = 5;
|
||||
let then = new Date();
|
||||
then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES);
|
||||
|
||||
const initialState = {
|
||||
windowStart: then.toISOString(),
|
||||
windowEnd: new Date().toISOString(),
|
||||
};
|
||||
|
||||
export default function dashboard (state = initialState, _action) {
|
||||
return state;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import auth from './auth';
|
||||
import assets from './assets';
|
||||
import settings from './settings';
|
||||
import dashboard from './dashboard';
|
||||
import configure from './configure';
|
||||
import community from './community';
|
||||
import moderation from './moderation';
|
||||
import install from './install';
|
||||
@@ -12,10 +13,11 @@ import userDetail from './userDetail';
|
||||
export default {
|
||||
auth,
|
||||
banUserDialog,
|
||||
dashboard,
|
||||
configure,
|
||||
suspendUserDialog,
|
||||
userDetail,
|
||||
assets,
|
||||
settings,
|
||||
community,
|
||||
moderation,
|
||||
install,
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import * as actions from '../actions/settings';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
// this is initialized here because
|
||||
// currently you have to reload the dashboard to get new stats
|
||||
// cleaner updates are planned in the future.
|
||||
// TODO: if there are more than two fields for the dashboard being created here,
|
||||
// please create a new reducer specifically for the Dashboard.
|
||||
const DASHBOARD_WINDOW_MINUTES = 5;
|
||||
let then = new Date();
|
||||
then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES);
|
||||
|
||||
const initialState = {
|
||||
wordlist: {
|
||||
banned: [],
|
||||
suspect: []
|
||||
},
|
||||
dashboardWindowStart: then.toISOString(),
|
||||
dashboardWindowEnd: new Date().toISOString(),
|
||||
domains: {
|
||||
whitelist: []
|
||||
},
|
||||
saveSettingsError: null,
|
||||
fetchSettingsError: null,
|
||||
fetchingSettings: false
|
||||
};
|
||||
|
||||
export default function settings (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SETTINGS_LOADING:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: true,
|
||||
fetchSettingsError: null,
|
||||
};
|
||||
case actions.SETTINGS_RECEIVED:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
};
|
||||
case actions.SETTINGS_FETCH_ERROR:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: action.error,
|
||||
};
|
||||
case actions.SETTINGS_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
};
|
||||
case actions.SAVE_SETTINGS_LOADING:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: true,
|
||||
saveSettingsError: null,
|
||||
};
|
||||
case actions.SAVE_SETTINGS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
};
|
||||
case actions.SAVE_SETTINGS_FAILED:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: action.error,
|
||||
};
|
||||
case actions.WORDLIST_UPDATED:
|
||||
return update(state, {
|
||||
wordlist: {
|
||||
[action.listName]: {
|
||||
$set: action.list
|
||||
}
|
||||
}
|
||||
});
|
||||
case actions.DOMAINLIST_UPDATED:
|
||||
return update(state, {
|
||||
domains: {
|
||||
[action.listName]: {$set: action.list},
|
||||
}
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,7 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
|
||||
*/
|
||||
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
|
||||
h3 {
|
||||
color: black;
|
||||
font-size: 1.26em;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.leftColumn {
|
||||
@@ -21,6 +9,15 @@
|
||||
width: 234px;
|
||||
}
|
||||
|
||||
.saveBox {
|
||||
margin-top: 38px;
|
||||
}
|
||||
|
||||
.changedSave {
|
||||
background-color: #00796B;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(100% - 300px);
|
||||
padding: 10px 14px;
|
||||
@@ -28,183 +25,3 @@
|
||||
max-width: 718px;
|
||||
}
|
||||
|
||||
.configSetting {
|
||||
margin-bottom: 20px;
|
||||
align-items: flex-start;
|
||||
min-height: 100px;
|
||||
max-width: 600px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
.copiedText {
|
||||
display: inline-block;
|
||||
color: #00796b;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settingsError {
|
||||
color: #d50000;
|
||||
}
|
||||
|
||||
.settingsError i {
|
||||
font-size: 14px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.settingsHeader {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 7px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.disabledSettingText {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.configSettingInfoBox {
|
||||
min-height: 100px;
|
||||
margin-bottom: 20px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
text-align: left;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.configSettingEmbed {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.configTimeoutSelect {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
|
||||
i { /* fix for firefox and react-mdl-selectfield@0.2.0 */
|
||||
padding: 20px 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.changedSave {
|
||||
background-color: #00796B;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.embedInput {
|
||||
width: 100%;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
min-height: 175px;
|
||||
font-size: 14px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.customCSSInput {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
padding: 14px;
|
||||
letter-spacing: 0.03em;
|
||||
color: #555;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.enabledSetting {
|
||||
border-left-color: #00796b;
|
||||
border-left-style: solid;
|
||||
border-left-width: 7px;
|
||||
}
|
||||
|
||||
.disabledSetting {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.saveBox {
|
||||
margin-top: 38px;
|
||||
}
|
||||
|
||||
.settingsSection {
|
||||
padding-bottom: 200px;
|
||||
.action {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: inline-block;
|
||||
padding: 0px 30px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.Configure {
|
||||
|
||||
p {
|
||||
line-height: 1.2;
|
||||
max-width: 550px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.descriptionBox {
|
||||
margin-top: 15px;
|
||||
|
||||
input {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,119 +1,40 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import {Button, List, Item, Card, Spinner} from 'coral-ui';
|
||||
import {Button, List, Item} from 'coral-ui';
|
||||
import styles from './Configure.css';
|
||||
import StreamSettings from './StreamSettings';
|
||||
import ModerationSettings from './ModerationSettings';
|
||||
import TechSettings from './TechSettings';
|
||||
import StreamSettings from '../containers/StreamSettings';
|
||||
import ModerationSettings from '../containers/ModerationSettings';
|
||||
import TechSettings from '../containers/TechSettings';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class Configure extends Component {
|
||||
|
||||
state = {
|
||||
activeSection: 'stream',
|
||||
changed: false,
|
||||
errors: {}
|
||||
};
|
||||
|
||||
saveSettings = () => {
|
||||
this.props.saveSettingsToServer();
|
||||
this.setState({changed: false});
|
||||
}
|
||||
|
||||
changeSection = (activeSection) => {
|
||||
this.setState({activeSection});
|
||||
}
|
||||
|
||||
onChangeWordlist = (listName, list) => {
|
||||
this.setState({changed: true});
|
||||
this.props.updateWordlist(listName, list);
|
||||
}
|
||||
|
||||
onChangeDomainlist = (listName, list) => {
|
||||
this.setState({changed: true});
|
||||
this.props.updateDomainlist(listName, list);
|
||||
}
|
||||
|
||||
onSettingUpdate = (setting) => {
|
||||
this.setState({changed: true});
|
||||
this.props.updateSettings(setting);
|
||||
}
|
||||
|
||||
// Sets an arbitrary error string and a boolean state.
|
||||
// This allows the system to track multiple errors.
|
||||
onSettingError = (error, state) => {
|
||||
this.setState((prevState) => {
|
||||
prevState.errors[error] = state;
|
||||
return prevState;
|
||||
});
|
||||
}
|
||||
|
||||
getSection (section) {
|
||||
const pageTitle = this.getPageTitle(section);
|
||||
let sectionComponent;
|
||||
getSectionComponent(section) {
|
||||
switch(section){
|
||||
case 'stream':
|
||||
sectionComponent = <StreamSettings
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate}
|
||||
errors={this.state.errors}
|
||||
settingsError={this.onSettingError}/>;
|
||||
break;
|
||||
return StreamSettings;
|
||||
case 'moderation':
|
||||
sectionComponent = <ModerationSettings
|
||||
onChangeWordlist={this.onChangeWordlist}
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate} />;
|
||||
break;
|
||||
return ModerationSettings;
|
||||
case 'tech':
|
||||
sectionComponent = <TechSettings
|
||||
onChangeDomainlist={this.onChangeDomainlist}
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate} />;
|
||||
}
|
||||
|
||||
if (this.props.settings.fetchingSettings) {
|
||||
return <Card shadow="4"><Spinner/>Loading settings...</Card>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.settingsSection}>
|
||||
<h3>{pageTitle}</h3>
|
||||
{sectionComponent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getPageTitle (section) {
|
||||
switch(section) {
|
||||
case 'stream':
|
||||
return t('configure.stream_settings');
|
||||
case 'moderation':
|
||||
return t('configure.moderation_settings');
|
||||
case 'tech':
|
||||
return t('configure.tech_settings');
|
||||
default:
|
||||
return '';
|
||||
return TechSettings;
|
||||
}
|
||||
throw new Error(`Unknown section ${section}`);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {activeSection} = this.state;
|
||||
const section = this.getSection(activeSection);
|
||||
const {auth: {user}} = this.props;
|
||||
const {auth: {user}, canSave, savePending, setActiveSection, activeSection} = this.props;
|
||||
const SectionComponent = this.getSectionComponent(activeSection);
|
||||
|
||||
if (!can(user, 'UPDATE_CONFIG')) {
|
||||
return <p>You must be an administrator to access config settings. Please find the nearest Admin and ask them to level you up!</p>;
|
||||
}
|
||||
|
||||
const showSave = Object.keys(this.state.errors).reduce(
|
||||
(bool, error) => this.state.errors[error] ? false : bool, this.state.changed);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<List onChange={this.changeSection} activeItem={activeSection}>
|
||||
<List onChange={setActiveSection} activeItem={activeSection}>
|
||||
<Item itemId='stream' icon='speaker_notes'>
|
||||
{t('configure.stream_settings')}
|
||||
</Item>
|
||||
@@ -126,10 +47,10 @@ export default class Configure extends Component {
|
||||
</List>
|
||||
<div className={styles.saveBox}>
|
||||
{
|
||||
showSave ?
|
||||
canSave ?
|
||||
<Button
|
||||
raised
|
||||
onClick={this.saveSettings}
|
||||
onClick={savePending}
|
||||
className={styles.changedSave}
|
||||
icon='check'
|
||||
full
|
||||
@@ -150,11 +71,25 @@ export default class Configure extends Component {
|
||||
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
{ this.props.saveFetchingError }
|
||||
{ this.props.fetchSettingsError }
|
||||
{ section }
|
||||
<SectionComponent
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
settings={this.props.settings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Configure.propTypes = {
|
||||
notify: PropTypes.func.isRequired,
|
||||
savePending: PropTypes.func.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
setActiveSection: PropTypes.func.isRequired,
|
||||
activeSection: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.title {
|
||||
color: black;
|
||||
font-size: 1.26em;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ConfigurePage.css';
|
||||
|
||||
const ConfigurePage = ({title, children, ...rest}) => (
|
||||
<div {...rest}>
|
||||
<h3 className={styles.title}>{title}</h3>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
ConfigurePage.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ConfigurePage;
|
||||
@@ -1,25 +1,25 @@
|
||||
import React from 'react';
|
||||
import {Card} from 'coral-ui';
|
||||
import styles from './Configure.css';
|
||||
import PropTypes from 'prop-types';
|
||||
import TagsInput from 'coral-admin/src/components/TagsInput';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import ConfigureCard from 'coral-framework/components/ConfigureCard';
|
||||
|
||||
const Domainlist = ({domains, onChangeDomainlist}) => {
|
||||
return (
|
||||
<Card id={styles.domainlist} className={styles.configSetting}>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.settingsHeader}>{t('configure.domain_list_title')}</div>
|
||||
<p className={styles.domainlistDesc}>{t('configure.domain_list_text')}</p>
|
||||
<div className={styles.wrapper}>
|
||||
<TagsInput
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
onChange={(tags) => onChangeDomainlist('whitelist', tags)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<ConfigureCard title={t('configure.domain_list_title')}>
|
||||
<p>{t('configure.domain_list_text')}</p>
|
||||
<TagsInput
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
onChange={(tags) => onChangeDomainlist('whitelist', tags)}
|
||||
/>
|
||||
</ConfigureCard>
|
||||
);
|
||||
};
|
||||
|
||||
Domainlist.propTypes = {
|
||||
domains: PropTypes.array.isRequired,
|
||||
onChangeDomainlist: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Domainlist;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
.embedInput {
|
||||
width: 100%;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
min-height: 175px;
|
||||
font-size: 14px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.copiedText {
|
||||
display: inline-block;
|
||||
color: #00796b;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
import React, {Component} from 'react';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import join from 'url-join';
|
||||
import styles from './Configure.css';
|
||||
import {Button, Card} from 'coral-ui';
|
||||
import styles from './EmbedLink.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {BASE_URL} from 'coral-framework/constants/url';
|
||||
import ConfigureCard from 'coral-framework/components/ConfigureCard';
|
||||
|
||||
class EmbedLink extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {copied: false};
|
||||
}
|
||||
state = {copied: false};
|
||||
|
||||
copyToClipBoard = () => {
|
||||
const copyTextarea = document.querySelector(`.${styles.embedInput}`);
|
||||
@@ -38,21 +35,18 @@ class EmbedLink extends Component {
|
||||
"></script>
|
||||
`.trim();
|
||||
return (
|
||||
<Card shadow="2" className={styles.configSetting}>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.settingsHeader}>Embed Comment Stream</div>
|
||||
<p>{t('configure.copy_and_paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<div className={styles.actions}>
|
||||
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
|
||||
{t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>
|
||||
{this.state.copied && 'Copied!'}
|
||||
</div>
|
||||
<ConfigureCard title={'Embed Comment Stream'}>
|
||||
<p>{t('configure.copy_and_paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<div className={styles.actions}>
|
||||
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
|
||||
{t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>
|
||||
{this.state.copied && 'Copied!'}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</ConfigureCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.Configure}>
|
||||
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateEmailConfirmation(updateSettings, settings.requireEmailConfirmation)}
|
||||
checked={settings.requireEmailConfirmation} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{t('configure.require_email_verification')}</div>
|
||||
<p className={settings.requireEmailConfirmation ? '' : styles.disabledSettingText}>
|
||||
{t('configure.require_email_verification_text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateModeration(updateSettings, settings.moderation)}
|
||||
checked={settings.moderation === 'PRE'} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{t('configure.enable_pre_moderation')}</div>
|
||||
<p className={settings.moderation === 'PRE' ? '' : styles.disabledSettingText}>
|
||||
{t('configure.enable_pre_moderation_text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${settings.premodLinksEnable ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updatePremodLinksEnable(updateSettings, settings.premodLinksEnable)}
|
||||
checked={settings.premodLinksEnable} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{t('configure.enable_premod_links')}</div>
|
||||
<p>
|
||||
{t('configure.enable_premod_links_text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Wordlist
|
||||
bannedWords={settings.wordlist.banned}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
onChangeWordlist={onChangeWordlist} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
render() {
|
||||
const {settings, data, root} = this.props;
|
||||
|
||||
return (
|
||||
<ConfigurePage
|
||||
title={t('configure.moderation_settings')}
|
||||
>
|
||||
<ConfigureCard
|
||||
checked={settings.requireEmailConfirmation}
|
||||
onCheckbox={this.updateEmailConfirmation}
|
||||
title={t('configure.require_email_verification')}
|
||||
>
|
||||
{t('configure.require_email_verification_text')}
|
||||
</ConfigureCard>
|
||||
<ConfigureCard
|
||||
checked={settings.moderation === 'PRE'}
|
||||
onCheckbox={this.updateModeration}
|
||||
title={t('configure.enable_pre_moderation')}
|
||||
>
|
||||
{t('configure.enable_pre_moderation_text')}
|
||||
</ConfigureCard>
|
||||
<ConfigureCard
|
||||
checked={settings.premodLinksEnable}
|
||||
onCheckbox={this.updatePremodLinksEnable}
|
||||
title={t('configure.enable_premod_links')}
|
||||
>
|
||||
{t('configure.enable_premod_links_text')}
|
||||
</ConfigureCard>
|
||||
<Wordlist
|
||||
bannedWords={settings.wordlist.banned}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
onChangeWordlist={this.updateWordlist} />
|
||||
<Slot
|
||||
fill="adminModerationSettings"
|
||||
data={data}
|
||||
queryData={{root, settings}}
|
||||
/>
|
||||
</ConfigurePage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.Configure}>
|
||||
<Card className={`${styles.configSetting} ${settings.charCountEnable ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateCharCountEnable(updateSettings, settings.charCountEnable)}
|
||||
checked={settings.charCountEnable} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{t('configure.comment_count_header')}</div>
|
||||
<p className={settings.charCountEnable ? '' : styles.disabledSettingText}>
|
||||
<span>{t('configure.comment_count_text_pre')}</span>
|
||||
<input type='text'
|
||||
className={`${styles.inlineTextfield} ${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
|
||||
htmlFor='charCount'
|
||||
onChange={updateCharCount(updateSettings, settingsError)}
|
||||
value={settings.charCount}
|
||||
disabled={settings.charCountEnable ? '' : 'disabled'}
|
||||
/>
|
||||
<span>{t('configure.comment_count_text_post')}</span>
|
||||
{
|
||||
errors.charCount &&
|
||||
<span className={styles.settingsError}>
|
||||
<br/>
|
||||
<Icon name="error_outline"/>
|
||||
{t('configure.comment_count_error')}
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${styles.configSettingInfoBox} ${settings.infoBoxEnable ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
|
||||
checked={settings.infoBoxEnable} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>
|
||||
{t('configure.include_comment_stream')}
|
||||
</div>
|
||||
<p className={settings.infoBoxEnable ? '' : styles.disabledSettingText}>
|
||||
{t('configure.include_comment_stream_desc')}
|
||||
</p>
|
||||
<div className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<MarkdownEditor
|
||||
className={styles.descriptionBox}
|
||||
onChange={updateInfoBoxContent(updateSettings)}
|
||||
value={settings.infoBoxContent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${styles.configSettingInfoBox}`}>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.settingsHeader}>{t('configure.closed_stream_settings')}</div>
|
||||
<p>{t('configure.closed_comments_desc')}</p>
|
||||
<div>
|
||||
<TextArea className={styles.descriptionBox}
|
||||
onChange={updateClosedMessage(updateSettings)}
|
||||
value={settings.closedMessage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* Edit Comment Timeframe */}
|
||||
<Card className={styles.configSetting}>
|
||||
<div className={styles.settingsHeader}>{t('configure.edit_comment_timeframe_heading')}</div>
|
||||
<p>
|
||||
{t('configure.edit_comment_timeframe_text_pre')}
|
||||
|
||||
<input
|
||||
className={`${styles.inlineTextfield} ${styles.editCommentTimeframeTextfield}`}
|
||||
type="number"
|
||||
min="0"
|
||||
onChange={updateEditCommentWindowLength(updateSettings)}
|
||||
placeholder="30"
|
||||
defaultValue={(settings.editCommentWindowLength / 1000) /* saved as ms, rendered as seconds */}
|
||||
pattern='[0-9]+([\.][0-9]*)?'
|
||||
/>
|
||||
|
||||
{t('configure.edit_comment_timeframe_text_post')}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${styles.configSettingInfoBox}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateAutoClose(updateSettings, !settings.autoCloseStream)}
|
||||
checked={settings.autoCloseStream} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{t('configure.close_after')}</div>
|
||||
<br />
|
||||
<Textfield
|
||||
type='number'
|
||||
pattern='[0-9]+'
|
||||
style={{width: 50}}
|
||||
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout)}
|
||||
value={getTimeoutAmount(settings.closedTimeout)}
|
||||
label={t('configure.closed_comments_label')} />
|
||||
<div className={styles.configTimeoutSelect}>
|
||||
<SelectField
|
||||
label="comments closed time window"
|
||||
value={getTimeoutMeasure(settings.closedTimeout)}
|
||||
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout, true)}>
|
||||
<Option value={'hours'}>{t('configure.hours')}</Option>
|
||||
<Option value={'days'}>{t('configure.days')}</Option>
|
||||
<Option value={'weeks'}>{t('configure.weeks')}</Option>
|
||||
</SelectField>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* the above card should be the last one if at all possible because of z-index issues with the selects */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<ConfigurePage
|
||||
title={t('configure.stream_settings')}
|
||||
>
|
||||
<ConfigureCard
|
||||
checked={settings.charCountEnable}
|
||||
onCheckbox={this.updateCharCountEnable}
|
||||
title={t('configure.comment_count_header')}
|
||||
>
|
||||
<span>{t('configure.comment_count_text_pre')}</span>
|
||||
<input type='text'
|
||||
className={cn(styles.inlineTextfield, styles.charCountTexfield, settings.charCountEnable && styles.charCountTexfieldEnable)}
|
||||
htmlFor='charCount'
|
||||
onChange={this.updateCharCount}
|
||||
value={settings.charCount}
|
||||
disabled={settings.charCountEnable ? '' : 'disabled'}
|
||||
/>
|
||||
<span>{t('configure.comment_count_text_post')}</span>
|
||||
{
|
||||
errors.charCount &&
|
||||
<span className={styles.settingsError}>
|
||||
<br/>
|
||||
<Icon name="error_outline"/>
|
||||
{t('configure.comment_count_error')}
|
||||
</span>
|
||||
}
|
||||
</ConfigureCard>
|
||||
<ConfigureCard
|
||||
checked={settings.infoBoxEnable}
|
||||
onCheckbox={this.updateInfoBoxEnable}
|
||||
title={t('configure.include_comment_stream')}
|
||||
>
|
||||
<p>
|
||||
{t('configure.include_comment_stream_desc')}
|
||||
</p>
|
||||
<div className={cn(styles.configSettingInfoBox, settings.infoBoxEnable ? null : styles.hidden)} >
|
||||
<MarkdownEditor
|
||||
className={styles.descriptionBox}
|
||||
onChange={this.updateInfoBoxContent}
|
||||
value={settings.infoBoxContent}
|
||||
/>
|
||||
</div>
|
||||
</ConfigureCard>
|
||||
<ConfigureCard
|
||||
checked={settings.configSettingInfoBox}
|
||||
onCheckbox={this.updateClosedMessage}
|
||||
title={t('configure.closed_stream_settings')}
|
||||
>
|
||||
<p>{t('configure.closed_comments_desc')}</p>
|
||||
<div>
|
||||
<TextArea className={styles.descriptionBox}
|
||||
onChange={this.updateClosedMessage}
|
||||
value={settings.closedMessage}
|
||||
/>
|
||||
</div>
|
||||
</ConfigureCard>
|
||||
<ConfigureCard
|
||||
title={t('configure.edit_comment_timeframe_heading')}
|
||||
>
|
||||
{t('configure.edit_comment_timeframe_text_pre')}
|
||||
|
||||
<input
|
||||
className={cn(styles.inlineTextfield, styles.editCommentTimeframeTextfield)}
|
||||
type="number"
|
||||
min="0"
|
||||
onChange={this.updateEditCommentWindowLength}
|
||||
placeholder="30"
|
||||
defaultValue={(settings.editCommentWindowLength / 1000) /* saved as ms, rendered as seconds */}
|
||||
pattern='[0-9]+([\.][0-9]*)?'
|
||||
/>
|
||||
|
||||
{t('configure.edit_comment_timeframe_text_post')}
|
||||
</ConfigureCard>
|
||||
<ConfigureCard
|
||||
checked={settings.autoCloseStream}
|
||||
onCheckbox={this.updateAutoClose}
|
||||
title={t('configure.close_after')}
|
||||
>
|
||||
<Textfield
|
||||
type='number'
|
||||
pattern='[0-9]+'
|
||||
style={{width: 50}}
|
||||
onChange={this.updateClosedTimeout}
|
||||
value={getTimeoutAmount(settings.closedTimeout)}
|
||||
label={t('configure.closed_comments_label')} />
|
||||
<div className={styles.configTimeoutSelect}>
|
||||
<SelectField
|
||||
label="comments closed time window"
|
||||
value={getTimeoutMeasure(settings.closedTimeout)}
|
||||
onChange={this.updateClosedTimeoutMeasure}>
|
||||
<Option value={'hours'}>{t('configure.hours')}</Option>
|
||||
<Option value={'days'}>{t('configure.days')}</Option>
|
||||
<Option value={'weeks'}>{t('configure.weeks')}</Option>
|
||||
</SelectField>
|
||||
</div>
|
||||
</ConfigureCard>
|
||||
{/* the above card should be the last one if at all possible because of z-index issues with the selects */}
|
||||
<Slot
|
||||
fill="adminStreamSettings"
|
||||
data={data}
|
||||
queryData={{root, settings}}
|
||||
/>
|
||||
</ConfigurePage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.customCSSInput {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
padding: 14px;
|
||||
letter-spacing: 0.03em;
|
||||
color: #555;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.Configure}>
|
||||
<Domainlist
|
||||
domains={settings.domains.whitelist}
|
||||
onChangeDomainlist={onChangeDomainlist} />
|
||||
<EmbedLink />
|
||||
<Card className={styles.configSetting}>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.settingsHeader}>{t('configure.custom_css_url')}</div>
|
||||
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 (
|
||||
<ConfigurePage
|
||||
title={t('configure.tech_settings')}
|
||||
>
|
||||
<Domainlist
|
||||
domains={settings.domains.whitelist}
|
||||
onChangeDomainlist={this.updateDomainlist} />
|
||||
<EmbedLink />
|
||||
<ConfigureCard title={t('configure.custom_css_url')}>
|
||||
<p>{t('configure.custom_css_url_desc')}</p>
|
||||
<input
|
||||
className={styles.customCSSInput}
|
||||
value={settings.customCssUrl}
|
||||
onChange={updateCustomCssUrl(updateSettings)} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
onChange={this.updateCustomCssUrl} />
|
||||
</ConfigureCard>
|
||||
<Slot
|
||||
fill="adminTechSettings"
|
||||
data={data}
|
||||
queryData={{root, settings}}
|
||||
/>
|
||||
</ConfigurePage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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}) => (
|
||||
<div>
|
||||
<Card id={styles.bannedWordlist} className={styles.configSetting}>
|
||||
<div className={styles.settingsHeader}>{t('configure.banned_words_title')}</div>
|
||||
<p className={styles.wordlistDesc}>{t('configure.banned_word_text')}</p>
|
||||
<div className={styles.wrapper}>
|
||||
<TagsInput
|
||||
value={bannedWords}
|
||||
inputProps={{placeholder: 'word or phrase'}}
|
||||
onChange={(tags) => onChangeWordlist('banned', tags)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card id={styles.suspectWordlist} className={styles.configSetting}>
|
||||
<div className={styles.settingsHeader}>{t('configure.suspect_word_title')}</div>
|
||||
<p className={styles.wordlistDesc}>{t('configure.suspect_word_text')}</p>
|
||||
<div className={styles.wrapper}>
|
||||
<TagsInput
|
||||
value={suspectWords}
|
||||
inputProps={{placeholder: 'word or phrase'}}
|
||||
onChange={(tags) => onChangeWordlist('suspect', tags)} />
|
||||
</div>
|
||||
</Card>
|
||||
<ConfigureCard title={t('configure.banned_words_title')}>
|
||||
<p>{t('configure.banned_word_text')}</p>
|
||||
<TagsInput
|
||||
value={bannedWords}
|
||||
inputProps={{placeholder: 'word or phrase'}}
|
||||
onChange={(tags) => onChangeWordlist('banned', tags)}
|
||||
/>
|
||||
</ConfigureCard>
|
||||
<ConfigureCard title={t('configure.suspect_word_title')}>
|
||||
<p>{t('configure.suspect_word_text')}</p>
|
||||
<TagsInput
|
||||
value={suspectWords}
|
||||
inputProps={{placeholder: 'word or phrase'}}
|
||||
onChange={(tags) => onChangeWordlist('suspect', tags)} />
|
||||
</ConfigureCard>
|
||||
</div>
|
||||
);
|
||||
|
||||
Wordlist.propTypes = {
|
||||
suspectWords: PropTypes.array.isRequired,
|
||||
bannedWords: PropTypes.array.isRequired,
|
||||
onChangeWordlist: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Wordlist;
|
||||
|
||||
@@ -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 <Configure {...this.props} />;
|
||||
if(this.props.data.loading) {
|
||||
return <Spinner/>;
|
||||
}
|
||||
|
||||
return <Configure
|
||||
notify={this.props.notify}
|
||||
auth={this.props.auth}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
settings={this.mergedSettings}
|
||||
canSave={this.props.canSave}
|
||||
savePending={this.savePending}
|
||||
setActiveSection={this.props.setActiveSection}
|
||||
activeSection={this.props.activeSection}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
<div className={styles.itemBody}>
|
||||
<div className={styles.body}>
|
||||
<CommentBodyHighlighter
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
bannedWords={settings.wordlist.banned}
|
||||
body={comment.body}
|
||||
/>
|
||||
{' '}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}) => (
|
||||
<Card {...rest} className={cn(
|
||||
styles.card,
|
||||
className,
|
||||
{
|
||||
[styles.enabledSetting]: checked === true,
|
||||
[styles.disabledSetting]: checked === false,
|
||||
},
|
||||
)}>
|
||||
{checked !== undefined &&
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={onCheckbox}
|
||||
checked={checked} />
|
||||
</div>
|
||||
}
|
||||
<div className={cn(styles.wrapper, {
|
||||
[styles.content]: checked !== undefined,
|
||||
})}>
|
||||
<div className={styles.header}>{title}</div>
|
||||
<div className={cn({
|
||||
[styles.disabledSettingText]: checked === false,
|
||||
})}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
ConfigureCard.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
onCheckbox: PropTypes.func,
|
||||
checked: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ConfigureCard;
|
||||
@@ -16,6 +16,7 @@ export default {
|
||||
'ModifyTagResponse',
|
||||
'IgnoreUserResponse',
|
||||
'StopIgnoringUserResponse',
|
||||
'UpdateSettingsResponse',
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
|
||||
+12
-11
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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';
|
||||
|
||||
+27
-14
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(', ') : '<None>', 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user