mirror of
https://github.com/wassname/talk.git
synced 2026-07-05 19:42:06 +08:00
Merge branch 'master' into 138617379_best_comments
This commit is contained in:
+2
-2
@@ -1,7 +1,7 @@
|
||||
FROM node:7.6
|
||||
|
||||
# Install yarn
|
||||
RUN npm install -g yarn
|
||||
# Add node-gyp for bcrypt build support
|
||||
RUN yarn global add node-gyp
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
import * as actions from '../constants/auth';
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
import coralApi from 'coral-framework/helpers/response';
|
||||
|
||||
// Log In.
|
||||
export const handleLogin = (email, password) => dispatch => {
|
||||
dispatch({type: actions.LOGIN_REQUEST});
|
||||
return coralApi('/auth/local', {method: 'POST', body: {email, password}})
|
||||
.then(result => {
|
||||
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(checkLoginSuccess(result.user, isAdmin));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch({type: actions.LOGIN_FAILURE, message: error.translation_key});
|
||||
});
|
||||
};
|
||||
|
||||
const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUEST});
|
||||
const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS});
|
||||
const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE});
|
||||
|
||||
export const requestPasswordReset = email => dispatch => {
|
||||
dispatch(forgotPassowordRequest(email));
|
||||
return coralApi('/account/password/reset', {method: 'POST', body: {email}})
|
||||
.then(() => dispatch(forgotPassowordSuccess()))
|
||||
.catch(error => dispatch(forgotPassowordFailure(error)));
|
||||
};
|
||||
|
||||
// Check Login
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
import styles from './NotFound.css';
|
||||
import {Button, TextField, Alert, Success} from 'coral-ui';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
class AdminLogin extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {email: '', password: '', requestPassword: false};
|
||||
}
|
||||
|
||||
handleSignIn = e => {
|
||||
e.preventDefault();
|
||||
this.props.handleLogin(this.state.email, this.state.password);
|
||||
}
|
||||
|
||||
handleRequestPassword = e => {
|
||||
e.preventDefault();
|
||||
this.props.requestPasswordReset(this.state.email);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {errorMessage} = this.props;
|
||||
const signInForm = (
|
||||
<form onSubmit={this.handleSignIn}>
|
||||
{errorMessage && <Alert>{lang.t(`errors.${errorMessage}`)}</Alert>}
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({email: e.target.value})} />
|
||||
<TextField
|
||||
label='Password'
|
||||
value={this.state.password}
|
||||
onChange={e => this.setState({password: e.target.value})}
|
||||
type='password' />
|
||||
<div style={{height: 10}}></div>
|
||||
<Button
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
full
|
||||
onClick={this.handleSignIn}>Sign In</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={e => {
|
||||
e.preventDefault();
|
||||
this.setState({requestPassword: true});
|
||||
}}>Request a new one.</a>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
const requestPasswordForm = (
|
||||
this.props.passwordRequestSuccess
|
||||
? <p className={styles.passwordRequestSuccess} onClick={() => {
|
||||
location.href = location.href;
|
||||
}}>
|
||||
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
|
||||
<Success />
|
||||
</p>
|
||||
: <form onSubmit={this.handleRequestPassword}>
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({email: e.target.value})} />
|
||||
<Button
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
full
|
||||
onClick={this.handleRequestPassword}>Reset Password</Button>
|
||||
</form>
|
||||
);
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={styles.loginLayout}>
|
||||
<h1 className={styles.loginHeader}>Team sign in</h1>
|
||||
<p className={styles.loginCTA}>Sign in to interact with your community.</p>
|
||||
{ this.state.requestPassword ? requestPasswordForm : signInForm }
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AdminLogin.propTypes = {
|
||||
handleLogin: PropTypes.func.isRequired,
|
||||
passwordRequestSuccess: PropTypes.string,
|
||||
loginError: PropTypes.string
|
||||
};
|
||||
|
||||
export default AdminLogin;
|
||||
@@ -1,12 +1,37 @@
|
||||
.layout {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loginLayout {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loginHeader, .loginCTA, .forgotPasswordCTA, .passwordRequestSuccess {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.forgotPasswordLink, .signInLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgotPasswordLink:hover, .signInLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.layout img {
|
||||
width: 100%;
|
||||
.loginHeader {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
cursor: pointer;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Layout} from 'react-mdl';
|
||||
import styles from './NotFound.css';
|
||||
|
||||
export const PermissionRequired = () => (
|
||||
<Layout fixedDrawer>
|
||||
<div className={styles.layout} >
|
||||
<h1>Permission Required</h1>
|
||||
<p>We’re sorry, but you don’t have access to that page.</p>
|
||||
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Navigation, Drawer} from 'react-mdl';
|
||||
import {IndexLink, Link} from 'react-router';
|
||||
import styles from './Drawer.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
|
||||
export default ({handleLogout, restricted = false}) => (
|
||||
const CoralDrawer = ({handleLogout, restricted = false}) => (
|
||||
<Drawer className={styles.header}>
|
||||
{ !restricted ?
|
||||
<div>
|
||||
@@ -45,5 +45,11 @@ export default ({handleLogout, restricted = false}) => (
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
CoralDrawer.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool // hide app elements from a logged out user
|
||||
};
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default CoralDrawer;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
|
||||
import {Link, IndexLink} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
@@ -6,7 +6,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import {Logo} from './Logo';
|
||||
|
||||
export default ({handleLogout, restricted = false}) => (
|
||||
const CoralHeader = ({handleLogout, restricted = false}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
{
|
||||
@@ -64,4 +64,11 @@ export default ({handleLogout, restricted = false}) => (
|
||||
</Header>
|
||||
);
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool // hide elemnts from a user that's logged out
|
||||
};
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default CoralHeader;
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Layout as LayoutMDL} from 'react-mdl';
|
||||
import Header from './Header';
|
||||
import Drawer from './Drawer';
|
||||
import styles from './Layout.css';
|
||||
|
||||
export const Layout = ({children, ...props}) => (
|
||||
const Layout = ({children, handleLogout = () => {}, restricted = false, ...props}) => (
|
||||
<LayoutMDL fixedDrawer>
|
||||
<Header {...props} />
|
||||
<Drawer {...props} />
|
||||
<div className={styles.layout} >
|
||||
<Header handleLogout={handleLogout} restricted={restricted} {...props} />
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} {...props} />
|
||||
<div className={styles.layout}>
|
||||
{children}
|
||||
</div>
|
||||
</LayoutMDL>
|
||||
);
|
||||
|
||||
Layout.propTypes = {
|
||||
handleLogout: PropTypes.func,
|
||||
restricted: PropTypes.bool // hide elements from a user that's logged out
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
||||
@@ -7,3 +7,11 @@ export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN';
|
||||
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
|
||||
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
|
||||
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
|
||||
|
||||
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
|
||||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
||||
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
|
||||
|
||||
export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
|
||||
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
|
||||
export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
h3 {
|
||||
color: black;
|
||||
font-size: 1.76em;
|
||||
font-size: 1.26em;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,10 @@
|
||||
margin-bottom: 20px;
|
||||
align-items: flex-start;
|
||||
min-height: 100px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.settingsError {
|
||||
@@ -118,18 +122,19 @@
|
||||
}
|
||||
|
||||
#bannedWordlist, #suspectWordlist {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
|
||||
display: block;
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.wordlistHeader {
|
||||
font-weight: bold;
|
||||
font-size:18px;
|
||||
margin-bottom:3px;
|
||||
.customCSSInput {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
padding: 14px;
|
||||
letter-spacing: 0.03em;
|
||||
color: #555;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.enabledSetting {
|
||||
@@ -150,7 +155,7 @@
|
||||
margin-top: 38px;
|
||||
}
|
||||
|
||||
.commentSettingsSection {
|
||||
.settingsSection {
|
||||
padding-bottom: 200px;
|
||||
.action {
|
||||
display: inline-block;
|
||||
|
||||
@@ -8,22 +8,20 @@ import {
|
||||
updateDomainlist
|
||||
} from '../../actions/settings';
|
||||
|
||||
import {Button, List, Item} from 'coral-ui';
|
||||
import {Button, List, Item, Card, Spinner} from 'coral-ui';
|
||||
import styles from './Configure.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import EmbedLink from './EmbedLink';
|
||||
import CommentSettings from './CommentSettings';
|
||||
import Wordlist from './Wordlist';
|
||||
import Domainlist from './Domainlist';
|
||||
import has from 'lodash/has';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
import StreamSettings from './StreamSettings';
|
||||
import ModerationSettings from './ModerationSettings';
|
||||
import TechSettings from './TechSettings';
|
||||
|
||||
class Configure extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeSection: 'comments',
|
||||
activeSection: 'stream',
|
||||
changed: false,
|
||||
errors: {}
|
||||
};
|
||||
@@ -70,40 +68,48 @@ class Configure extends Component {
|
||||
|
||||
getSection (section) {
|
||||
const pageTitle = this.getPageTitle(section);
|
||||
let sectionComponent;
|
||||
switch(section){
|
||||
case 'comments':
|
||||
return <CommentSettings
|
||||
title={pageTitle}
|
||||
fetchingSettings={this.props.fetchingSettings}
|
||||
case 'stream':
|
||||
sectionComponent = <StreamSettings
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate}
|
||||
errors={this.state.errors}
|
||||
settingsError={this.onSettingError}/>;
|
||||
case 'embed':
|
||||
return has(this, 'props.settings.domains.whitelist')
|
||||
? <div>
|
||||
<Domainlist
|
||||
domains={this.props.settings.domains.whitelist}
|
||||
onChangeDomainlist={this.onChangeDomainlist}/>
|
||||
<EmbedLink title={pageTitle} />
|
||||
</div>
|
||||
: <EmbedLink title={pageTitle} />;
|
||||
case 'wordlist':
|
||||
return has(this, 'props.settings.wordlist')
|
||||
? <Wordlist
|
||||
bannedWords={this.props.settings.wordlist.banned}
|
||||
suspectWords={this.props.settings.wordlist.suspect}
|
||||
onChangeWordlist={this.onChangeWordlist} />
|
||||
: <p>loading wordlists</p>;
|
||||
break;
|
||||
case 'moderation':
|
||||
sectionComponent = <ModerationSettings
|
||||
onChangeWordlist={this.onChangeWordlist}
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate} />;
|
||||
break;
|
||||
case 'tech':
|
||||
sectionComponent = <TechSettings
|
||||
onChangeDomainlist={this.onChangeDomainlist}
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate} />;
|
||||
}
|
||||
|
||||
if (this.props.settings.fetchingSettings) {
|
||||
return <Card shadow="4"><Spinner/>Loading settings...</Card>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.settingsSection}>
|
||||
<h3>{pageTitle}</h3>
|
||||
{sectionComponent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getPageTitle (section) {
|
||||
switch(section) {
|
||||
case 'comments':
|
||||
return lang.t('configure.comment-settings');
|
||||
case 'embed':
|
||||
return lang.t('configure.embed-comment-stream');
|
||||
case 'stream':
|
||||
return lang.t('configure.stream-settings');
|
||||
case 'moderation':
|
||||
return lang.t('configure.moderation-settings');
|
||||
case 'tech':
|
||||
return lang.t('configure.tech-settings');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -120,14 +126,14 @@ class Configure extends Component {
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<List onChange={this.changeSection} activeItem={activeSection}>
|
||||
<Item itemId='comments' icon="settings">
|
||||
{lang.t('configure.comment-settings')}
|
||||
<Item itemId='stream' icon='speaker_notes'>
|
||||
{lang.t('configure.stream-settings')}
|
||||
</Item>
|
||||
<Item itemId='embed' icon='code'>
|
||||
{lang.t('configure.embed-comment-stream')}
|
||||
<Item itemId='moderation' icon='thumbs_up_down'>
|
||||
{lang.t('configure.moderation-settings')}
|
||||
</Item>
|
||||
<Item itemId='wordlist' icon='settings'>
|
||||
{lang.t('configure.wordlist')}
|
||||
<Item itemId='tech' icon='code'>
|
||||
{lang.t('configure.tech-settings')}
|
||||
</Item>
|
||||
</List>
|
||||
<div className={styles.saveBox}>
|
||||
|
||||
@@ -9,19 +9,17 @@ const lang = new I18n(translations);
|
||||
|
||||
const Domainlist = ({domains, onChangeDomainlist}) => {
|
||||
return (
|
||||
<div>
|
||||
<Card id={styles.domainlist} className={styles.configSetting}>
|
||||
<h3>{lang.t('configure.domain-list-title')}</h3>
|
||||
<Card id={styles.domainlist}>
|
||||
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
|
||||
<TagsInput
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => onChangeDomainlist('whitelist', tags)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
|
||||
<TagsInput
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => onChangeDomainlist('whitelist', tags)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -44,19 +44,15 @@ class EmbedLink extends Component {
|
||||
"></script>
|
||||
`.trim();
|
||||
return (
|
||||
<div>
|
||||
<h3>{this.props.title}</h3>
|
||||
<div>
|
||||
<Card shadow="2">
|
||||
<p>{lang.t('configure.copy-and-paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
|
||||
{lang.t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<Card shadow="2" className={styles.configSetting}>
|
||||
<h3>Embed Comment Stream</h3>
|
||||
<p>{lang.t('configure.copy-and-paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
|
||||
{lang.t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './Configure.css';
|
||||
import {Card} from 'coral-ui';
|
||||
import {Checkbox} from 'react-mdl';
|
||||
import Wordlist from './Wordlist';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const updateModeration = (updateSettings, mod) => () => {
|
||||
const moderation = mod === 'PRE' ? 'POST' : 'PRE';
|
||||
updateSettings({moderation});
|
||||
};
|
||||
|
||||
const updateEmailConfirmation = (updateSettings, verify) => () => {
|
||||
updateSettings({requireEmailConfirmation: !verify});
|
||||
};
|
||||
|
||||
const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => {
|
||||
|
||||
// just putting this here for shorthand below
|
||||
const on = styles.enabledSetting;
|
||||
const off = styles.disabledSetting;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateEmailConfirmation(updateSettings, settings.requireEmailConfirmation)}
|
||||
checked={settings.requireEmailConfirmation} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.require-email-verification')}</div>
|
||||
<p className={settings.requireEmailConfirmation ? '' : styles.disabledSettingText}>
|
||||
{lang.t('configure.require-email-verification-text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateModeration(updateSettings, settings.moderation)}
|
||||
checked={settings.moderation === 'PRE'} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
|
||||
<p className={settings.moderation === 'PRE' ? '' : styles.disabledSettingText}>
|
||||
{lang.t('configure.enable-pre-moderation-text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Wordlist
|
||||
bannedWords={settings.wordlist.banned}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
onChangeWordlist={onChangeWordlist} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ModerationSettings.propTypes = {
|
||||
onChangeWordlist: PropTypes.func.isRequired,
|
||||
settings: PropTypes.shape({
|
||||
moderation: PropTypes.string.isRequired,
|
||||
wordlist: PropTypes.shape({
|
||||
banned: PropTypes.array.isRequired,
|
||||
suspect: PropTypes.array.isRequired
|
||||
})
|
||||
}).isRequired,
|
||||
updateSettings: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ModerationSettings;
|
||||
+4
-60
@@ -4,7 +4,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
import {Textfield, Checkbox} from 'react-mdl';
|
||||
import {Card, Icon, Spinner} from 'coral-ui';
|
||||
import {Card, Icon} from 'coral-ui';
|
||||
|
||||
const TIMESTAMPS = {
|
||||
weeks: 60 * 60 * 24 * 7,
|
||||
@@ -27,15 +27,6 @@ const updateCharCount = (updateSettings, settingsError) => (event) => {
|
||||
updateSettings({charCount: charCount});
|
||||
};
|
||||
|
||||
const updateModeration = (updateSettings, mod) => () => {
|
||||
const moderation = mod === 'PRE' ? 'POST' : 'PRE';
|
||||
updateSettings({moderation});
|
||||
};
|
||||
|
||||
const updateEmailConfirmation = (updateSettings, verify) => () => {
|
||||
updateSettings({requireEmailConfirmation: !verify});
|
||||
};
|
||||
|
||||
const updateInfoBoxEnable = (updateSettings, infoBox) => () => {
|
||||
const infoBoxEnable = !infoBox;
|
||||
updateSettings({infoBoxEnable});
|
||||
@@ -51,11 +42,6 @@ const updateClosedMessage = (updateSettings) => (event) => {
|
||||
updateSettings({closedMessage});
|
||||
};
|
||||
|
||||
const updateCustomCssUrl = (updateSettings) => (event) => {
|
||||
const customCssUrl = event.target.value;
|
||||
updateSettings({customCssUrl});
|
||||
};
|
||||
|
||||
// If we are changing the measure we need to recalculate using the old amount
|
||||
// Same thing if we are just changing the amount
|
||||
const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
|
||||
@@ -71,44 +57,14 @@ const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
|
||||
}
|
||||
};
|
||||
|
||||
const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError, settings, errors}) => {
|
||||
if (fetchingSettings) {
|
||||
return <Card shadow="4"><Spinner/>Loading settings...</Card>;
|
||||
}
|
||||
const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
|
||||
|
||||
// just putting this here for shorthand below
|
||||
const on = styles.enabledSetting;
|
||||
const off = styles.disabledSetting;
|
||||
|
||||
return (
|
||||
<div className={styles.commentSettingsSection}>
|
||||
<h3>{title}</h3>
|
||||
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateModeration(updateSettings, settings.moderation)}
|
||||
checked={settings.moderation === 'PRE'} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
|
||||
<p className={settings.moderation === 'PRE' ? '' : styles.disabledSettingText}>
|
||||
{lang.t('configure.enable-pre-moderation-text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateEmailConfirmation(updateSettings, settings.requireEmailConfirmation)}
|
||||
checked={settings.requireEmailConfirmation} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.require-email-verification')}</div>
|
||||
<p className={settings.requireEmailConfirmation ? '' : styles.disabledSettingText}>
|
||||
{lang.t('configure.require-email-verification-text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<div>
|
||||
<Card className={`${styles.configSetting} ${settings.charCountEnable ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
@@ -193,23 +149,11 @@ const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={styles.configSettingInfoBox}>
|
||||
<div className={styles.content}>
|
||||
{lang.t('configure.custom-css-url')}
|
||||
<p>{lang.t('configure.custom-css-url-desc')}</p>
|
||||
<br />
|
||||
<Textfield
|
||||
style={{width: '100%'}}
|
||||
label={lang.t('configure.custom-css-url')}
|
||||
value={settings.customCssUrl}
|
||||
onChange={updateCustomCssUrl(updateSettings)} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentSettings;
|
||||
export default StreamSettings;
|
||||
|
||||
// To see if we are talking about weeks, days or hours
|
||||
// We talk the remainder of the division and see if it's 0
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Card} from 'coral-ui';
|
||||
import Domainlist from './Domainlist';
|
||||
import EmbedLink from './EmbedLink';
|
||||
import styles from './Configure.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const updateCustomCssUrl = (updateSettings) => (event) => {
|
||||
const customCssUrl = event.target.value;
|
||||
updateSettings({customCssUrl});
|
||||
};
|
||||
|
||||
const TechSettings = ({settings, onChangeDomainlist, updateSettings}) => {
|
||||
return (
|
||||
<div>
|
||||
<Domainlist
|
||||
domains={settings.domains.whitelist}
|
||||
onChangeDomainlist={onChangeDomainlist} />
|
||||
<EmbedLink />
|
||||
<Card className={styles.configSetting}>
|
||||
<h3>{lang.t('configure.custom-css-url')}</h3>
|
||||
<p>{lang.t('configure.custom-css-url-desc')}</p>
|
||||
<br />
|
||||
<input
|
||||
className={styles.customCSSInput}
|
||||
value={settings.customCssUrl}
|
||||
onChange={updateCustomCssUrl(updateSettings)} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TechSettings.propTypes = {
|
||||
settings: PropTypes.shape({
|
||||
domains: PropTypes.shape({
|
||||
whitelist: PropTypes.array.isRequired
|
||||
})
|
||||
}).isRequired,
|
||||
updateSettings: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default TechSettings;
|
||||
@@ -7,9 +7,8 @@ import {Card} from 'coral-ui';
|
||||
|
||||
const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
|
||||
<div>
|
||||
<h3>{lang.t('configure.banned-words-title')}</h3>
|
||||
<Card id={styles.bannedWordlist}>
|
||||
<p className={styles.wordlistHeader}>{lang.t('configure.banned-word-header')}</p>
|
||||
<Card id={styles.bannedWordlist} className={styles.configSetting}>
|
||||
<h3>{lang.t('configure.banned-words-title')}</h3>
|
||||
<p className={styles.wordlistDesc}>{lang.t('configure.banned-word-text')}</p>
|
||||
<TagsInput
|
||||
value={bannedWords}
|
||||
@@ -19,9 +18,8 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
|
||||
onChange={tags => onChangeWordlist('banned', tags)}
|
||||
/>
|
||||
</Card>
|
||||
<h3>{lang.t('configure.suspect-words-title')}</h3>
|
||||
<Card id={styles.suspectWordlist}>
|
||||
<p className={styles.wordlistHeader}>{lang.t('configure.suspect-word-header')}</p>
|
||||
<Card id={styles.suspectWordlist} className={styles.configSetting}>
|
||||
<h3>{lang.t('configure.suspect-words-title')}</h3>
|
||||
<p className={styles.wordlistDesc}>{lang.t('configure.suspect-word-text')}</p>
|
||||
<TagsInput
|
||||
value={suspectWords}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import styles from './style.css';
|
||||
import {Wizard, WizardNav} from 'coral-ui';
|
||||
import {Layout} from '../../components/ui/Layout';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
|
||||
import {
|
||||
goToStep,
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {Layout} from '../components/ui/Layout';
|
||||
import {checkLogin, logout} from '../actions/auth';
|
||||
import Layout from '../components/ui/Layout';
|
||||
import {checkLogin, handleLogin, logout, requestPasswordReset} from '../actions/auth';
|
||||
import {FullLoading} from '../components/FullLoading';
|
||||
import {PermissionRequired} from '../components/PermissionRequired';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
|
||||
class LayoutContainer extends Component {
|
||||
componentWillMount () {
|
||||
const {checkLogin} = this.props;
|
||||
checkLogin().then(() => {
|
||||
if (!this.props.auth.isAdmin) {
|
||||
location.href = '/admin/login';
|
||||
}
|
||||
});
|
||||
checkLogin();
|
||||
}
|
||||
render () {
|
||||
const {isAdmin, loggedIn, loadingUser} = this.props.auth;
|
||||
const {
|
||||
isAdmin,
|
||||
loggedIn,
|
||||
loadingUser,
|
||||
loginError,
|
||||
passwordRequestSuccess
|
||||
} = this.props.auth;
|
||||
const {handleLogout} = this.props;
|
||||
if (loadingUser) { return <FullLoading />; }
|
||||
if (!isAdmin) { return <PermissionRequired />; }
|
||||
if (isAdmin && loggedIn) { return <Layout {...this.props} />; }
|
||||
if (!isAdmin) {
|
||||
return <AdminLogin
|
||||
handleLogin={this.props.handleLogin}
|
||||
requestPasswordReset={this.props.requestPasswordReset}
|
||||
passwordRequestSuccess={passwordRequestSuccess}
|
||||
errorMessage={loginError} />;
|
||||
}
|
||||
if (isAdmin && loggedIn) { return <Layout handleLogout={handleLogout} {...this.props} />; }
|
||||
return <FullLoading />;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +38,8 @@ const mapStateToProps = state => ({
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
checkLogin: () => dispatch(checkLogin()),
|
||||
handleLogin: (username, password) => dispatch(handleLogin(username, password)),
|
||||
requestPasswordReset: email => dispatch(requestPasswordReset(email)),
|
||||
handleLogout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import * as actions from '../constants/auth';
|
||||
const initialState = Map({
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
isAdmin: false
|
||||
isAdmin: false,
|
||||
loginError: null,
|
||||
passwordRequestSuccess: null
|
||||
});
|
||||
|
||||
export default function auth (state = initialState, action) {
|
||||
@@ -25,6 +27,14 @@ export default function auth (state = initialState, action) {
|
||||
.set('user', action.user);
|
||||
case actions.LOGOUT_SUCCESS:
|
||||
return initialState;
|
||||
case actions.LOGIN_REQUEST:
|
||||
return state.set('loginError', null);
|
||||
case actions.LOGIN_FAILURE:
|
||||
return state.set('loginError', action.message);
|
||||
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
|
||||
return state.set('passwordRequestSuccess', null);
|
||||
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
|
||||
return state.set('passwordRequestSuccess', 'If you have a registered account, a password reset link was sent to that email.');
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function settings (state = initialState, action) {
|
||||
.set('fetchSettingsError', null);
|
||||
case actions.SETTINGS_RECEIVED:
|
||||
return state.merge({
|
||||
fetchingSettings: null,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
});
|
||||
@@ -43,7 +43,7 @@ export default function settings (state = initialState, action) {
|
||||
.set('fetchSettingsError', action.error);
|
||||
case actions.SETTINGS_UPDATED:
|
||||
return state.merge({
|
||||
fetchingSettings: null,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"en": {
|
||||
"errors": {
|
||||
"NOT_AUTHORIZED": "Your username or password is not recognized by our system."
|
||||
},
|
||||
"community": {
|
||||
"username_and_email": "Username and Email",
|
||||
"account_creation_date": "Account Creation Date",
|
||||
@@ -50,6 +53,9 @@
|
||||
"copy": "Copy to Clipboard"
|
||||
},
|
||||
"configure": {
|
||||
"stream-settings": "Stream Settings",
|
||||
"moderation-settings": "Moderation Settings",
|
||||
"tech-settings": "Tech Settings",
|
||||
"custom-css-url": "Custom CSS URL",
|
||||
"custom-css-url-desc": "URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external.",
|
||||
"dashboard": "Dashboard",
|
||||
@@ -62,8 +68,6 @@
|
||||
"include-text": "Include your text here.",
|
||||
"comment-settings": "Settings",
|
||||
"embed-comment-stream": "Embed Stream",
|
||||
"banned-word-header": "Write the banned words list",
|
||||
"suspect-word-header": "Write the suspect words list",
|
||||
"banned-word-text": "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
|
||||
"suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
|
||||
"wordlist": "Banned Words",
|
||||
@@ -85,7 +89,7 @@
|
||||
"comment-count-text-pre": "Comments will be limited to ",
|
||||
"comment-count-text-post": " characters.",
|
||||
"comment-count-error": "Please enter a valid number.",
|
||||
"domain-list-title": "Domain Whitelist",
|
||||
"domain-list-title": "Permitted Domains",
|
||||
"domain-list-text": "Enter the domains you would like to permit for Talk, e.g. your local, staging and production environments (ex. localhost:3000, staging.domain.com, domain.com)."
|
||||
},
|
||||
"bandialog": {
|
||||
@@ -134,6 +138,9 @@
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"errors": {
|
||||
"NOT_AUTHORIZED": "Acción no autorizada."
|
||||
},
|
||||
"community": {
|
||||
"username_and_email": "Usuario y E-mail",
|
||||
"account_creation_date": "Fecha de creación de la cuenta",
|
||||
@@ -171,6 +178,9 @@
|
||||
"username_flags": ""
|
||||
},
|
||||
"configure": {
|
||||
"stream-settings": "Configuración de Comentarios",
|
||||
"moderation-settings": "Configuración de Moderación",
|
||||
"tech-settings": "Configuración Technical",
|
||||
"custom-css-url": "URL CSS a medida",
|
||||
"custom-css-url-desc": "URL de una hoja de estilo que va a sobrescribir los estilos por defecto de Embed Stream. Puede ser interna o externa.",
|
||||
"dashboard": "Panel",
|
||||
@@ -184,8 +194,6 @@
|
||||
"comment-settings": "Configuración de Comentarios",
|
||||
"embed-comment-stream": "Colocar Hilo de Comentarios",
|
||||
"wordlist": "Palabras Suspendidas y Suspechosas",
|
||||
"banned-word-header": "Escribir las palabras no permitidas",
|
||||
"suspect-word-header": "Write the suspect words list",
|
||||
"banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente separadas de los comentarios publicados.",
|
||||
"suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
|
||||
"banned-words-title": "Banned words list",
|
||||
@@ -215,7 +223,7 @@
|
||||
},
|
||||
"bandialog": {
|
||||
"ban_user": "Quieres suspender el Usuario?",
|
||||
"are_you_sure": "Estas segura que quieres suspender a {props.author.username}?",
|
||||
"are_you_sure": "Estas segura que quieres suspender a {0}?",
|
||||
"note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.",
|
||||
"cancel": "Cancelar",
|
||||
"yes_ban_user": "Si, Suspendan el usuario"
|
||||
|
||||
@@ -83,8 +83,8 @@ class Embed extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
setActiveReplyBox (reactKey) {
|
||||
if (!this.props.currentUser) {
|
||||
setActiveReplyBox = (reactKey) => {
|
||||
if (!this.props.auth.user) {
|
||||
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
} else {
|
||||
|
||||
@@ -5,24 +5,30 @@ const Coral = {};
|
||||
const Talk = Coral.Talk = {};
|
||||
|
||||
// build the URL to load in the pym iframe
|
||||
function buildStreamIframeUrl(talkBaseUrl, asset, comment) {
|
||||
function buildStreamIframeUrl(talkBaseUrl, asset_url, comment, asset_id) {
|
||||
let iframeArray = [
|
||||
talkBaseUrl,
|
||||
(talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/'
|
||||
'embed/stream?asset_url=',
|
||||
encodeURIComponent(asset)
|
||||
encodeURIComponent(asset_url)
|
||||
];
|
||||
|
||||
if (comment) {
|
||||
iframeArray.push('&comment_id=');
|
||||
iframeArray.push(encodeURIComponent(comment));
|
||||
}
|
||||
|
||||
if (asset_id) {
|
||||
iframeArray.push('&asset_id=');
|
||||
iframeArray.push(encodeURIComponent(asset_id));
|
||||
}
|
||||
|
||||
return iframeArray.join('');
|
||||
}
|
||||
|
||||
// Set up postMessage listeners/handlers on the pymParent
|
||||
// e.g. to resize the iframe, and navigate the host page
|
||||
function configurePymParent(pymParent, assetUrl) {
|
||||
function configurePymParent(pymParent, asset_url) {
|
||||
let notificationOffset = 200;
|
||||
let ready = false;
|
||||
|
||||
@@ -52,7 +58,7 @@ function configurePymParent(pymParent, assetUrl) {
|
||||
window.clearInterval(interval);
|
||||
|
||||
// @todo - It's weird to me that this is sent here in addition to the iframe URL. Could it just be in one place?
|
||||
pymParent.sendMessage('DOMContentLoaded', assetUrl);
|
||||
pymParent.sendMessage('DOMContentLoaded', asset_url);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
@@ -88,11 +94,11 @@ function configurePymParent(pymParent, assetUrl) {
|
||||
* @param {Object} opts - Configuration options for talk
|
||||
* @param {String} opts.talk - Talk base URL
|
||||
* @param {String} [opts.title] - Title of Stream (rendered in iframe)
|
||||
* @param {String} [opts.asset] - parent Asset ID or URL. Comments in the
|
||||
* stream will replies to this asset
|
||||
* @param {String} [opts.asset_url] - Asset URL
|
||||
* @param {String} [opts.asset_id] - Asset ID
|
||||
*/
|
||||
Talk.render = function (el, opts) {
|
||||
if ( ! el) {
|
||||
if (!el) {
|
||||
throw new Error('Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.');
|
||||
}
|
||||
if (typeof el !== 'object') {
|
||||
@@ -101,7 +107,7 @@ Talk.render = function (el, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
// @todo infer this URL without explicit user input (if possible, may have to be added at build/render time of this script)
|
||||
if (! opts.talk) {
|
||||
if (!opts.talk) {
|
||||
throw new Error('Coral.Talk.render() expects opts.talk as the Talk Base URL');
|
||||
}
|
||||
|
||||
@@ -110,16 +116,23 @@ Talk.render = function (el, opts) {
|
||||
el.id = `_${Math.random()}`;
|
||||
}
|
||||
|
||||
let asset = opts.asset || window.location.href.split('#')[0];
|
||||
let asset_url = opts.asset_url || window.location.href.split('#')[0];
|
||||
let comment = window.location.hash.slice(1);
|
||||
let pymParent = new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, asset, comment), {
|
||||
|
||||
let query = {
|
||||
title: opts.title,
|
||||
asset_url: asset,
|
||||
asset_url: asset_url,
|
||||
id: `${el.id}_iframe`,
|
||||
name: `${el.id}_iframe`
|
||||
});
|
||||
};
|
||||
|
||||
configurePymParent(pymParent, asset);
|
||||
if (opts.asset_id && opts.asset_id.length > 0) {
|
||||
query.asset_id = opts.asset_id;
|
||||
}
|
||||
|
||||
let pymParent = new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, asset_url, comment), query);
|
||||
|
||||
configurePymParent(pymParent, asset_url);
|
||||
};
|
||||
|
||||
export default Coral;
|
||||
|
||||
@@ -42,7 +42,7 @@ export const postComment = graphql(POST_COMMENT, {
|
||||
updateQueries: {
|
||||
AssetQuery: (oldData, {mutationResult:{data:{createComment:{comment}}}}) => {
|
||||
|
||||
if (oldData.asset.moderation === 'PRE') {
|
||||
if (oldData.asset.settings.moderation === 'PRE') {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ function getQueryVariable(variable) {
|
||||
}
|
||||
}
|
||||
|
||||
// If no query is included, return a default string for development
|
||||
return 'http://localhost/default/stream';
|
||||
// If not found, return null.
|
||||
return null;
|
||||
}
|
||||
|
||||
export const getCounts = (data) => ({asset_id, limit, sort}) => {
|
||||
@@ -85,12 +85,19 @@ export const loadMore = (data) => ({limit, cursor, parent_id, asset_id, sort}, n
|
||||
};
|
||||
|
||||
export const queryStream = graphql(STREAM_QUERY, {
|
||||
options: () => ({
|
||||
variables: {
|
||||
asset_url: getQueryVariable('asset_url'),
|
||||
comment_id: getQueryVariable('comment_id')
|
||||
}
|
||||
}),
|
||||
options: () => {
|
||||
let comment_id = getQueryVariable('comment_id');
|
||||
let has_comment = comment_id != null;
|
||||
|
||||
return {
|
||||
variables: {
|
||||
asset_id: getQueryVariable('asset_id'),
|
||||
asset_url: getQueryVariable('asset_url'),
|
||||
comment_id: has_comment ? comment_id : 'no-comment',
|
||||
has_comment
|
||||
}
|
||||
};
|
||||
},
|
||||
props: ({data}) => ({
|
||||
data,
|
||||
loadMore: loadMore(data),
|
||||
|
||||
@@ -3,6 +3,11 @@ query myCommentHistory {
|
||||
comments {
|
||||
id
|
||||
body
|
||||
asset {
|
||||
id
|
||||
title
|
||||
url
|
||||
}
|
||||
created_at
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
query AssetQuery($asset_url: String!, $comment_id: ID!) {
|
||||
comment(id: $comment_id) {
|
||||
query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comment: Boolean!) {
|
||||
comment(id: $comment_id) @include(if: $has_comment) {
|
||||
...commentView
|
||||
parent {
|
||||
...commentView
|
||||
@@ -10,7 +10,7 @@ query AssetQuery($asset_url: String!, $comment_id: ID!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
asset(url: $asset_url) {
|
||||
asset(id: $asset_id, url: $asset_url) {
|
||||
id
|
||||
title
|
||||
url
|
||||
|
||||
@@ -5,7 +5,7 @@ const Comment = props => {
|
||||
return (
|
||||
<div className={styles.myComment}>
|
||||
<p className="myCommentAsset">
|
||||
<a className={`${styles.assetURL} myCommentAnchor`} href='#' onClick={props.link(`${props.asset.url}#${props.comment.id}`)}>{props.asset.url}</a>
|
||||
<a className={`${styles.assetURL} myCommentAnchor`} href='#' onClick={props.link(`${props.asset.url}#${props.comment.id}`)}>{props.asset.title ? props.asset.title : props.asset.url}</a>
|
||||
</p>
|
||||
<p className={`${styles.commentBody} myCommentBody`}>{props.comment.body}</p>
|
||||
</div>
|
||||
@@ -18,7 +18,8 @@ Comment.propTypes = {
|
||||
body: PropTypes.string
|
||||
}).isRequired,
|
||||
asset: PropTypes.shape({
|
||||
url: PropTypes.string
|
||||
url: PropTypes.string,
|
||||
title: PropTypes.string
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const CommentHistory = props => {
|
||||
key={i}
|
||||
comment={comment}
|
||||
link={props.link}
|
||||
asset={props.asset} />;
|
||||
asset={comment.asset} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,8 +19,7 @@ const CommentHistory = props => {
|
||||
};
|
||||
|
||||
CommentHistory.propTypes = {
|
||||
comments: PropTypes.array.isRequired,
|
||||
asset: PropTypes.object.isRequired
|
||||
comments: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default CommentHistory;
|
||||
|
||||
@@ -5,9 +5,9 @@ import translations from '../translations';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default ({showSignInDialog}) => (
|
||||
export default ({showSignInDialog, requireEmailConfirmation}) => (
|
||||
<div className={styles.message}>
|
||||
<SignInContainer noButton={true}/>
|
||||
<SignInContainer noButton={true} requireEmailConfirmation={requireEmailConfirmation}/>
|
||||
<div>
|
||||
<a onClick={() => {
|
||||
showSignInDialog();
|
||||
|
||||
@@ -35,7 +35,7 @@ class ProfileContainer extends Component {
|
||||
const {me} = this.props.data;
|
||||
|
||||
if (!loggedIn || !me) {
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog}/>;
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog} requireEmailConfirmation={asset.settings.requireEmailConfirmation}/>;
|
||||
}
|
||||
|
||||
if (data.loading) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import TextField from 'coral-ui/components/TextField';
|
||||
import Alert from './Alert';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import {Dialog, Alert} from 'coral-ui';
|
||||
import FakeComment from './FakeComment';
|
||||
|
||||
import styles from './styles.css';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Alert from './Alert';
|
||||
import {Button, TextField, Spinner, Success} from 'coral-ui';
|
||||
import {Button, TextField, Spinner, Success, Alert} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Alert from './Alert';
|
||||
import {Button, TextField, Spinner, Success} from 'coral-ui';
|
||||
import {Button, TextField, Spinner, Success, Alert} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
|
||||
@@ -65,23 +65,6 @@ input.error{
|
||||
padding: 3px 0 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.userBox {
|
||||
padding: 10px 0 20px;
|
||||
letter-spacing: 0.1px;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
import styles from './Alert.css';
|
||||
|
||||
const Alert = ({cStyle = 'error', children, className, ...props}) => (
|
||||
<div
|
||||
@@ -1,3 +1,4 @@
|
||||
export {default as Alert} from './components/Alert';
|
||||
export {default as Dialog} from './components/Dialog';
|
||||
export {default as CoralLogo} from './components/CoralLogo';
|
||||
export {default as FabButton} from './components/FabButton';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const errors = require('../../errors');
|
||||
|
||||
const AssetsService = require('../../services/assets');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const CommentsService = require('../../services/comments');
|
||||
|
||||
const Wordlist = require('../../services/wordlist');
|
||||
@@ -146,10 +147,11 @@ const createPublicComment = (context, commentInput) => {
|
||||
// TODO: this is kind of fragile, we should refactor this to resolve
|
||||
// all these const's that we're using like 'COMMENTS', 'FLAG' to be
|
||||
// defined in a checkable schema.
|
||||
return context.mutators.Action.create({
|
||||
return ActionsService.insertUserAction({
|
||||
item_id: comment.id,
|
||||
item_type: 'COMMENTS',
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'Matched suspect word filter',
|
||||
metadata: {}
|
||||
})
|
||||
|
||||
@@ -15,10 +15,6 @@ router.get('/password-reset', (req, res) => {
|
||||
res.render('password-reset', {redirectUri: process.env.TALK_ROOT_URL});
|
||||
});
|
||||
|
||||
router.get('/login', (req, res, next) => {
|
||||
res.render('admin/login');
|
||||
});
|
||||
|
||||
router.get('*', (req, res) => {
|
||||
res.render('admin', {basePath: '/client/coral-admin'});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,10 @@ describe('coral-plugin-history/CommentHistory', () => {
|
||||
'closedAt':null
|
||||
};
|
||||
|
||||
comments.forEach((comment) => {
|
||||
comment.asset = asset;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
render = shallow(<CommentHistory comments={comments} asset={asset} link={()=>{}}/>);
|
||||
});
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<title>Admin Login</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
|
||||
<style media="screen">
|
||||
body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#root form {
|
||||
max-width: 300px;
|
||||
border: 1px solid lightgrey;
|
||||
box-shadow: 0px 10px 24px 2px rgba(0,0,0,0.2);
|
||||
margin: 50px auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 3px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 4px;
|
||||
margin-top: 3px;
|
||||
border: 1px solid lightgrey;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.submit-password-reset {
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
display: block;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error-console {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
border-radius: 4px;
|
||||
background-color: pink;
|
||||
color: red;
|
||||
border: 1px solid red;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.error-console.active {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<form id="login-form">
|
||||
<legend class="legend">Admin Login</legend>
|
||||
<label for="email">
|
||||
Email
|
||||
<input type="email" name="email" placeholder=""/>
|
||||
</label>
|
||||
|
||||
<label for="password">
|
||||
Password
|
||||
<input type="password" name="password" placeholder="" />
|
||||
</label>
|
||||
<button class="submit-password-reset" type="submit">Login</button>
|
||||
<div class="error-console"></div>
|
||||
</form>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
function showError(message) {
|
||||
$('.error-console').text(message).addClass('active');
|
||||
}
|
||||
|
||||
function handleSubmit (e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
var password = $('[name="password"]').val();
|
||||
var email = $('[name="email"]').val();
|
||||
|
||||
$.ajax({
|
||||
url: '/api/v1/auth/local',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': '<%= csrfToken %>'
|
||||
},
|
||||
data: JSON.stringify({password: password, email: email})
|
||||
}).then(function (success) {
|
||||
location.href = '/admin';
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$('#login-form').on('submit', handleSubmit);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+3
-2
@@ -17,7 +17,7 @@ const buildEmbeds = [
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
devtool: 'cheap-source-map',
|
||||
devtool: '#cheap-module-source-map',
|
||||
entry: Object.assign({}, {
|
||||
'embed': [
|
||||
'babel-polyfill',
|
||||
@@ -58,7 +58,8 @@ module.exports = {
|
||||
exclude: /node_modules/,
|
||||
test: /\.js$/,
|
||||
query: {
|
||||
cacheDirectory: true
|
||||
cacheDirectory: true,
|
||||
sourceMap: true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user