Merge branch 'master' into bugfix-user-state

This commit is contained in:
David Jay
2017-03-01 12:57:40 -05:00
committed by GitHub
48 changed files with 816 additions and 412 deletions
+4 -1
View File
@@ -1,6 +1,9 @@
# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
A commenting platform from [The Coral Project](https://coralproject.net). Talk enters a closed beta in March 2017, but you can download the code for our alpha here. [Read more about Talk here.](https://coralproject.net/products/talk.html)
A commenting platform from [The Coral Project](https://coralproject.net). Talk enters a closed beta in March 2017, but you can download the code for our alpha here. [Read more about Talk here.](https://coralproject.net/products/talk.html)
Third party licenses are available via the `/client/3rdpartylicenses.txt`
endpoint when the server is running with built assets.
## Contributing to Talk
+32 -22
View File
@@ -20,13 +20,17 @@ const validation = (formData, dispatch, next) => {
return dispatch(hasError());
}
const validKeys = Object.keys(formData)
.filter(name => name !== 'domains');
// Required Validation
const empty = Object.keys(formData).filter(name => {
const empty = validKeys
.filter(name => {
const cond = !formData[name].length;
if (cond) {
// Adding Error
// Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
@@ -40,18 +44,19 @@ const validation = (formData, dispatch, next) => {
}
// RegExp Validation
const validation = Object.keys(formData).filter(name => {
const cond = !validate[name](formData[name]);
if (cond) {
const validation = validKeys
.filter(name => {
const cond = !validate[name](formData[name]);
if (cond) {
// Adding Error
dispatch(addError(name, errorMsj[name]));
} else {
dispatch(addError(name, ''));
}
dispatch(addError(name, errorMsj[name]));
} else {
dispatch(addError(name, ''));
}
return cond;
});
return cond;
});
if (validation.length) {
return dispatch(hasError());
@@ -71,22 +76,27 @@ export const submitSettings = () => (dispatch, getState) => {
export const submitUser = () => (dispatch, getState) => {
const userFormData = getState().install.toJS().data.user;
validation(userFormData, dispatch, function() {
const data = getState().install.toJS().data;
dispatch(installRequest());
coralApi('/setup', {method: 'POST', body: data})
.then(result => {
dispatch(installSuccess(result));
dispatch(nextStep());
})
.catch(error => {
console.error(error);
dispatch(installFailure(`${error.translation_key}`));
});
dispatch(nextStep());
});
};
export const finishInstall = () => (dispatch, getState) => {
const data = getState().install.toJS().data;
dispatch(installRequest());
return coralApi('/setup', {method: 'POST', body: data})
.then(() => {
dispatch(installSuccess());
dispatch(nextStep());
})
.catch(error => {
console.error(error);
dispatch(installFailure(`${error.translation_key}`));
});
};
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
const checkInstallSuccess = installed => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
@@ -1,21 +1,16 @@
import React from 'react';
import styles from './ModerationList.css';
import BanUserButton from './BanUserButton';
import {FabButton} from 'coral-ui';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../containers/ModerationQueue/helpers/moderationQueueActionsMap';
const ActionButton = ({type = '', user, ...props}) => {
if (type === 'BAN') {
return <BanUserButton user={user} onClick={() => props.showBanUserDialog(props.user, props.id)} />;
}
const ActionButton = ({type = '', ...props}) => {
return (
<FabButton
<Button
className={`${type.toLowerCase()} ${styles.actionButton}`}
cStyle={type.toLowerCase()}
icon={menuActionsMap[type].icon}
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
/>
>{menuActionsMap[type].text}</Button>
);
};
@@ -1,6 +1,7 @@
.banButton {
width: 114px;
letter-spacing: 1px;
-webkit-transform: scale(.8);
transform: scale(.8);
margin: 0;
i {
vertical-align: middle;
@@ -8,7 +8,7 @@ const lang = new I18n(translations);
const BanUserButton = ({user, ...props}) => (
<div className={styles.ban}>
<Button cStyle='darkGrey'
<Button cStyle='ban'
className={`ban ${styles.banButton}`}
disabled={user.status === 'BANNED' ? 'disabled' : ''}
onClick={props.onClick}
@@ -19,6 +19,7 @@
background: #E5E5E5;
height: 100%;
width: 128px;
z-index: 10;
}
.base {
@@ -10,6 +10,7 @@ export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
export const INSTALL_FAILURE = 'INSTALL_FAILURE';
export const UPDATE_FORMDATA_USER = 'UPDATE_FORMDATA_USER';
export const UPDATE_FORMDATA_SETTINGS = 'UPDATE_FORMDATA_SETTINGS';
export const UPDATE_PERMITTED_DOMAINS_SETTINGS = 'UPDATE_PERMITTED_DOMAINS_SETTINGS';
export const CHECK_INSTALL_REQUEST = 'CHECK_INSTALL_REQUEST';
export const CHECK_INSTALL_SUCCESS = 'CHECK_INSTALL_SUCCESS';
@@ -1,26 +1,28 @@
import React from 'react';
import {Card} from 'coral-ui';
import styles from './Configure.css';
import TagsInput from 'react-tagsinput';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import TagsInput from 'react-tagsinput';
import styles from './Configure.css';
import {Card} from 'coral-ui';
const lang = new I18n(translations);
const Domainlist = ({domains, onChangeDomainlist}) => (
<div>
<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>
);
const Domainlist = ({domains, onChangeDomainlist}) => {
return (
<div>
<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>
);
};
export default Domainlist;
const lang = new I18n(translations);
@@ -5,19 +5,22 @@ import {Wizard, WizardNav} from 'coral-ui';
import {Layout} from '../../components/ui/Layout';
import {
nextStep,
previousStep,
goToStep,
nextStep,
submitUser,
checkInstall,
previousStep,
finishInstall,
submitSettings,
updateUserFormData,
updateSettingsFormData,
submitSettings,
submitUser,
checkInstall
updatePermittedDomains
} from '../../actions/install';
import InitialStep from './components/Steps/InitialStep';
import AddOrganizationName from './components/Steps/AddOrganizationName';
import CreateYourAccount from './components/Steps/CreateYourAccount';
import PermittedDomainsStep from './components/Steps/PermittedDomainsStep';
import FinalStep from './components/Steps/FinalStep';
class InstallContainer extends Component {
@@ -43,6 +46,7 @@ class InstallContainer extends Component {
<InitialStep/>
<AddOrganizationName/>
<CreateYourAccount/>
<PermittedDomainsStep/>
<FinalStep/>
</Wizard>
</div>
@@ -65,10 +69,14 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
checkInstall: next => dispatch(checkInstall(next)),
nextStep: () => dispatch(nextStep()),
previousStep: () => dispatch(previousStep()),
goToStep: step => dispatch(goToStep(step)),
previousStep: () => dispatch(previousStep()),
finishInstall: () => dispatch(finishInstall()),
checkInstall: next => dispatch(checkInstall(next)),
handleDomainsChange: value => {
dispatch(updatePermittedDomains(value));
},
handleSettingsChange: e => {
const {name, value} = e.currentTarget;
dispatch(updateSettingsFormData(name, value));
@@ -2,25 +2,26 @@ import React from 'react';
import styles from './style.css';
import {TextField, Button} from 'coral-ui';
const lang = new I18n(translations);
import translations from '../../translations.json';
import I18n from 'coral-framework/modules/i18n/i18n';
const AddOrganizationName = props => {
const {handleSettingsChange, handleSettingsSubmit, install} = props;
return (
<div className={styles.step}>
<p>
Please tell us the name of your organization. This will appear in emails when
inviting new team members
</p>
<p>{lang.t('ADD_ORGANIZATION.DESCRIPTION')}</p>
<div className={styles.form}>
<form onSubmit={handleSettingsSubmit}>
<TextField
className={styles.TextField}
id="organizationName"
type="text"
label='Organization name'
label={lang.t('ADD_ORGANIZATION.LABEL')}
onChange={handleSettingsChange}
showErrors={install.showErrors}
errorMsg={install.errors.organizationName} />
<Button type="submit" cStyle='black' full>Save</Button>
<Button type="submit" cStyle='black' full>{lang.t('ADD_ORGANIZATION.SAVE')}</Button>
</form>
</div>
</div>
@@ -2,6 +2,10 @@ import React from 'react';
import styles from './style.css';
import {TextField, Button, Spinner} from 'coral-ui';
const lang = new I18n(translations);
import translations from '../../translations.json';
import I18n from 'coral-framework/modules/i18n/i18n';
const InitialStep = props => {
const {handleUserChange, handleUserSubmit, install} = props;
return (
@@ -12,7 +16,7 @@ const InitialStep = props => {
className={styles.textField}
id="email"
type="email"
label='Email address'
label={lang.t('CREATE.EMAIL')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.email}
@@ -23,7 +27,7 @@ const InitialStep = props => {
className={styles.textField}
id="username"
type="text"
label='Username'
label={lang.t('CREATE.USERNAME')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.username}
@@ -33,7 +37,7 @@ const InitialStep = props => {
className={styles.textField}
id="password"
type="password"
label='Password'
label={lang.t('CREATE.PASSWORD')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.password}
@@ -43,7 +47,7 @@ const InitialStep = props => {
className={styles.textField}
id="confirmPassword"
type="password"
label='Confirm Password'
label={lang.t('CREATE.CONFIRM_PASSWORD')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.confirmPassword}
@@ -51,7 +55,7 @@ const InitialStep = props => {
{
!props.install.isLoading ?
<Button cStyle='black' type="submit" full>Save</Button>
<Button cStyle='black' type="submit" full>{lang.t('CREATE.SAVE')}</Button>
:
<Spinner />
}
@@ -3,16 +3,16 @@ import styles from './style.css';
import {Button} from 'coral-ui';
import {Link} from 'react-router';
const lang = new I18n(translations);
import translations from '../../translations.json';
import I18n from 'coral-framework/modules/i18n/i18n';
const InitialStep = () => {
return (
<div className={`${styles.step} ${styles.finalStep}`}>
<p>
Thanks for installing Talk! We sent an email to verify your email
address. While you finish setting the account, you can start engaging
with your readers now.
</p>
<Button raised><Link to='/admin'>Launch Talk</Link></Button>
<Button cStyle='black' raised><a href="http://coralproject.net">Close this Installer</a></Button>
<p>{lang.t('FINAL.DESCRIPTION')}</p>
<Button raised><Link to='/admin'>{lang.t('FINAL.LAUNCH')}</Link></Button>
<Button cStyle='black' raised><a href="http://coralproject.net">{lang.t('FINAL.CLOSE')}</a></Button>
</div>
);
};
@@ -2,16 +2,16 @@ import React from 'react';
import styles from './style.css';
import {Button} from 'coral-ui';
const lang = new I18n(translations);
import translations from '../../translations.json';
import I18n from 'coral-framework/modules/i18n/i18n';
const InitialStep = props => {
const {nextStep} = props;
return (
<div className={styles.step}>
<p>
The remainder of the Talk installation will take about ten minutes.
Once you complete the following two steps, you will have a free
installation and provision of Mongo and Redis.
</p>
<Button cStyle='green' onClick={nextStep} raised>Get Started</Button>
<p>{lang.t('INITIAL.DESCRIPTION')}</p>
<Button cStyle='green' onClick={nextStep} raised>{lang.t('INITIAL.SUBMIT')}</Button>
</div>
);
};
@@ -0,0 +1,31 @@
import React from 'react';
import styles from './style.css';
import {Button, Card} from 'coral-ui';
import TagsInput from 'react-tagsinput';
const lang = new I18n(translations);
import translations from '../../translations.json';
import I18n from 'coral-framework/modules/i18n/i18n';
const PermittedDomainsStep = props => {
const {finishInstall, install, handleDomainsChange} = props;
const domains = install.data.settings.domains.whitelist;
return (
<div className={styles.step}>
<h3>{lang.t('PERMITTED_DOMAINS.TITLE')}</h3>
<Card className={styles.card}>
<p>{lang.t('PERMITTED_DOMAINS.DESCRIPTION')}</p>
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => handleDomainsChange(tags)}
/>
</Card>
<Button cStyle='green' onClick={finishInstall} raised>{lang.t('PERMITTED_DOMAINS.SUBMIT')}</Button>
</div>
);
};
export default PermittedDomainsStep;
@@ -59,6 +59,12 @@
}
}
}
.card {
max-width: 500px;
margin: 20px auto;
text-align: left;
}
}
.finalStep {
@@ -0,0 +1,58 @@
{
"en": {
"INITIAL" : {
"DESCRIPTION": "Let's set up your Talk community in just a few short steps.",
"SUBMIT": "Get Started"
},
"ADD_ORGANIZATION": {
"DESCRIPTION": "Please tell us the name of your organization. This will appear in emails when inviting new team members.",
"LABEL": "Organization Name",
"SAVE": "Save"
},
"CREATE": {
"EMAIL": "Email address",
"USERNAME": "Username",
"PASSWORD": "Password",
"CONFIRM_PASSWORD": "Confirm Password",
"SAVE": "Save"
},
"PERMITTED_DOMAINS": {
"TITLE": "Permitted domains",
"DESCRIPTION": "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).",
"SUBMIT": "Finish install"
},
"FINAL": {
"DESCRIPTION": "Thanks for installing Talk! We sent an email to verify your email address. While you finish setting up the account, you can start engaging with your readers now.",
"LAUNCH": "Launch Talk",
"CLOSE": "Close this Installer"
}
},
"es": {
"INITIAL" : {
"DESCRIPTION": "Configuremos tu comunidad de Talk en sólo algunos pasos.",
"SUBMIT": "Empezá!"
},
"ADD_ORGANIZATION": {
"DESCRIPTION": "Por favor, dinos el nombre de tu organización. Este aparecerá en los emails cuando invites nuevos miembros.",
"LABEL": "Nombre de la Organización",
"SAVE": "Guardar"
},
"CREATE": {
"EMAIL": "Dirección de E-Mail",
"USERNAME": "Usuario",
"PASSWORD": "Contraseña",
"CONFIRM_PASSWORD": "Confirmar contraseña",
"SAVE": "Guardar"
},
"PERMITTED_DOMAINS": {
"TITLE": "Dominios Permitidos",
"DESCRIPTION": "Agrega dominios permitidos a Talk, e.g. tu localhost, staging y ambientes de production (ex. localhost:3000, staging.domain.com, domain.com).",
"SUBMIT": "Finalizar instalación"
},
"FINAL": {
"DESCRIPTION": "Gracias por instalar Talk! Te enviamos un email para verificar tu identidad. Mientras se termina de configurar la cuenta, ya puedes empezar a interactuar con tus lectores",
"LAUNCH": "Lanzar Talk",
"CLOSE": "Cerrar este instalador"
}
}
}
@@ -80,6 +80,7 @@ class ModerationContainer extends Component {
<ModerationQueue
currentAsset={asset}
comments={comments}
activeTab={activeTab}
suspectWords={settings.wordlist.suspect}
showBanUserDialog={props.showBanUserDialog}
acceptComment={props.acceptComment}
@@ -19,6 +19,7 @@ const ModerationQueue = ({comments, ...props}) => {
key={i}
index={i}
comment={comment}
commentType={props.activeTab}
suspectWords={props.suspectWords}
actions={actionsMap[status]}
showBanUserDialog={props.showBanUserDialog}
@@ -6,8 +6,10 @@ import {Link} from 'react-router';
import styles from './styles.css';
import {Icon} from 'coral-ui';
import ActionButton from '../../../components/ActionButton';
import FlagBox from './FlagBox';
import CommentType from './CommentType';
import ActionButton from 'coral-admin/src/components/ActionButton';
import BanUserButton from 'coral-admin/src/components/BanUserButton';
const linkify = new Linkify();
@@ -21,46 +23,48 @@ const Comment = ({actions = [], ...props}) => {
return (
<li tabIndex={props.index}
className={`mdl-card mdl-shadow--2dp ${styles.Comment} ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<div className={styles.itemHeader}>
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<span>{props.comment.user.name}</span>
<span className={styles.created}>
{timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
</span>
{props.comment.action_summaries ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
{timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
</span>
<BanUserButton user={props.comment.user} onClick={() => props.showBanUserDialog(props.comment.user, props.comment.id)} />
<CommentType type={props.commentType} />
</div>
<div className={styles.sideActions}>
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={`actions ${styles.actions}`}>
{actions.map((action, i) =>
<ActionButton key={i}
type={action}
user={props.comment.user}
acceptComment={() => props.acceptComment({commentId: props.comment.id})}
rejectComment={() => props.rejectComment({commentId: props.comment.id})}
showBanUserDialog={() => props.showBanUserDialog(props.comment.user, props.comment.id)}
/>
)}
</div>
<div className={`actions ${styles.actions}`}>
{actions.map((action, i) =>
<ActionButton key={i}
type={action}
user={props.comment.user}
acceptComment={() => props.acceptComment({commentId: props.comment.id})}
rejectComment={() => props.rejectComment({commentId: props.comment.id})}
/>
)}
</div>
{props.comment.user.status === 'banned' ?
<span className={styles.banned}>
<Icon name='error_outline'/>
<Icon name='error_outline'/>
{lang.t('comment.banned_user')}
</span>
: null}
</span>
: null}
</div>
</div>
{!props.currentAsset && (
<div className={styles.moderateArticle}>
Article: {props.comment.asset.title} <Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate Article</Link>
{!props.currentAsset && (
<div className={styles.moderateArticle}>
Story: {props.comment.asset.title} <Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate &rarr;</Link>
</div>
)}
<div className={styles.itemBody}>
<p className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
<Highlighter searchWords={props.suspectWords} textToHighlight={props.comment.body}/>
</Linkify>
</p>
</div>
)}
<div className={styles.itemBody}>
<p className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
<Highlighter searchWords={props.suspectWords} textToHighlight={props.comment.body}/>
</Linkify>
</p>
</div>
{actionSummaries && <FlagBox actionSummaries={actionSummaries} />}
</li>
@@ -72,7 +76,6 @@ Comment.propTypes = {
rejectComment: PropTypes.func.isRequired,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
currentAsset: PropTypes.object,
isActive: PropTypes.bool.isRequired,
comment: PropTypes.shape({
body: PropTypes.string.isRequired,
action_summaries: PropTypes.array,
@@ -0,0 +1,33 @@
.commentType {
position: absolute;
right: 15px;
top: 11px;
color: white;
background: grey;
padding: 2px 13px;
font-size: 14px;
height: 32px;
box-sizing: border-box;
line-height: 29px;
padding-left: 26px;
i {
font-size: 14px;
position: absolute;
left: 6px;
top: 8px;
margin: 0;
}
&.premod {
background: #063B9A;
}
&.flagged {
background: #d03235;
}
&.no-type {
display: none;
}
}
@@ -0,0 +1,30 @@
import React, {PropTypes} from 'react';
import styles from './CommentType.css';
import {Icon} from 'coral-ui';
const CommentType = props => {
const typeData = getTypeData(props.type);
return (
<span className={`${styles.commentType} ${styles[typeData.className]}`}>
<Icon name={typeData.icon}/>{typeData.text}
</span>
);
};
const getTypeData = type => {
switch (type) {
case 'premod':
return {icon: 'query_builder', text: 'Pre-Mod', className: 'premod'};
case 'flagged':
return {icon: 'flag', text: 'Flagged', className: 'flagged'};
default:
return {icon: 'flag', className: 'no-type'};
}
};
CommentType.propTypes = {
type: PropTypes.string.isRequired
};
export default CommentType;
@@ -0,0 +1,55 @@
.flagBox {
border-top: 1px solid rgba(66, 66, 66, 0.12);
.container {
padding: 0 14px;
}
.detail {
padding: 0 20px 16px;
ul {
padding: 0;
list-style: none;
font-size: 12px;
font-weight: 500;
}
}
.header {
position: relative;
.moreDetail {
float: right;
font-size: 12px;
font-weight: 500;
margin-right: 10px;
margin-top: 8px;
color: black;
&:hover {
opacity: 0.9;
cursor: pointer;
}
}
i {
vertical-align: middle;
font-size: 12px;
}
ul {
vertical-align: middle;
list-style: none;
display: inline-block;
padding: 0;
margin-left: 10px;
font-size: 12px;
}
}
h3 {
vertical-align: middle;
margin: 0;
font-weight: 500;
display: inline-block;
margin-left: 7px;
font-size: 12px;
}
}
@@ -1,16 +1,47 @@
import React, {PropTypes} from 'react';
import styles from './styles.css';
import React, {Component, PropTypes} from 'react';
import {Icon} from 'coral-ui';
import styles from './FlagBox.css';
const FlagBox = props => (
<div className={styles.flagBox}>
<h3>Flags:</h3>
<ul>
{props.actionSummaries.map((action, i) =>
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
)}
</ul>
</div>
);
class FlagBox extends Component {
constructor () {
super();
this.state = {
showDetail: false
};
}
toggleDetail = () => {
this.setState((state) => ({
showDetail: !state.showDetail
}));
}
render() {
const {props} = this;
return (
<div className={styles.flagBox}>
<div className={styles.container}>
<div className={styles.header}>
<Icon name='flag'/><h3>Flags ({props.actionSummaries.length}):</h3>
<ul>
{props.actionSummaries.map((action, i) =>
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
)}
</ul>
{/* <a onClick={this.toggleDetail} className={styles.moreDetail}>More detail</a>*/}
</div>
{this.state.showDetail && (<div className={styles.detail}>
<ul>
{props.actionSummaries.map((action, i) =>
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
)}
</ul>
</div>)}
</div>
</div>
);
}
}
FlagBox.propTypes = {
actionSummaries: PropTypes.array.isRequired
@@ -162,16 +162,21 @@ span {
.listItem {
border-bottom: 1px solid #e0e0e0;
font-size: 16px;
font-size: 18px;
width: 100%;
max-width: 660px;
min-width: 400px;
margin: 0 auto;
padding: 16px 14px;
position: relative;
transition: box-shadow 200ms;
margin-top: 0;
padding: 4px 0 0;
min-height: 220px;
.container {
padding: 0 14px;
min-height: 220px;
}
&:hover {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
@@ -194,7 +199,7 @@ span {
right: 0;
height: 100%;
top: 0;
padding: 40px 18px;
padding: 100px 12px 65px;
box-sizing: border-box;
}
@@ -213,6 +218,8 @@ span {
.itemBody {
display: flex;
justify-content: space-between;
margin-top: 15px;
font-size: 16px;
}
.avatar {
@@ -228,7 +235,7 @@ span {
.created {
color: #666;
font-size: 13px;
margin-left: 40px;
margin-left: 15px;
}
.actionButton {
@@ -310,20 +317,26 @@ span {
}
.Comment {
.moderateArticle {
font-size: 12px;
font-size: 15px;
margin-top: 14px;
font-weight: 500;
a {
display: inline-block;
color: #679af3;
color: #063b9a;
text-decoration: none;
font-size: 1em;
font-weight: 400;
font-weight: 500;
letter-spacing: .5px;
font-size: 12px;
margin-left: 10px;
font-size: 13px;
margin-left: 5px;
padding-bottom: 0px;
border-bottom: solid 1px;
line-height: 16px;
&:hover {
text-decoration: underline;
opacity: .9;
cursor: pointer;
}
@@ -331,16 +344,6 @@ span {
}
}
.flagBox {
max-width: 480px;
border-top: 1px solid rgba(66, 66, 66, 0.12);
h3 {
font-size: 14px;
margin: 0;
font-weight: 500;
}
}
.selectField {
position: relative;
width: 140px;
@@ -1,13 +1,14 @@
export const actionsMap = {
PREMOD: ['REJECT', 'APPROVE', 'BAN'],
FLAGGED: ['REJECT', 'APPROVE', 'BAN'],
REJECTED: ['APPROVE']
PREMOD: ['APPROVE', 'REJECT'],
FLAGGED: ['APPROVE', 'REJECT'],
REJECTED: ['APPROVE', 'REJECTED']
};
export const menuActionsMap = {
'REJECT': {status: 'REJECTED', icon: 'close', key: 'r'},
'APPROVE': {status: 'ACCEPTED', icon: 'done', key: 't'},
'FLAGGED': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
'BAN': {status: 'BANNED', icon: 'not interested'},
'REJECT': {status: 'REJECTED', text: 'Reject', icon: 'close', key: 'r'},
'REJECTED': {status: 'REJECTED', text: 'Rejected', icon: 'close'},
'APPROVE': {status: 'ACCEPTED', text: 'Approve', icon: 'done', key: 't'},
'FLAGGED': {status: 'FLAGGED', text: 'Flag', icon: 'flag', filter: 'Untouched'},
'BAN': {status: 'BANNED', text: 'Ban User', icon: 'not interested'},
'': {icon: 'done'}
};
+12 -2
View File
@@ -1,4 +1,4 @@
import {Map} from 'immutable';
import {Map, List} from 'immutable';
import * as actions from '../constants/install';
@@ -6,7 +6,10 @@ const initialState = Map({
isLoading: false,
data: Map({
settings: Map({
organizationName: ''
organizationName: '',
domains: Map({
whitelist: List()
})
}),
user: Map({
username: '',
@@ -33,6 +36,10 @@ const initialState = Map({
{
text: '2. Create your account',
step: 2
},
{
text: '3. Domain Whitelist',
step: 3
}],
installRequest: null,
installRequestError: null,
@@ -50,6 +57,9 @@ export default function install (state = initialState, action) {
case actions.GO_TO_STEP:
return state
.set('step', action.step);
case actions.UPDATE_PERMITTED_DOMAINS_SETTINGS:
return state
.setIn(['data', 'settings', 'domains', 'whitelist'], action.value);
case actions.UPDATE_FORMDATA_SETTINGS:
return state
.setIn(['data', 'settings', action.name], action.value);
@@ -2,6 +2,7 @@ import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
export const client = new ApolloClient({
addTypename: true,
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
+2 -2
View File
@@ -86,7 +86,7 @@
"comment-count-text-post": " characters.",
"comment-count-error": "Please enter a valid number.",
"domain-list-title": "Domain Whitelist",
"domain-list-text": "Some instructions on how to type the urls."
"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": {
"ban_user": "Ban User?",
@@ -207,7 +207,7 @@
"comment-count-text-post": " caracteres",
"comment-count-error": "Por favor escribe un número válido.",
"domain-list-title": "Lista de Dominios Permitidos",
"domain-list-text": "Instrucciones de como ingresar las URLs."
"domain-list-text": "Agrega dominios permitidos a Talk, e.g. tu localhost, staging y ambientes de production (ex. localhost:3000, staging.domain.com, domain.com)."
},
"embedlink": {
"copy": "Copiar"
+6 -1
View File
@@ -43,6 +43,7 @@ class Comment extends React.Component {
postLike: PropTypes.func.isRequired,
deleteAction: PropTypes.func.isRequired,
parentId: PropTypes.string,
highlighted: PropTypes.string,
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
depth: PropTypes.number.isRequired,
@@ -87,6 +88,7 @@ class Comment extends React.Component {
addNotification,
showSignInDialog,
postLike,
highlighted,
postFlag,
postDontAgree,
loadMore,
@@ -98,10 +100,12 @@ class Comment extends React.Component {
const like = getActionSummary('LikeActionSummary', comment);
const flag = getActionSummary('FlagActionSummary', comment);
const dontagree = getActionSummary('DontAgreeActionSummary', comment);
let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`;
commentClass += highlighted === comment.id ? ' highlighted-comment' : '';
return (
<div
className={parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`}
className={commentClass}
id={`c_${comment.id}`}
style={{marginLeft: depth * 30}}>
<hr aria-hidden={true} />
@@ -163,6 +167,7 @@ class Comment extends React.Component {
postItem={postItem}
depth={depth + 1}
asset={asset}
highlighted={highlighted}
currentUser={currentUser}
postLike={postLike}
postFlag={postFlag}
+45 -19
View File
@@ -31,12 +31,13 @@ import ChangeUsernameContainer from '../../coral-sign-in/containers/ChangeUserna
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer';
import Comment from './Comment';
import LoadMore from './LoadMore';
import NewCount from './NewCount';
class Embed extends Component {
state = {activeTab: 0, showSignInDialog: false};
state = {activeTab: 0, showSignInDialog: false, activeReplyBox: ''};
changeTab = (tab) => {
@@ -59,23 +60,6 @@ class Embed extends Component {
componentDidMount () {
pym.sendMessage('childReady');
pym.onMessage('DOMContentLoaded', hash => {
const commentId = hash.replace('#', 'c_');
let count = 0;
const interval = setInterval(() => {
if (document.getElementById(commentId)) {
window.clearInterval(interval);
pym.scrollParentToChildEl(commentId);
}
if (++count > 100) { // ~10 seconds
// give up waiting for the comments to load.
// it would be weird for the page to jump after that long.
window.clearInterval(interval);
}
}, 100);
});
}
componentWillReceiveProps (nextProps) {
@@ -85,11 +69,29 @@ class Embed extends Component {
}
}
componentDidUpdate(prevProps) {
if(!isEqual(prevProps.data.comment, this.props.data.comment)) {
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
setTimeout(()=>pym.scrollParentToChildEl(`c_${this.props.data.comment.id}`), 0);
}
}
setActiveReplyBox (reactKey) {
if (!this.props.currentUser) {
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
this.props.showSignInDialog(offset);
} else {
this.setState({activeReplyBox: reactKey});
}
}
render () {
const {activeTab} = this.state;
const {closedAt, countCache = {}} = this.props.asset;
const {loading, asset, refetch} = this.props.data;
const {loading, asset, refetch, comment} = this.props.data;
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
const highlightedComment = comment && comment.parent ? comment.parent : comment;
const openStream = closedAt === null;
@@ -162,6 +164,28 @@ class Embed extends Component {
refetch={refetch}
offset={signInOffset}/>}
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} offset={signInOffset} user={user} />}
{
highlightedComment &&
<Comment
refetch={refetch}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={this.props.postItem}
asset={asset}
currentUser={user}
highlighted={comment.id}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
key={highlightedComment.id}
reactKey={highlightedComment.id}
comment={highlightedComment} />
}
<NewCount
commentCount={asset.commentCount}
countCache={countCache[asset.id]}
@@ -173,6 +197,8 @@ class Embed extends Component {
<Stream
addNotification={this.props.addNotification}
postItem={this.props.postItem}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
asset={asset}
currentUser={user}
postLike={this.props.postLike}
+2 -12
View File
@@ -18,7 +18,6 @@ class Stream extends React.Component {
constructor(props) {
super(props);
this.state = {activeReplyBox: '', countPoll: null};
this.setActiveReplyBox = this.setActiveReplyBox.bind(this);
}
componentDidMount() {
@@ -41,15 +40,6 @@ class Stream extends React.Component {
clearInterval(this.state.countPoll);
}
setActiveReplyBox (reactKey) {
if (!this.props.currentUser) {
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
this.props.showSignInDialog(offset);
} else {
this.setState({activeReplyBox: reactKey});
}
}
render () {
const {
comments,
@@ -70,8 +60,8 @@ class Stream extends React.Component {
{
comments.map(comment =>
<Comment
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
setActiveReplyBox={this.props.setActiveReplyBox}
activeReplyBox={this.props.activeReplyBox}
addNotification={addNotification}
depth={0}
postItem={postItem}
@@ -180,6 +180,11 @@ hr {
float: right;
}
.highlighted-comment {
padding-left: 10px;
border-left: 3px solid rgb(35,118,216);
}
/* Tag Labels */
.coral-plugin-tag-label {
File diff suppressed because one or more lines are too long
+125
View File
@@ -0,0 +1,125 @@
import pym from 'pym.js';
// This function should return value of window.Coral
const Coral = {};
const Talk = Coral.Talk = {};
// build the URL to load in the pym iframe
function buildStreamIframeUrl(talkBaseUrl, asset, comment) {
let iframeArray = [
talkBaseUrl,
(talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/'
'embed/stream?asset_url=',
encodeURIComponent(asset)
];
if (comment) {
iframeArray.push('&comment_id=');
iframeArray.push(encodeURIComponent(comment));
}
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) {
let notificationOffset = 200;
let ready = false;
// Resize parent iframe height when child height changes
pymParent.onMessage('height', function(height) {
// TODO: In local testing, this is firing nonstop. Maybe there's a bug on the inside?
// Or it's by design of pym... but that's very wasteful of CPU and DOM reflows (jank)
pymParent.el.querySelector('iframe').height = `${height }px`;
});
// Helps child show notifications at the right scrollTop
pymParent.onMessage('getPosition', function() {
let position = viewport().height + document.body.scrollTop;
if (position > notificationOffset) {
position = position - notificationOffset;
}
pymParent.sendMessage('position', position);
});
// Tell child when parent's DOMContentLoaded
pymParent.onMessage('childReady', function () {
const interval = setInterval(function () {
if (ready) {
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);
}
}, 100);
});
// When end-user clicks link in iframe, open it in parent context
pymParent.onMessage('navigate', function (url) {
window.open(url, '_blank').focus();
});
// wait till images and other iframes are loaded before scrolling the page.
// or do we want to be more aggressive and scroll when we hit DOM ready?
document.addEventListener('DOMContentLoaded', function () {
ready = true;
});
// get dimensions of viewport
const viewport = () => {
let e = window, a = 'inner';
if ( !( 'innerWidth' in window ) ){
a = 'client';
e = document.documentElement || document.body;
}
return {
width : e[`${a}Width`],
height : e[`${a}Height`]
};
};
}
/**
* Render a Talk stream
* @param {HTMLElement} el - Element to render the stream in
* @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
*/
Talk.render = function (el, opts) {
if ( ! el) {
throw new Error('Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.');
}
if (typeof el !== 'object') {
throw new Error(`Coral.Talk.render() expected HTMLElement but got ${el} (${typeof el})`);
}
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) {
throw new Error('Coral.Talk.render() expects opts.talk as the Talk Base URL');
}
// ensure el has an id, as pym can't directly accept the HTMLElement
if (!el.id) {
el.id = `_${Math.random()}`;
}
let asset = opts.asset || window.location.href.split('#')[0];
let comment = window.location.hash.slice(1);
let pymParent = new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, asset, comment), {
title: opts.title,
asset_url: asset,
id: `${el.id}_iframe`,
name: `${el.id}_iframe`
});
configurePymParent(pymParent, asset);
};
export default Coral;
@@ -0,0 +1,13 @@
#import "../fragments/commentView.graphql"
query commentQuery($id: ID!) {
comment(id: $id) {
...commentView
parent {
...commentView
replies {
...commentView
}
}
}
}
@@ -87,7 +87,8 @@ 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')
asset_url: getQueryVariable('asset_url'),
comment_id: getQueryVariable('comment_id')
}
}),
props: ({data}) => ({
@@ -1,6 +1,15 @@
#import "../fragments/commentView.graphql"
query AssetQuery($asset_url: String!) {
query AssetQuery($asset_url: String!, $comment_id: ID!) {
comment(id: $comment_id) {
...commentView
parent {
...commentView
replies {
...commentView
}
}
}
asset(url: $asset_url) {
id
title
+76
View File
@@ -110,6 +110,82 @@
background: #00a291;
}
.type--approve {
display: block;
color: #519954;
border: solid 2px rgba(81, 153, 84, 0.75);
background: white;
padding: 10px 12px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 128px;
&:hover {
box-shadow: none;
color: white;
background-color: #519954;
}
}
.type--reject, .type--rejected {
display: block;
color: #D03235;
border: solid 1px #D03235;
background: white;
padding: 10px 11px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 128px;
&:hover {
color: white;
background-color: #D03235;
box-shadow: none;
}
}
.type--rejected {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: not-allowed;
}
.type--ban {
display: block;
color: #616161;
border: solid 2px rgba(97, 97, 97, 0.77);
background: white;
padding: 10px 12px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 128px;
&:hover {
box-shadow: none;
color: white;
background-color: #616161;
}
}
.full {
width: 100%;
margin: 0;
+1
View File
@@ -13,6 +13,7 @@
position: relative;
padding-left: 40px;
border-radius: 1px;
min-height: 24px;
&:hover {
cursor: pointer;
+22 -10
View File
@@ -39,7 +39,7 @@ const getCountsByAssetID = (context, asset_ids) => {
/**
* Returns the comment count for all comments that are public based on their
* parent ids.
* @param {Object} context graph context
*
* @param {Array<String>} parent_ids the ids of parents for which there are
* comments that we want to get
*/
@@ -271,18 +271,30 @@ const genRecentComments = (_, ids) => {
};
/**
* Returns the comment's by their id.
* genComments returns the comments by the id's. Only admins can see non-public comments.
* @param {Object} context graph context
* @param {Array<String>} ids the comment id's to fetch
* @return {Promise} resolves to the comments
*/
const genCommentsByID = (context, ids) => {
return CommentModel.find({
id: {
$in: ids
}
})
.then(util.singleJoinBy(ids, 'id'));
const genComments = ({user}, ids) => {
let comments;
if (user && user.hasRoles('ADMIN')) {
comments = CommentModel.find({
id: {
$in: ids
}
});
} else {
comments = CommentModel.find({
id: {
$in: ids
},
status: {
$in: ['NONE', 'ACCEPTED']
}
});
}
return comments.then(util.singleJoinBy(ids, 'id'));
};
/**
@@ -292,7 +304,7 @@ const genCommentsByID = (context, ids) => {
*/
module.exports = (context) => ({
Comments: {
get: new DataLoader((ids) => genCommentsByID(context, ids)),
get: new DataLoader((ids) => genComments(context, ids)),
getByQuery: (query) => getCommentsByQuery(context, query),
getCountByQuery: (query) => getCommentCountByQuery(context, query),
countByAssetID: new util.SharedCacheDataLoader('Comments.countByAssetID', 3600, (ids) => getCountsByAssetID(context, ids)),
+7
View File
@@ -1,4 +1,11 @@
const Comment = {
parent({parent_id}, _, {loaders: {Comments}}) {
if (parent_id == null) {
return null;
}
return Comments.get.load(parent_id);
},
user({author_id}, _, {loaders: {Users}}) {
return Users.getByID.load(author_id);
},
+3 -1
View File
@@ -38,7 +38,9 @@ const RootQuery = {
return Comments.getByQuery(query);
},
comment(_, {id}, {loaders: {Comments}}) {
return Comments.get.load(id);
},
commentCount(_, {query: {action_type, statuses, asset_id, parent_id}}, {user, loaders: {Actions, Comments}}) {
if (user == null || !user.hasRoles('ADMIN')) {
return null;
+6
View File
@@ -149,6 +149,9 @@ input CommentCountQuery {
# Comment is the base representation of user interaction in Talk.
type Comment {
# The parent of the comment (if there is one).
parent: Comment
# The ID of the comment.
id: ID!
@@ -477,6 +480,9 @@ type RootQuery {
# Site wide settings and defaults.
settings: Settings
# Finds a specific comment based on it's id.
comment(id: ID!): Comment
# All assets. Requires the `ADMIN` role.
assets: [Asset]
+3 -2
View File
@@ -6,8 +6,8 @@
"scripts": {
"start": "./bin/cli serve --jobs",
"dev-start": "nodemon --config .nodemon.json --exec \"./bin/cli -c .env serve --jobs\"",
"build": "NODE_ENV=production webpack -p --config webpack.config.js --bail",
"build-watch": "NODE_ENV=development webpack --config webpack.config.js --watch",
"build": "NODE_ENV=production webpack --progress -p --config webpack.config.js --bail",
"build-watch": "NODE_ENV=development webpack --progress --config webpack.config.js --watch",
"lint": "eslint bin/* .",
"lint-fix": "eslint bin/* . --fix",
"test": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
@@ -130,6 +130,7 @@
"jsdom": "^9.8.3",
"json-loader": "^0.5.4",
"keymaster": "^1.6.2",
"license-webpack-plugin": "^0.4.2",
"material-design-lite": "^1.2.1",
"mocha": "^3.1.2",
"mocha-junit-reporter": "^1.12.1",
+13 -9
View File
@@ -5,16 +5,20 @@ const router = express.Router();
router.use('/api/v1', require('./api'));
router.use('/admin', require('./admin'));
router.use('/embed', require('./embed'));
router.get('/embed.js', (req, res) => res.sendFile(path.join(__dirname, '../client/coral-embed/index.js')));
router.use('/assets', require('./assets'));
router.get('/embed.js', (req, res) => res.sendFile(path.join(__dirname, '../dist/embed.js')));
router.get('/embed.js.map', (req, res) => res.sendFile(path.join(__dirname, '../dist/embed.js.map')));
router.get('/', (req, res) => {
return res.render('article', {
title: 'Coral Talk',
asset_url: '',
body: '',
basePath: '/client/embed/stream'
if (process.env.NODE_ENV !== 'production') {
router.use('/assets', require('./assets'));
router.get('/', (req, res) => {
return res.render('article', {
title: 'Coral Talk',
asset_url: '',
body: '',
basePath: '/client/embed/stream'
});
});
});
}
module.exports = router;
+22 -4
View File
@@ -2,6 +2,7 @@ const path = require('path');
const autoprefixer = require('autoprefixer');
const precss = require('precss');
const Copy = require('copy-webpack-plugin');
const LicenseWebpackPlugin = require('license-webpack-plugin');
const webpack = require('webpack');
// Edit the build targets and embeds below.
@@ -17,7 +18,12 @@ const buildEmbeds = [
module.exports = {
devtool: 'cheap-source-map',
entry: buildTargets.reduce((entry, target) => {
entry: Object.assign({}, {
'embed': [
'babel-polyfill',
path.join(__dirname, 'client/coral-embed/src/index')
]
}, buildTargets.reduce((entry, target) => {
// Add the entry for the bundle.
entry[`${target}/bundle`] = [
@@ -26,7 +32,7 @@ module.exports = {
];
return entry;
}, buildEmbeds.reduce((entry, embed) => {
}, {}), buildEmbeds.reduce((entry, embed) => {
// Add the entry for the bundle.
entry[`embed/${embed}/bundle`] = [
@@ -38,14 +44,22 @@ module.exports = {
}, {})),
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
publicPath: '/client/',
filename: '[name].js',
// NOTE: this causes all exports to override the global.Coral, so no more
// than one bundle.js can be included on a page.
library: 'Coral'
},
module: {
rules: [
{
loader: 'babel-loader',
exclude: /node_modules/,
test: /\.js$/
test: /\.js$/,
query: {
cacheDirectory: true
}
},
{
loader: 'json-loader',
@@ -80,6 +94,10 @@ module.exports = {
]
},
plugins: [
new LicenseWebpackPlugin({
pattern: /^(MIT|ISC|BSD.*)$/,
addUrl: true
}),
new Copy([
...buildEmbeds.map(embed => ({
from: path.join(__dirname, 'client', `coral-embed-${embed}`, 'style'),
+6
View File
@@ -4385,6 +4385,12 @@ libqp@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/libqp/-/libqp-1.1.0.tgz#f5e6e06ad74b794fb5b5b66988bf728ef1dedbe8"
license-webpack-plugin@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-0.4.2.tgz#188df5418a72ab61cad648b9ffd9355be270242b"
dependencies:
object-assign "^4.1.0"
linkify-it@^1.2.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-1.2.4.tgz#0773526c317c8fd13bd534ee1d180ff88abf881a"