diff --git a/.snyk b/.snyk new file mode 100644 index 000000000..3e46b4ab9 --- /dev/null +++ b/.snyk @@ -0,0 +1,12 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.7.1 +ignore: {} +# patches apply the minimum changes required to fix a vulnerability +patch: + 'npm:marked:20170112': + - marked: + patched: '2017-10-11T02:07:15.455Z' + - graphql-docs > marked: + patched: '2017-10-11T02:07:15.455Z' + - simplemde > marked: + patched: '2017-10-11T02:07:15.455Z' diff --git a/README.md b/README.md index 057b29cf1..ad20faf03 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,13 @@ Check out our Docs: https://coralproject.github.io/talk/ - Project: https://coralproject.net/ - Roadmap: https://www.pivotaltracker.com/n/projects/1863625 +## End-to-End Testing + +Talk uses [Nightwatch](http://nightwatchjs.org/) to write e2e tests. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at Browserstack. + + +![](/public/img/browserstack_logo.png) + ## License Copyright 2017 Mozilla Foundation @@ -26,8 +33,12 @@ Check out our Docs: https://coralproject.github.io/talk/ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + either express or implied. - See the License for the specific language governing permissions and limitations under the License. + See the License for the specific language governing permissions + and limitations under the License. diff --git a/client/coral-admin/src/actions/configure.js b/client/coral-admin/src/actions/configure.js new file mode 100644 index 000000000..128de75bc --- /dev/null +++ b/client/coral-admin/src/actions/configure.js @@ -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}; +}; diff --git a/client/coral-admin/src/actions/settings.js b/client/coral-admin/src/actions/settings.js deleted file mode 100644 index 35f2013c9..000000000 --- a/client/coral-admin/src/actions/settings.js +++ /dev/null @@ -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}); - }); -}; diff --git a/client/coral-admin/src/components/Drawer.css b/client/coral-admin/src/components/Drawer.css new file mode 100644 index 000000000..861d4074b --- /dev/null +++ b/client/coral-admin/src/components/Drawer.css @@ -0,0 +1,13 @@ +@custom-media --table-viewport (max-width: 1024px); + +:global { + .mdl-layout__drawer-button { + visibility: hidden; + } + + @media (--table-viewport) { + .mdl-layout__drawer-button { + visibility: visible; + } + } +} \ No newline at end of file diff --git a/client/coral-admin/src/components/ui/Drawer.js b/client/coral-admin/src/components/Drawer.js similarity index 90% rename from client/coral-admin/src/components/ui/Drawer.js rename to client/coral-admin/src/components/Drawer.js index 2b728a33a..0385b1773 100644 --- a/client/coral-admin/src/components/ui/Drawer.js +++ b/client/coral-admin/src/components/Drawer.js @@ -6,8 +6,8 @@ import styles from './Drawer.css'; import t from 'coral-framework/services/i18n'; import {can} from 'coral-framework/services/perms'; -const CoralDrawer = ({handleLogout, auth}) => ( - +const CoralDrawer = ({handleLogout, auth = {}}) => ( + { auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
@@ -57,7 +57,8 @@ const CoralDrawer = ({handleLogout, auth}) => ( CoralDrawer.propTypes = { handleLogout: PropTypes.func.isRequired, - restricted: PropTypes.bool // hide app elements from a logged out user + restricted: PropTypes.bool, // hide app elements from a logged out user + auth: PropTypes.object }; export default CoralDrawer; diff --git a/client/coral-admin/src/components/ModerationKeysModal.css b/client/coral-admin/src/components/ModerationKeysModal.css index 73697d930..6c7dc61e9 100644 --- a/client/coral-admin/src/components/ModerationKeysModal.css +++ b/client/coral-admin/src/components/ModerationKeysModal.css @@ -21,7 +21,6 @@ .callToAction { position: fixed; - left: 10px; bottom: 10px; width: 280px; height: 200px; @@ -29,6 +28,7 @@ padding: 15px; box-sizing: border-box; box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15); + z-index: 10; .ctaHeader { font-size: 16px; diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index a5955af45..275503085 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -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} diff --git a/client/coral-admin/src/components/UserDetailComment.js b/client/coral-admin/src/components/UserDetailComment.js index f8d81ce8b..e3e89d00a 100644 --- a/client/coral-admin/src/components/UserDetailComment.js +++ b/client/coral-admin/src/components/UserDetailComment.js @@ -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 {
{' '} @@ -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; diff --git a/client/coral-admin/src/components/ui/Header.css b/client/coral-admin/src/components/ui/Header.css index 6458dc161..d88774c19 100644 --- a/client/coral-admin/src/components/ui/Header.css +++ b/client/coral-admin/src/components/ui/Header.css @@ -15,10 +15,16 @@ } } +.headerWrapper { + background-color: #696969; +} + .header { box-shadow: none; min-height: 58px; display: block; + max-width: 1280px; + margin: 0 auto; background-color: #696969; box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12); } diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 30c5fa7e6..62f6a7fd1 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -15,93 +15,95 @@ const CoralHeader = ({ root }) => { return ( -
- -
- { - auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? - - - {t('configure.dashboard')} - - { - can(auth.user, 'MODERATE_COMMENTS') && ( - - {t('configure.moderate')} - {(root.premodCount !== 0 || root.reportedCount !== 0) && } - - ) - } - - {t('configure.stories')} - +
+
+ +
+ { + auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? + + + {t('configure.dashboard')} + + { + can(auth.user, 'MODERATE_COMMENTS') && ( + + {t('configure.moderate')} + {(root.premodCount !== 0 || root.reportedCount !== 0) && } + + ) + } + + {t('configure.stories')} + - - {t('configure.community')} - {root.flaggedUsernamesCount !== 0 && } - + + {t('configure.community')} + {root.flaggedUsernamesCount !== 0 && } + - { - can(auth.user, 'UPDATE_CONFIG') && ( - - {t('configure.configure')} - - ) - } - - : - null - } -
- + { + can(auth.user, 'UPDATE_CONFIG') && ( + + {t('configure.configure')} + + ) + } + + : + null + } +
+ +
-
-
+
+
); }; diff --git a/client/coral-admin/src/components/ui/Layout.css b/client/coral-admin/src/components/ui/Layout.css index 924c6ced7..ac13e96f7 100644 --- a/client/coral-admin/src/components/ui/Layout.css +++ b/client/coral-admin/src/components/ui/Layout.css @@ -1,4 +1,4 @@ .layout { - margin: 0 auto; - background-color: #FAFAFA; + margin: 0 auto; + background-color: #FAFAFA; } diff --git a/client/coral-admin/src/components/ui/Layout.js b/client/coral-admin/src/components/ui/Layout.js index 9ee9fda9f..859191927 100644 --- a/client/coral-admin/src/components/ui/Layout.js +++ b/client/coral-admin/src/components/ui/Layout.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {Layout as LayoutMDL} from 'react-mdl'; import Header from '../../containers/Header'; -import Drawer from './Drawer'; +import Drawer from '../Drawer'; import styles from './Layout.css'; const Layout = ({ @@ -21,6 +21,7 @@ const Layout = ({
{children} diff --git a/client/coral-admin/src/components/ui/Logo.css b/client/coral-admin/src/components/ui/Logo.css index 62a223683..767d91560 100644 --- a/client/coral-admin/src/components/ui/Logo.css +++ b/client/coral-admin/src/components/ui/Logo.css @@ -20,7 +20,6 @@ background: #696969; height: 100%; width: 128px; - z-index: 10; border-right: 1px #757575 solid; } diff --git a/client/coral-admin/src/constants/configure.js b/client/coral-admin/src/constants/configure.js new file mode 100644 index 000000000..05673b5aa --- /dev/null +++ b/client/coral-admin/src/constants/configure.js @@ -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`; diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index 2ae842b1d..ffc661150 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -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) => ({ diff --git a/client/coral-admin/src/containers/UserDetailComment.js b/client/coral-admin/src/containers/UserDetailComment.js index 9eef5934f..fe95f4ae4 100644 --- a/client/coral-admin/src/containers/UserDetailComment.js +++ b/client/coral-admin/src/containers/UserDetailComment.js @@ -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)} } diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js index 6b8aa7456..d7656d9a5 100644 --- a/client/coral-admin/src/graphql/index.js +++ b/client/coral-admin/src/graphql/index.js @@ -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; + } + } + }), }, }; diff --git a/client/coral-admin/src/reducers/configure.js b/client/coral-admin/src/reducers/configure.js new file mode 100644 index 000000000..e03c9ea8f --- /dev/null +++ b/client/coral-admin/src/reducers/configure.js @@ -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; +} diff --git a/client/coral-admin/src/reducers/dashboard.js b/client/coral-admin/src/reducers/dashboard.js new file mode 100644 index 000000000..3b3414d3a --- /dev/null +++ b/client/coral-admin/src/reducers/dashboard.js @@ -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; +} diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index b329675a4..372fedbae 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -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, diff --git a/client/coral-admin/src/reducers/settings.js b/client/coral-admin/src/reducers/settings.js deleted file mode 100644 index 336047e5b..000000000 --- a/client/coral-admin/src/reducers/settings.js +++ /dev/null @@ -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; - } -} diff --git a/client/coral-admin/src/routes/Configure/components/Configure.css b/client/coral-admin/src/routes/Configure/components/Configure.css index eae75caba..33a0c9c6f 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.css +++ b/client/coral-admin/src/routes/Configure/components/Configure.css @@ -1,17 +1,7 @@ -/** - * @TODO: deprecated as this file contains styles from multiple components. Please refactor. - */ - .container { max-width: 1280px; margin: 0 auto; display: flex; - - h3 { - color: black; - font-size: 1.26em; - font-weight: 500; - } } .leftColumn { @@ -19,6 +9,15 @@ width: 234px; } +.saveBox { + margin-top: 38px; +} + +.changedSave { + background-color: #00796B; + color: white; +} + .mainContent { width: calc(100% - 300px); padding: 10px 14px; @@ -26,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; - } - } -} diff --git a/client/coral-admin/src/routes/Configure/components/Configure.js b/client/coral-admin/src/routes/Configure/components/Configure.js index 15ff69b69..ecdbddcc0 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.js +++ b/client/coral-admin/src/routes/Configure/components/Configure.js @@ -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 = ; - break; + return StreamSettings; case 'moderation': - sectionComponent = ; - break; + return ModerationSettings; case 'tech': - sectionComponent = ; - } - - if (this.props.settings.fetchingSettings) { - return Loading settings...; - } - - return ( -
-

{pageTitle}

- {sectionComponent} -
- ); - } - - 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

You must be an administrator to access config settings. Please find the nearest Admin and ask them to level you up!

; } - const showSave = Object.keys(this.state.errors).reduce( - (bool, error) => this.state.errors[error] ? false : bool, this.state.changed); - return (
- + {t('configure.stream_settings')} @@ -126,10 +47,10 @@ export default class Configure extends Component {
{ - showSave ? + canSave ?
); } } + +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, +}; diff --git a/client/coral-admin/src/routes/Configure/components/ConfigurePage.css b/client/coral-admin/src/routes/Configure/components/ConfigurePage.css new file mode 100644 index 000000000..f592f0383 --- /dev/null +++ b/client/coral-admin/src/routes/Configure/components/ConfigurePage.css @@ -0,0 +1,5 @@ +.title { + color: black; + font-size: 1.26em; + font-weight: 500; +} diff --git a/client/coral-admin/src/routes/Configure/components/ConfigurePage.js b/client/coral-admin/src/routes/Configure/components/ConfigurePage.js new file mode 100644 index 000000000..dc1182fc2 --- /dev/null +++ b/client/coral-admin/src/routes/Configure/components/ConfigurePage.js @@ -0,0 +1,17 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './ConfigurePage.css'; + +const ConfigurePage = ({title, children, ...rest}) => ( +
+

{title}

+ {children} +
+); + +ConfigurePage.propTypes = { + title: PropTypes.string.isRequired, + children: PropTypes.node, +}; + +export default ConfigurePage; diff --git a/client/coral-admin/src/routes/Configure/components/Domainlist.js b/client/coral-admin/src/routes/Configure/components/Domainlist.js index 7b7565c5e..9d2d624ac 100644 --- a/client/coral-admin/src/routes/Configure/components/Domainlist.js +++ b/client/coral-admin/src/routes/Configure/components/Domainlist.js @@ -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 ( - -
-
{t('configure.domain_list_title')}
-

{t('configure.domain_list_text')}

-
- onChangeDomainlist('whitelist', tags)} - /> -
-
-
+ +

{t('configure.domain_list_text')}

+ onChangeDomainlist('whitelist', tags)} + /> +
); }; +Domainlist.propTypes = { + domains: PropTypes.array.isRequired, + onChangeDomainlist: PropTypes.func.isRequired, +}; + export default Domainlist; diff --git a/client/coral-admin/src/routes/Configure/components/EmbedLink.css b/client/coral-admin/src/routes/Configure/components/EmbedLink.css new file mode 100644 index 000000000..07987df02 --- /dev/null +++ b/client/coral-admin/src/routes/Configure/components/EmbedLink.css @@ -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%; +} diff --git a/client/coral-admin/src/routes/Configure/components/EmbedLink.js b/client/coral-admin/src/routes/Configure/components/EmbedLink.js index 1ce3d5304..aa1cddadc 100644 --- a/client/coral-admin/src/routes/Configure/components/EmbedLink.js +++ b/client/coral-admin/src/routes/Configure/components/EmbedLink.js @@ -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 { "> `.trim(); return ( - -
-
Embed Comment Stream
-

{t('configure.copy_and_paste')}

-