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..ea13d2fac 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 is provided with love by our friends at Browserstack that allows us to run our tests in real browsers. + + +![](/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/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/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 5faf41ba2..33a0c9c6f 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.css +++ b/client/coral-admin/src/routes/Configure/components/Configure.css @@ -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; - } - } -} 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')}

-