diff --git a/.gitignore b/.gitignore index 9c55f2708..d30e961bd 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ client/coral-framework/graphql/introspection.json *.DS_STORE coverage/ +test/e2e/reports/ +test/e2e/bslocal.log +test/e2e/selenium-debug.log +browserstack.err plugins.json plugins/* @@ -45,5 +49,6 @@ plugins/* !plugins/talk-plugin-deep-reply-count !plugins/talk-plugin-subscriber !plugins/talk-plugin-flag-details +!plugins/talk-plugin-slack-notifications **/node_modules/* diff --git a/.nsprc b/.nsprc new file mode 100644 index 000000000..071431eff --- /dev/null +++ b/.nsprc @@ -0,0 +1,5 @@ +{ + "exceptions": [ + "https://nodesecurity.io/advisories/531" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 919093750..a089f040c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ -# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) +# Talk + +[![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore) +[![NSP Status](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8/badge)](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8) Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html). -## Documentation +Built with <3 by The Coral Project & Mozilla. -Developer Documentation & Setup Guides https://coralproject.github.io/talk/. +## Getting Started + +Check out our Docs: https://coralproject.github.io/talk/ ## Relevant Links @@ -15,6 +20,13 @@ Developer Documentation & Setup Guides 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 @@ -23,8 +35,12 @@ Developer Documentation & Setup Guides 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/bin/cli-serve b/bin/cli-serve index b04714690..4324cac92 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -1,161 +1,7 @@ #!/usr/bin/env node const program = require('./commander'); -const app = require('../app'); -const debug = require('debug')('talk:cli:serve'); -const errors = require('../errors'); -const {createServer} = require('http'); -const scraper = require('../services/scraper'); -const mailer = require('../services/mailer'); -const MigrationService = require('../services/migration'); -const SetupService = require('../services/setup'); -const kue = require('../services/kue'); -const mongoose = require('../services/mongoose'); -const util = require('./util'); -const cache = require('../services/cache'); -const {createSubscriptionManager} = require('../graph/subscriptions'); -const { - PORT -} = require('../config'); - -/** -* Get port from environment and store in Express. -*/ - -const port = normalizePort(PORT); -app.set('port', port); - -/** -* Create HTTP server. -*/ -const server = createServer(app); - -/** - * Event listener for HTTP server "error" event. - */ -function onError(error) { - if (error.syscall !== 'listen') { - throw error; - } - - let bind = typeof port === 'string' - ? `Pipe ${port}` - : `Port ${port}`; - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - console.error(`${bind} requires elevated privileges`); - break; - case 'EADDRINUSE': - console.error(`${bind} is already in use`); - break; - } - - throw error; -} - -/** - * Normalize a port into a number, string, or false. - */ - -function normalizePort(val) { - let port = parseInt(val, 10); - - if (isNaN(port)) { - - // named pipe - return val; - } - - if (port >= 0) { - - // port number - return port; - } - - return false; -} - -/** - * Event listener for HTTP server "listening" event. - */ - -async function onListening() { - - // Start the cache instance. - await cache.init(); - - let addr = server.address(); - let bind = typeof addr === 'string' - ? `pipe ${addr}` - : `port ${addr.port}`; - debug(`API Server Listening on ${bind}`); -} - -/** - * Start the app. - */ -async function startApp(program) { - - try { - - // Check to see if the application is installed. If the application - // has been installed, then it will throw errors.ErrSettingsNotInit, this - // just means we don't have to check that the migrations have run. - await SetupService.isAvailable(); - - debug('setup is currently available, migrations not being checked'); - - } catch (e) { - - // Check the error. - switch (e) { - case errors.ErrInstallLock, errors.ErrSettingsInit: - - debug('setup is not currently available, migrations now being checked'); - - // The error was expected, just continue. - break; - default: - - // The error was not expected, throw the error! - throw e; - } - - // Now try and check the migration status. - try { - - // Verify that the minimum migration version is met. - await MigrationService.verify(); - - } catch (e) { - console.error(e); - process.exit(1); - } - - debug('migrations do not have to be run'); - } - - /** - * Listen on provided port, on all network interfaces. - */ - server.on('error', onError); - server.on('listening', onListening); - server.on('listening', () => { - - }); - server.listen(port, () => { - - // Mount the websocket server if requested. - if (program.websockets) { - debug(`Websocket Server Listening on ${port}`); - - // Mount the subscriptions server on the application server. - createSubscriptionManager(server); - } - }); -} +const serve = require('../serve'); //============================================================================== // Setting up the program command line arguments. @@ -166,24 +12,6 @@ program .option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread') .parse(process.argv); -// Start the application serving. -startApp(program); +// Start serving. +serve({jobs: program.jobs, websockets: program.websockets}); -// Enable job processing on the thread if enabled. -if (program.jobs) { - - // Start the scraper processor. - scraper.process(); - - // Start the mail processor. - mailer.process(); -} - -// Define a safe shutdown function to call in the event we need to shutdown -// because the node hooks are below which will interrupt the shutdown process. -// Shutdown the mongoose connection, the app server, and the scraper. -util.onshutdown([ - () => program.jobs ? kue.Task.shutdown() : null, - () => mongoose.disconnect(), - () => server.close() -]); diff --git a/bin/cli-setup b/bin/cli-setup index 2e5c1e5b8..c7a44c453 100755 --- a/bin/cli-setup +++ b/bin/cli-setup @@ -195,7 +195,7 @@ const performSetup = async () => { password: user.password } }); - + console.log('Settings created.'); console.log(`User ${newUser.id} created.`); console.log('\nTalk is now installed!'); diff --git a/circle.yml b/circle.yml index f6b35970c..9f35736c6 100644 --- a/circle.yml +++ b/circle.yml @@ -7,9 +7,7 @@ machine: environment: PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" NODE_ENV: "test" - -dependencies: - override: + pre: # TODO: use the following to add in support for MongoDB 3.4. # # Upgrade the database version to 3.4. # - sudo apt-get purge mongodb-org* @@ -19,10 +17,20 @@ dependencies: # - sudo apt-get install -y mongodb-org # - sudo service mongod restart + # Install chromium for e2e and remove old google-chrome + - sudo rm -rf /opt/google/chrome + - sudo rm -f /usr/bin/google-chrome* + - sudo apt-get update + - sudo apt-get install chromium-browser + +dependencies: + override: + # Install node dependencies. - yarn --version - - yarn global add node-gyp --force + - yarn global add node-gyp nsp --force - yarn + post: # Build the static assets. - yarn build @@ -40,6 +48,9 @@ test: override: # Run the tests using the junit reporter. - MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test + # Check dependancies using nsp. + - nsp check + - yarn e2e-ci deployment: release: 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/AdminLogin.js b/client/coral-admin/src/components/AdminLogin.js index be8dd8b09..d195ad72b 100644 --- a/client/coral-admin/src/components/AdminLogin.js +++ b/client/coral-admin/src/components/AdminLogin.js @@ -4,6 +4,7 @@ import Layout from 'coral-admin/src/components/ui/Layout'; import styles from './NotFound.css'; import {Button, TextField, Alert, Success} from 'coral-ui'; import Recaptcha from 'react-recaptcha'; +import cn from 'classnames'; class AdminLogin extends React.Component { @@ -34,19 +35,22 @@ class AdminLogin extends React.Component { render () { const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props; const signInForm = ( -
+ {errorMessage && {errorMessage}} this.setState({email: e.target.value})} /> this.setState({password: e.target.value})} type='password' />
- {`${selectedCommentIds.length} comments selected`} + {selectedCommentIds.length} comments selected ) } @@ -184,8 +186,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 f85a7cdf9..d88774c19 100644 --- a/client/coral-admin/src/components/ui/Header.css +++ b/client/coral-admin/src/components/ui/Header.css @@ -15,20 +15,25 @@ } } +.headerWrapper { + background-color: #696969; +} + .header { - background-color: transparent; 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); } .header > div { - background-color: #696969; position: relative; padding: 0; - min-width: 1280px; + max-width: 1280px; margin: 0 auto; - 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); height: 58px; } 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 a3cc7b7b6..ac13e96f7 100644 --- a/client/coral-admin/src/components/ui/Layout.css +++ b/client/coral-admin/src/components/ui/Layout.css @@ -1,5 +1,4 @@ .layout { - max-width: 1280px; - 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..5d9ef0537 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -152,7 +152,7 @@ export const withUserDetailQuery = withQuery(gql` } ${getSlotFragmentSpreads(slots, 'user')} } - totalComments: commentCount(query: {author_id: $author_id}) + totalComments: commentCount(query: {author_id: $author_id, statuses: []}) rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]}) comments: comments(query: { author_id: $author_id, @@ -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/index.js b/client/coral-admin/src/index.js index 736d3efc9..442a4f536 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -23,6 +23,7 @@ function init({store, storage}) { async function main() { const notification = createNotificationService(toast); const context = await createContext({reducers, graphqlExtension, pluginsConfig, notification, init}); + render( 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/Community/components/FlaggedAccounts.css b/client/coral-admin/src/routes/Community/components/FlaggedAccounts.css index 4e9600cbd..a41bda485 100644 --- a/client/coral-admin/src/routes/Community/components/FlaggedAccounts.css +++ b/client/coral-admin/src/routes/Community/components/FlaggedAccounts.css @@ -2,6 +2,8 @@ padding: 10px; display: flex; padding-bottom: 200px; + max-width: 1280px; + margin: 0 auto; } .mainFlaggedContent { diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js index ff1c2c279..b0524f273 100644 --- a/client/coral-admin/src/routes/Community/components/People.js +++ b/client/coral-admin/src/routes/Community/components/People.js @@ -5,6 +5,7 @@ import Table from '../containers/Table'; import {Pager, Icon} from 'coral-ui'; import EmptyCard from '../../../components/EmptyCard'; import t from 'coral-framework/services/i18n'; +import PropTypes from 'prop-types'; const tableHeaders = [ { @@ -62,4 +63,12 @@ const People = ({commenters, searchValue, onSearchChange, ...props}) => { ); }; +People.propTypes = { + commenters: PropTypes.array, + searchValue: PropTypes.string, + onSearchChange: PropTypes.func, + totalPages: PropTypes.number, + onNewPageHandler: PropTypes.func, +}; + export default People; diff --git a/client/coral-admin/src/routes/Community/components/Table.css b/client/coral-admin/src/routes/Community/components/Table.css index affc6c0e2..65eda5b4c 100644 --- a/client/coral-admin/src/routes/Community/components/Table.css +++ b/client/coral-admin/src/routes/Community/components/Table.css @@ -19,6 +19,13 @@ } } +.username, .email { + max-width: 215px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .email { display: block; } diff --git a/client/coral-admin/src/routes/Community/components/Table.js b/client/coral-admin/src/routes/Community/components/Table.js index 18f52592d..28b20b82f 100644 --- a/client/coral-admin/src/routes/Community/components/Table.js +++ b/client/coral-admin/src/routes/Community/components/Table.js @@ -1,9 +1,11 @@ import React from 'react'; -import {SelectField, Option} from 'react-mdl-selectfield'; import styles from '../components/Table.css'; import t from 'coral-framework/services/i18n'; +import PropTypes from 'prop-types'; +import {Dropdown, Option} from 'coral-ui'; +import cn from 'classnames'; -export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onCommenterStatusChange, viewUserDetail}) => ( +const Table = ({headers, commenters, onHeaderClickHandler, onRoleChange, onCommenterStatusChange, viewUserDetail}) => ( @@ -11,6 +13,7 @@ export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onComm @@ -21,36 +24,45 @@ export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onComm {commenters.map((row, i)=> ( ))}
onHeaderClickHandler({field: header.field})}> {header.title}
- + {row.profiles.map(({id}) => id)} {row.created_at} - onCommenterStatusChange(row.id, status)}> - - - + onCommenterStatusChange(row.id, status)}> + - onRoleChange(row.id, role)}> - - - - - +
); + +Table.propTypes = { + headers: PropTypes.array, + commenters: PropTypes.array, + onHeaderClickHandler: PropTypes.func, + onRoleChange: PropTypes.func, + onCommenterStatusChange: PropTypes.func, + viewUserDetail: PropTypes.func, +}; + +export default Table; diff --git a/client/coral-admin/src/routes/Community/containers/Table.js b/client/coral-admin/src/routes/Community/containers/Table.js index 5d4e4914f..086a56638 100644 --- a/client/coral-admin/src/routes/Community/containers/Table.js +++ b/client/coral-admin/src/routes/Community/containers/Table.js @@ -1,10 +1,10 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {compose} from 'react-apollo'; import {setRole, setCommenterStatus} from '../../../actions/community'; import Table from '../components/Table'; import {viewUserDetail} from '../../../actions/userDetail'; +import PropTypes from 'prop-types'; class TableContainer extends Component { @@ -22,6 +22,12 @@ class TableContainer extends Component { } } +TableContainer.propTypes = { + setRole: PropTypes.func, + setCommenterStatus: PropTypes.func, + commenters: PropTypes.array, +}; + const mapStateToProps = (state) => ({ commenters: state.community.accounts, }); @@ -33,7 +39,5 @@ const mapDispatchToProps = (dispatch) => viewUserDetail, }, dispatch); -export default compose( - connect(mapStateToProps, mapDispatchToProps), -)(TableContainer); +export default connect(mapStateToProps, mapDispatchToProps)(TableContainer); diff --git a/client/coral-admin/src/routes/Configure/components/Configure.css b/client/coral-admin/src/routes/Configure/components/Configure.css index 0b7319132..82760acff 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.css +++ b/client/coral-admin/src/routes/Configure/components/Configure.css @@ -1,15 +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 { @@ -17,108 +9,8 @@ width: 234px; } -.mainContent { - width: calc(100% - 300px); - padding: 10px 14px; - box-sizing: border-box; - 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; +.saveBox { + margin-top: 38px; } .changedSave { @@ -126,81 +18,10 @@ color: white; } -.embedInput { - width: 100%; - display: block; - outline: none; - border: 1px solid rgba(0,0,0,.12); - padding: 6px; +.mainContent { + width: calc(100% - 300px); + padding: 10px 14px 80px 14px; box-sizing: border-box; - border-radius: 2px; - margin: 5px auto; - min-height: 175px; - font-size: 14px; - resize: none; + max-width: 718px; } -.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')}

-