Merge branch 'master' into bugs-fix

This commit is contained in:
Kim Gardner
2017-06-22 12:30:49 +01:00
committed by GitHub
67 changed files with 1158 additions and 453 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ const routes = (
<Route path=':id' components={Moderation} />
</Route>
<Route path=':id' components={Moderation} />
<IndexRedirect to='premod' />
<IndexRedirect to='all' />
</Route>
</Route>
</div>
+17 -9
View File
@@ -9,6 +9,7 @@ import {
} from '../constants/assets';
import coralApi from '../../../coral-framework/helpers/request';
import t from 'coral-framework/services/i18n';
/**
* Action disptacher related to assets
@@ -19,12 +20,16 @@ import coralApi from '../../../coral-framework/helpers/request';
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => {
dispatch({type: FETCH_ASSETS_REQUEST});
return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
.then(({result, count}) =>
dispatch({type: FETCH_ASSETS_SUCCESS,
assets: result,
count
}))
.catch((error) => dispatch({type: FETCH_ASSETS_FAILURE, error}));
.then(({result, count}) =>
dispatch({type: FETCH_ASSETS_SUCCESS,
assets: result,
count
}))
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: FETCH_ASSETS_FAILURE, error: errorMessage});
});
};
// Update an asset state
@@ -32,9 +37,12 @@ export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filte
export const updateAssetState = (id, closedAt) => (dispatch) => {
dispatch({type: UPDATE_ASSET_STATE_REQUEST});
return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
.then(() =>
dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
.catch((error) => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error}));
.then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage});
});
};
export const updateAssets = (assets) => (dispatch) => {
+32 -11
View File
@@ -4,6 +4,7 @@ import coralApi from 'coral-framework/helpers/request';
import * as Storage from 'coral-framework/helpers/storage';
import {handleAuthToken} from 'coral-framework/actions/auth';
import {resetWebsocket} from 'coral-framework/services/client';
import t from 'coral-framework/services/i18n';
//==============================================================================
// SIGN IN
@@ -41,13 +42,27 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) =>
dispatch(checkLoginSuccess(user));
})
.catch((error) => {
if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
if (error.translation_key === 'NOT_AUTHORIZED') {
// invalid credentials
dispatch({
type: actions.LOGIN_FAILURE,
message: t('error.email_password')
});
}
else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
dispatch({
type: actions.LOGIN_MAXIMUM_EXCEEDED,
message: error.translation_key
message: t(`error.${error.translation_key}`),
});
} else {
dispatch({type: actions.LOGIN_FAILURE, message: error.translation_key});
dispatch({
type: actions.LOGIN_FAILURE,
message: errorMessage,
});
}
});
};
@@ -56,25 +71,30 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) =>
// FORGOT PASSWORD
//==============================================================================
const forgotPassowordRequest = () => ({
const forgotPasswordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
});
const forgotPassowordSuccess = () => ({
const forgotPasswordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
});
const forgotPassowordFailure = () => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE
const forgotPasswordFailure = (error) => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
error,
});
export const requestPasswordReset = (email) => (dispatch) => {
dispatch(forgotPassowordRequest(email));
dispatch(forgotPasswordRequest(email));
const redirectUri = location.href;
return coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
.then(() => dispatch(forgotPassowordSuccess()))
.catch((error) => dispatch(forgotPassowordFailure(error)));
.then(() => dispatch(forgotPasswordSuccess()))
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(forgotPasswordFailure(errorMessage));
});
};
//==============================================================================
@@ -112,6 +132,7 @@ export const checkLogin = () => (dispatch) => {
})
.catch((error) => {
console.error(error);
dispatch(checkLoginFailure(`${error.translation_key}`));
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(checkLoginFailure(errorMessage));
});
};
+6 -2
View File
@@ -15,6 +15,7 @@ import {
} from '../constants/community';
import coralApi from '../../../coral-framework/helpers/request';
import t from 'coral-framework/services/i18n';
export const fetchAccounts = (query = {}) => (dispatch) => {
@@ -30,7 +31,11 @@ export const fetchAccounts = (query = {}) => (dispatch) => {
totalPages
});
})
.catch((error) => dispatch({type: FETCH_COMMENTERS_FAILURE, error}));
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: FETCH_COMMENTERS_FAILURE, error: errorMessage});
});
};
const requestFetchAccounts = () => ({
@@ -47,7 +52,6 @@ export const newPage = () => ({
});
export const setRole = (id, role) => (dispatch) => {
return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}})
.then(() => {
return dispatch({type: SET_ROLE, id, role});
+11 -5
View File
@@ -2,6 +2,7 @@ import coralApi from 'coral-framework/helpers/request';
import * as actions from '../constants/install';
import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
import t from 'coral-framework/services/i18n';
export const nextStep = () => ({type: actions.NEXT_STEP});
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
@@ -17,7 +18,8 @@ const clearErrors = () => ({type: actions.CLEAR_ERRORS});
const validation = (formData, dispatch, next) => {
if (!(formData != null)) {
return dispatch(hasError());
dispatch(hasError());
return;
}
const validKeys = Object.keys(formData)
@@ -40,7 +42,8 @@ const validation = (formData, dispatch, next) => {
});
if (empty.length) {
return dispatch(hasError());
dispatch(hasError());
return;
}
// RegExp Validation
@@ -59,7 +62,8 @@ const validation = (formData, dispatch, next) => {
});
if (validation.length) {
return dispatch(hasError());
dispatch(hasError());
return;
}
dispatch(clearErrors());
@@ -90,7 +94,8 @@ export const finishInstall = () => (dispatch, getState) => {
})
.catch((error) => {
console.error(error);
dispatch(installFailure(`${error.translation_key}`));
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(installFailure(errorMessage));
});
};
@@ -113,6 +118,7 @@ export const checkInstall = (next) => (dispatch) => {
})
.catch((error) => {
console.error(error);
dispatch(checkInstallFailure(`${error.translation_key}`));
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(checkInstallFailure(errorMessage));
});
};
+7 -2
View File
@@ -1,4 +1,5 @@
import coralApi from '../../../coral-framework/helpers/request';
import t from 'coral-framework/services/i18n';
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED';
@@ -20,7 +21,9 @@ export const fetchSettings = () => (dispatch) => {
dispatch({type: SETTINGS_RECEIVED, settings});
})
.catch((error) => {
dispatch({type: SETTINGS_FETCH_ERROR, error});
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: SETTINGS_FETCH_ERROR, error: errorMessage});
});
};
@@ -49,6 +52,8 @@ export const saveSettingsToServer = () => (dispatch, getState) => {
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
})
.catch((error) => {
dispatch({type: SAVE_SETTINGS_FAILED, error});
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: SAVE_SETTINGS_FAILED, error: errorMessage});
});
};
+16 -3
View File
@@ -1,5 +1,6 @@
import coralApi from '../../../coral-framework/helpers/request';
import * as userTypes from '../constants/users';
import t from 'coral-framework/services/i18n';
/**
* Action disptacher related to users
@@ -10,7 +11,11 @@ export const userStatusUpdate = (status, userId, commentId) => {
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
.catch((error) => dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error}));
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error: errorMessage});
});
};
};
@@ -18,7 +23,11 @@ export const userStatusUpdate = (status, userId, commentId) => {
export const sendNotificationEmail = (userId, subject, body) => {
return (dispatch) => {
return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
.catch((error) => dispatch({type: userTypes.USER_EMAIL_FAILURE, error}));
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: userTypes.USER_EMAIL_FAILURE, error: errorMessage});
});
};
};
@@ -26,6 +35,10 @@ export const sendNotificationEmail = (userId, subject, body) => {
export const enableUsernameEdit = (userId) => {
return (dispatch) => {
return coralApi(`/users/${userId}/username-enable`, {method: 'POST'})
.catch((error) => dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error}));
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error: errorMessage});
});
};
};
@@ -2,7 +2,6 @@ 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 t from 'coral-framework/services/i18n';
import Recaptcha from 'react-recaptcha';
class AdminLogin extends React.Component {
@@ -35,7 +34,7 @@ class AdminLogin extends React.Component {
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
const signInForm = (
<form onSubmit={this.handleSignIn}>
{errorMessage && <Alert>{t(`error.${errorMessage}`)}</Alert>}
{errorMessage && <Alert>{errorMessage}</Alert>}
<TextField
label='Email Address'
value={this.state.email}
@@ -84,12 +84,12 @@ const CoralHeader = ({
<MenuItem onClick={handleLogout}>
{t('configure.sign_out')}
</MenuItem>
<MenuItem>
Talk {`v${process.env.VERSION}`}
</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
</ul>
</div>
</div>
@@ -0,0 +1,16 @@
import React from 'react';
import {Button} from 'coral-ui';
import t from 'coral-framework/services/i18n';
import {withCopyToClipboard} from 'coral-framework/hocs';
class ButtonCopyToClipboard extends React.Component {
render () {
return (
<Button {...this.props} >
{t('common.copy')}
</Button>
);
}
}
export default withCopyToClipboard(ButtonCopyToClipboard);
@@ -192,6 +192,7 @@ export default class Moderation extends Component {
shortcutsNoteVisible={moderation.shortcutsNoteVisible}
open={moderation.modalOpen}
onClose={this.onClose}/>
{moderation.userDetailId && (
<UserDetail
id={moderation.userDetailId}
@@ -203,7 +204,9 @@ export default class Moderation extends Component {
acceptComment={props.acceptComment}
rejectComment={props.rejectComment} />
)}
<StorySearch
assetId={assetId}
moderation={this.props.moderation}
closeSearch={this.closeSearch}
storySearchChange={this.props.storySearchChange}
@@ -9,6 +9,7 @@ import t from 'coral-framework/services/i18n';
import {CSSTransitionGroup} from 'react-transition-group';
class ModerationQueue extends React.Component {
isLoadingMore = false;
static propTypes = {
viewUserDetail: PropTypes.func.isRequired,
@@ -23,7 +24,15 @@ class ModerationQueue extends React.Component {
}
loadMore = () => {
this.props.loadMore(this.props.activeTab);
if (!this.isLoadingMore) {
this.isLoadingMore = true;
this.props.loadMore(this.props.activeTab)
.then(() => this.isLoadingMore = false)
.catch((e) => {
this.isLoadingMore = false;
throw e;
});
}
}
constructor(props) {
@@ -49,12 +49,14 @@
font-weight: 300;
}
.cta {
letter-spacing: 1px;
.cta > a {
display: inline-block;
cursor: pointer;
color: #000;
text-decoration: none;
font-weight: bold;
font-size: 15px;
margin: 0;
height: 50px;
box-sizing: border-box;
font-size: 15px;
font-weight: 500;
@@ -38,7 +38,11 @@ const StorySearch = (props) => {
</Button>
</div>
<div className={styles.results}>
<p className={styles.cta}>Moderate comments on All Stories</p>
{props.assetId &&
<div className={styles.cta}>
<a onClick={props.goToModerateAll}>Moderate comments on All Stories</a>
</div>
}
<div className={styles.storyList}>
{
@@ -87,7 +91,8 @@ StorySearch.propTypes = {
goToStory: PropTypes.func.isRequired,
closeSearch: PropTypes.func.isRequired,
moderation: PropTypes.object.isRequired,
handleSearchChange: PropTypes.func.isRequired
handleSearchChange: PropTypes.func.isRequired,
assetId: PropTypes.string
};
export default StorySearch;
@@ -35,9 +35,8 @@
border: none;
background-color: transparent;
font-size: 16px;
position: absolute;
width: 90%;
outline: none;
width: calc(100% - 90px);
}
.commentStatuses {
@@ -1,19 +1,14 @@
import React, {PropTypes} from 'react';
import {Button, Drawer, Copy} from 'coral-ui';
import styles from './UserDetail.css';
import Slot from 'coral-framework/components/Slot';
import Comment from './Comment';
import styles from './UserDetail.css';
import {Button, Drawer} from 'coral-ui';
import {Slot} from 'coral-framework/components';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import {actionsMap} from '../helpers/moderationQueueActionsMap';
import ClickOutside from 'coral-framework/components/ClickOutside';
export default class UserDetail extends React.Component {
constructor() {
super();
this.state = {
emailCopied: false
};
}
static propTypes = {
id: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired,
@@ -50,16 +45,6 @@ export default class UserDetail extends React.Component {
this.props.changeStatus('rejected');
}
showCopied() {
this.setState({
emailCopied: true
}, () => {
setTimeout(() => this.setState({
emailCopied: false
}), 3000);
});
}
render () {
const {
root: {
@@ -96,92 +81,92 @@ export default class UserDetail extends React.Component {
}
return (
<Drawer handleClickOutside={hideUserDetail}>
<h3>{user.username}</h3>
{profile && <input className={styles.profileEmail} readOnly type="text" ref={(ref) => this.profile = ref} value={profile} />}
<ClickOutside onClickOutside={hideUserDetail}>
<Drawer onClose={hideUserDetail}>
<h3>{user.username}</h3>
<Copy onCopy={() => this.showCopied()} text={profile} className={styles.profileEmail}>
<Button className={styles.copyButton}>
{this.state.emailCopied ? 'Copied!' : 'Copy'}
</Button>
</Copy>
<div>
{profile && <input className={styles.profileEmail} readOnly type="text" ref={(ref) => this.profile = ref} value={profile} />}
<ButtonCopyToClipboard className={styles.copyButton} copyText={profile} />
</div>
<Slot
fill="userProfile"
data={this.props.data}
root={this.props.root}
user={user}
/>
<p className={styles.memberSince}><strong>Member since</strong> {new Date(user.created_at).toLocaleString()}</p>
<hr/>
<p>
<strong>Account summary</strong>
<br/><small className={styles.small}>Data represents the last six months of activity</small>
</p>
<div className={styles.stats}>
<div className={styles.stat}>
<p>Total Comments</p>
<p>{totalComments}</p>
</div>
<div className={styles.stat}>
<p>Reject Rate</p>
<p>{`${(rejectedPercent).toFixed(1)}%`}</p>
</div>
</div>
{
selectedIds.length === 0
? (
<ul className={styles.commentStatuses}>
<li className={tab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
<li className={tab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
</ul>
)
: (
<div className={styles.bulkActionGroup}>
<Button
onClick={bulkAccept}
className={styles.bulkAction}
cStyle='approve'
icon='done'>
</Button>
<Button
onClick={bulkReject}
className={styles.bulkAction}
cStyle='reject'
icon='close'>
</Button>
{`${selectedIds.length} comments selected`}
<Slot
fill="userProfile"
data={this.props.data}
root={this.props.root}
user={user}
/>
<p className={styles.memberSince}><strong>Member since</strong> {new Date(user.created_at).toLocaleString()}</p>
<hr/>
<p>
<strong>Account summary</strong>
<br/><small className={styles.small}>Data represents the last six months of activity</small>
</p>
<div className={styles.stats}>
<div className={styles.stat}>
<p>Total Comments</p>
<p>{totalComments}</p>
</div>
)
}
<div>
<div className={styles.stat}>
<p>Reject Rate</p>
<p>{`${(rejectedPercent).toFixed(1)}%`}</p>
</div>
</div>
{
nodes.map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
const selected = selectedIds.indexOf(comment.id) !== -1;
return <Comment
key={comment.id}
index={i}
comment={comment}
selected={false}
suspectWords={suspectWords}
bannedWords={bannedWords}
viewUserDetail={() => {}}
actions={actionsMap[status]}
showBanUserDialog={showBanUserDialog}
showSuspendUserDialog={showSuspendUserDialog}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selected={selected}
toggleSelect={toggleSelect}
currentAsset={null}
currentUserId={this.props.id}
minimal={true} />;
})
selectedIds.length === 0
? (
<ul className={styles.commentStatuses}>
<li className={tab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
<li className={tab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
</ul>
)
: (
<div className={styles.bulkActionGroup}>
<Button
onClick={bulkAccept}
className={styles.bulkAction}
cStyle='approve'
icon='done'>
</Button>
<Button
onClick={bulkReject}
className={styles.bulkAction}
cStyle='reject'
icon='close'>
</Button>
{`${selectedIds.length} comments selected`}
</div>
)
}
</div>
</Drawer>
<div>
{
nodes.map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
const selected = selectedIds.indexOf(comment.id) !== -1;
return <Comment
key={comment.id}
index={i}
comment={comment}
selected={false}
suspectWords={suspectWords}
bannedWords={bannedWords}
viewUserDetail={() => {}}
actions={actionsMap[status]}
showBanUserDialog={showBanUserDialog}
showSuspendUserDialog={showSuspendUserDialog}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selected={selected}
toggleSelect={toggleSelect}
currentAsset={null}
currentUserId={this.props.id}
minimal={true} />;
})
}
</div>
</Drawer>
</ClickOutside>
);
}
}
@@ -190,8 +190,6 @@ span {
}
&.selected {
max-width: 720px;
max-height: 410px;
}
.context {
@@ -41,7 +41,13 @@ class StorySearchContainer extends React.Component {
goToStory = (id) => {
const {router, closeSearch} = this.props;
router.push(`/admin/moderate/${id}`);
router.push(`/admin/moderate/all/${id}`);
closeSearch();
}
goToModerateAll = () => {
const {router, closeSearch} = this.props;
router.push('/admin/moderate/all');
closeSearch();
}
@@ -50,6 +56,7 @@ class StorySearchContainer extends React.Component {
<StorySearch
search={this.search}
goToStory={this.goToStory}
goToModerateAll={this.goToModerateAll}
handleEsc={this.handleEsc}
handleEnter={this.handleEnter}
searchValue={this.state.searchValue}
@@ -76,6 +76,8 @@ const ActionButton = ({children}) => {
};
export default class Comment extends React.Component {
isLoadingReplies = false;
constructor(props) {
super(props);
@@ -211,14 +213,24 @@ export default class Comment extends React.Component {
}
loadNewReplies = () => {
const {replies, replyCount, id} = this.props.comment;
if (replyCount > replies.nodes.length) {
this.props.loadMore(id).then(() => {
this.setState(resetCursors(this.state, this.props));
});
return;
if (!this.isLoadingReplies) {
this.isLoadingReplies = true;
const {replies, replyCount, id} = this.props.comment;
if (replyCount > replies.nodes.length) {
this.props.loadMore(id)
.then(() => {
this.setState(resetCursors(this.state, this.props));
this.isLoadingReplies = false;
})
.catch((e) => {
this.isLoadingReplies = false;
throw e;
});
return;
}
this.setState(resetCursors);
this.isLoadingReplies = false;
}
this.setState(resetCursors);
};
showReplyBox = () => {
@@ -432,28 +444,28 @@ export default class Comment extends React.Component {
inline
/>
{ (currentUser &&
(comment.user.id === currentUser.id))
{ (currentUser && (comment.user.id === currentUser.id)) &&
/* User can edit/delete their own comment for a short window after posting */
? <span className={cn(styles.topRight)}>
{
commentIsStillEditable(comment) &&
<a
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
}
</span>
/* User can edit/delete their own comment for a short window after posting */
<span className={cn(styles.topRight)}>
{
commentIsStillEditable(comment) &&
<a
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
}
</span>
}
{ (currentUser && (comment.user.id !== currentUser.id)) &&
/* TopRightMenu allows currentUser to ignore other users' comments */
: <span className={cn(styles.topRight, styles.topRightMenu)}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
addNotification={addNotification} />
</span>
<span className={cn(styles.topRight, styles.topRightMenu)}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
addNotification={addNotification} />
</span>
}
{
this.state.isEditing
? <EditableCommentContent
@@ -80,14 +80,17 @@ export class EditableCommentContent extends React.Component {
}
successfullyEdited = true;
} catch (error) {
if (error.translation_key) {
addNotification('error', t(`error.${error.translation_key}`));
} else if (error.networkError) {
addNotification('error', t('error.network_error'));
} else {
addNotification('error', t('edit_comment.unexpected_error'));
throw error;
}
const errors = error.errors || [error];
errors.forEach((e) => {
if (e.translation_key) {
addNotification('error', t(`error.${e.translation_key}`));
} else if (error.networkError) {
addNotification('error', t('error.network_error'));
} else {
addNotification('error', t('edit_comment.unexpected_error'));
console.error(e);
}
});
}
if (successfullyEdited) {
const status = response.data.editComment.comment.status;
@@ -23,15 +23,17 @@ class LoadMore extends React.Component {
}
}
loadMore = () => {
this.initialState = false;
this.props.loadMore();
}
render () {
const {topLevel, moreComments, loadMore, replyCount} = this.props;
const {topLevel, moreComments, replyCount} = this.props;
return moreComments
? <div className='coral-load-more'>
<Button
onClick={() => {
this.initialState = false;
loadMore();
}}>
onClick={this.loadMore}>
{topLevel ? t('framework.view_more_comments') : this.replyCountFormat(replyCount)}
</Button>
</div>
@@ -53,6 +53,8 @@ function invalidateCursor(invalidated, state, props) {
class Stream extends React.Component {
isLoadingMore = false;
constructor(props) {
super(props);
this.state = resetCursors(this.state, props);
@@ -95,6 +97,18 @@ class Stream extends React.Component {
}
};
loadMoreComments = () => {
if (!this.isLoadingMore) {
this.isLoadingMore = true;
this.props.loadMoreComments()
.then(() => this.isLoadingMore = false)
.catch((e) => {
this.isLoadingMore = false;
throw e;
});
}
}
// getVisibileComments returns a list containing comments
// which were authored by current user or comes after the `idCursor`.
getVisibleComments() {
@@ -183,6 +197,7 @@ class Stream extends React.Component {
<SuspendedAccount
canEditName={user && user.canEditName}
editName={editName}
currentUsername={user.username}
/>}
{loggedIn &&
!banned &&
@@ -288,7 +303,7 @@ class Stream extends React.Component {
<LoadMore
topLevel={true}
moreComments={asset.comments.hasNextPage}
loadMore={this.props.loadMoreComments}
loadMore={this.loadMoreComments}
/>
</div>}
</div>
@@ -9,7 +9,8 @@ class SuspendedAccount extends Component {
static propTypes = {
canEditName: PropTypes.bool,
editName: PropTypes.func.isRequired
editName: PropTypes.func.isRequired,
currentUsername: PropTypes.string.isRequired,
}
state = {
@@ -21,7 +22,11 @@ class SuspendedAccount extends Component {
const {editName} = this.props;
const {username} = this.state;
e.preventDefault();
if (validate.username(username)) {
if (username === this.props.currentUsername) {
this.setState({alert: t('error.SAME_USERNAME_PROVIDED')});
}
else if (validate.username(username)) {
editName(username)
.then(() => location.reload())
.catch((error) => {
@@ -0,0 +1,39 @@
import React from 'react';
import ClickOutside from 'coral-framework/components/ClickOutside';
import styles from './Toggleable.css';
import classnames from 'classnames';
const upArrow = <span className={classnames(styles.chevron, styles.up)}></span>;
const downArrow = <span className={classnames(styles.chevron, styles.down)}></span>;
export default class Toggleable extends React.Component {
constructor(props) {
super(props);
this.state = {
isOpen: false
};
}
toggle = () => {
this.setState({isOpen: !this.state.isOpen});
}
close = () => {
this.setState({isOpen: false});
}
render() {
const {children} = this.props;
const {isOpen} = this.state;
return (
<ClickOutside onClickOutside={this.close}>
<span className={styles.Toggleable} tabIndex="0" >
<span className={styles.toggler}
onClick={this.toggle}>{isOpen ? upArrow : downArrow}</span>
{isOpen ? children : null}
</span>
</ClickOutside>
);
}
}
@@ -1,8 +1,6 @@
import React, {PropTypes} from 'react';
import classnames from 'classnames';
import {IgnoreUserWizard} from './IgnoreUserWizard';
import styles from './TopRightMenu.css';
import Toggleable from './Toggleable';
// TopRightMenu appears as a dropdown in the top right of the comment.
// when you click the down cehvron, it expands and shows IgnoreUserWizard
@@ -56,38 +54,6 @@ export class TopRightMenu extends React.Component {
/>
</div>
</Toggleable>
);
}
}
const upArrow = <span className={classnames(styles.chevron, styles.up)}></span>;
const downArrow = <span className={classnames(styles.chevron, styles.down)}></span>;
class Toggleable extends React.Component {
constructor(props) {
super(props);
this.toggle = this.toggle.bind(this);
this.close = this.close.bind(this);
this.state = {
isOpen: false
};
}
toggle() {
this.setState({isOpen: !this.state.isOpen});
}
close() {
this.setState({isOpen: false});
}
render() {
const {children} = this.props;
const {isOpen} = this.state;
return (
// /*onBlur={ this.close } */
<span className={styles.Toggleable} tabIndex="0" >
<span className={styles.toggler}
onClick={this.toggle}>{isOpen ? upArrow : downArrow}</span>
{isOpen ? children : null}
</span>
);
}
}
+5
View File
@@ -73,6 +73,11 @@ function configurePymParent(pymParent, opts) {
window.document.body.appendChild(snackbar);
// Notify embed that there was a click outside.
document.addEventListener('click', () => {
pymParent.sendMessage('click');
}, true);
// Workaround: IOS Safari ignores `width` but respects `min-width` value.
pymParent.el.firstChild.style.width = '1px';
pymParent.el.firstChild.style.minWidth = '100%';
+9 -3
View File
@@ -10,7 +10,7 @@ export const fetchAssetFailure = (error) => ({type: actions.FETCH_ASSET_FAILURE,
const updateAssetSettingsRequest = () => ({type: actions.UPDATE_ASSET_SETTINGS_REQUEST});
const updateAssetSettingsSuccess = (settings) => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings});
const updateAssetSettingsFailure = () => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE});
const updateAssetSettingsFailure = (error) => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE, error});
export const updateConfiguration = (newConfig) => (dispatch, getState) => {
const assetId = getState().asset.toJS().id;
@@ -20,7 +20,10 @@ export const updateConfiguration = (newConfig) => (dispatch, getState) => {
dispatch(addNotification('success', t('framework.success_update_settings')));
dispatch(updateAssetSettingsSuccess(newConfig));
})
.catch((error) => dispatch(updateAssetSettingsFailure(error)));
.catch((error) => {
console.error(error);
dispatch(updateAssetSettingsFailure(error));
});
};
export const updateOpenStream = (closedBody) => (dispatch, getState) => {
@@ -31,7 +34,10 @@ export const updateOpenStream = (closedBody) => (dispatch, getState) => {
dispatch(addNotification('success', t('framework.success_update_settings')));
dispatch(fetchAssetSuccess(closedBody));
})
.catch((error) => dispatch(fetchAssetFailure(error)));
.catch((error) => {
console.error(error);
dispatch(fetchAssetFailure(error));
});
};
const openStream = () => ({type: actions.OPEN_COMMENTS});
+25 -18
View File
@@ -75,7 +75,9 @@ export const createUsername = (userId, formData) => (dispatch) => {
dispatch(updateUsername(formData));
})
.catch((error) => {
dispatch(createUsernameFailure(t(`error.${error.translation_key}`)));
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(createUsernameFailure(errorMessage));
});
};
@@ -139,16 +141,20 @@ export const fetchSignIn = (formData) => {
dispatch(hideSignInDialog());
})
.catch((error) => {
console.error(error);
if (error.metadata) {
// the user might not have a valid email. prompt the user user re-request the confirmation email
dispatch(
signInFailure(t('error.email_not_verified', error.metadata))
);
} else {
} else if (error.translation_key === 'NOT_AUTHORIZED') {
// invalid credentials
dispatch(signInFailure(t('error.email_password')));
dispatch(signInFailure(t('error.email_password'), error.metadata));
} else {
const str = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(signInFailure(str));
}
});
};
@@ -234,12 +240,8 @@ export const fetchSignUp = (formData) => (dispatch, getState) => {
dispatch(signUpSuccess(user));
})
.catch((error) => {
let errorMessage = t(`error.${error.message}`);
// if there is no translation defined, just show the error string
if (errorMessage === `error.${error.message}`) {
errorMessage = error.message;
}
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(signUpFailure(errorMessage));
});
};
@@ -256,8 +258,9 @@ const forgotPasswordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
});
const forgotPasswordFailure = () => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE
const forgotPasswordFailure = (error) => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
error,
});
export const fetchForgotPassword = (email) => (dispatch, getState) => {
@@ -268,7 +271,11 @@ export const fetchForgotPassword = (email) => (dispatch, getState) => {
body: {email, loc: redirectUri}
})
.then(() => dispatch(forgotPasswordSuccess()))
.catch((error) => dispatch(forgotPasswordFailure(error)));
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(forgotPasswordFailure(errorMessage));
});
};
//==============================================================================
@@ -326,7 +333,8 @@ export const checkLogin = () => (dispatch) => {
})
.catch((error) => {
console.error(error);
dispatch(checkLoginFailure(`${error.translation_key}`));
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(checkLoginFailure(errorMessage));
});
};
@@ -360,11 +368,10 @@ export const requestConfirmEmail = (email) => (dispatch, getState) => {
.then(() => {
dispatch(verifyEmailSuccess());
})
.catch((err) => {
// email might have already been verifyed
dispatch(verifyEmailFailure(err));
throw err;
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(verifyEmailFailure(errorMessage));
});
};
+3 -1
View File
@@ -14,6 +14,8 @@ export const editName = (username) => (dispatch) => {
dispatch(addNotification('success', t('framework.success_name_update')));
})
.catch((error) => {
dispatch(editUsernameFailure(t(`error.${error.translation_key}`)));
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(editUsernameFailure(errorMessage));
});
};
@@ -0,0 +1,35 @@
import {Component, cloneElement, Children} from 'react';
import PropTypes from 'prop-types';
import {findDOMNode} from 'react-dom';
import {pym} from 'coral-framework';
export default class ClickOutside extends Component {
static propTypes = {
onClickOutside: PropTypes.func.isRequired
};
domNode = null;
handleClick = (e) => {
const {onClickOutside} = this.props;
if (!e || !this.domNode.contains(e.target)) {
onClickOutside(e);
}
};
componentDidMount() {
this.domNode = findDOMNode(this);
document.addEventListener('click', this.handleClick, true);
pym.onMessage('click', this.handleClick);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClick, true);
pym.messageHandlers.click = pym.messageHandlers.click.filter((h) => h !== this.handleClick);
}
render() {
const {children, onClickOutside: _, ...rest} = this.props;
return cloneElement(Children.only(children), rest);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
export {default as withFragments} from './withFragments';
export {default as withMutation} from './withMutation';
export {default as withQuery} from './withQuery';
export {default as withCopyToClipboard} from './withCopyToClipboard';
@@ -0,0 +1,29 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Clipboard from 'clipboard';
export default (WrappedComponent) => {
class WithCopyToClipboard extends React.Component {
componentDidMount() {
const clipboard = new Clipboard(ReactDOM.findDOMNode(this));
clipboard.on('success', (e) => {
e.clearSelection();
});
}
render() {
const {copyTarget = '', copyText = '', className = '', ...rest} = this.props;
return <WrappedComponent
className={className}
data-clipboard-action="copy"
data-clipboard-text={copyText}
data-clipboard-target={copyTarget}
{...rest}
/>;
}
}
return WithCopyToClipboard;
};
@@ -20,7 +20,7 @@ describe('CommentBox', () => {
});
it('should render the CommentBox appropriately', () => {
expect(render.contains('<div class="CommentBox"')).to.be.truthy;
expect(render.contains('<button class="postCommentButton"')).to.be.truthy;
expect(render.contains('<div class="CommentBox"')).to.be.true;
expect(render.contains('<button class="postCommentButton"')).to.be.true;
});
});
+67 -65
View File
@@ -4,11 +4,11 @@ import t from 'coral-framework/services/i18n';
import {can} from 'coral-framework/services/perms';
import {PopupMenu, Button} from 'coral-ui';
import onClickOutside from 'react-onclickoutside';
import ClickOutside from 'coral-framework/components/ClickOutside';
const name = 'coral-plugin-flags';
class FlagButton extends Component {
export default class FlagButton extends Component {
state = {
showMenu: false,
@@ -128,7 +128,7 @@ class FlagButton extends Component {
this.setState({message: e.target.value});
}
handleClickOutside () {
handleClickOutside = () => {
this.closeMenu();
}
@@ -138,79 +138,81 @@ class FlagButton extends Component {
const flagged = flaggedByCurrentUser || localPost;
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
return <div className={`${name}-container`}>
<button
ref={(ref) => this.flagButton = ref}
onClick={!this.props.banned && !flaggedByCurrentUser && !localPost ? this.onReportClick : null}
className={`${name}-button`}>
{
flagged
? <span className={`${name}-button-text`}>{t('reported')}</span>
: <span className={`${name}-button-text`}>{t('report')}</span>
}
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
style={flagged ? styles.flaggedIcon : {}}
aria-hidden={true}>flag</i>
</button>
{
this.state.showMenu &&
<div className={`${name}-popup`} ref={(ref) => this.popup = ref}>
<PopupMenu>
<div className={`${name}-popup-header`}>{popupMenu.header}</div>
return (
<ClickOutside onClickOutside={this.handleClickOutside}>
<div className={`${name}-container`}>
<button
ref={(ref) => this.flagButton = ref}
onClick={!this.props.banned && !flaggedByCurrentUser && !localPost ? this.onReportClick : null}
className={`${name}-button`}>
{
popupMenu.text &&
<div className={`${name}-popup-text`}>{popupMenu.text}</div>
flagged
? <span className={`${name}-button-text`}>{t('reported')}</span>
: <span className={`${name}-button-text`}>{t('report')}</span>
}
{
popupMenu.options && <form className={`${name}-popup-form`}>
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
style={flagged ? styles.flaggedIcon : {}}
aria-hidden={true}>flag</i>
</button>
{
this.state.showMenu &&
<div className={`${name}-popup`} ref={(ref) => this.popup = ref}>
<PopupMenu>
<div className={`${name}-popup-header`}>{popupMenu.header}</div>
{
popupMenu.options.map((option) =>
<div key={option.val}>
<input
className={`${name}-popup-radio`}
type="radio"
id={option.val}
checked={this.state[popupMenu.sets] === option.val}
onClick={this.onPopupOptionClick(popupMenu.sets)}
value={option.val}/>
<label htmlFor={option.val} className={`${name}-popup-radio-label`}>{option.text}</label><br/>
</div>
)
popupMenu.text &&
<div className={`${name}-popup-text`}>{popupMenu.text}</div>
}
{
this.state.reason && <div>
<label htmlFor={'message'} className={`${name}-popup-radio-label`}>
{t('flag_reason')}
</label><br/>
<textarea
className={`${name}-reason-text`}
id="message"
rows={4}
onChange={this.onNoteTextChange}
value={this.state.message}/>
</div>
popupMenu.options && <form className={`${name}-popup-form`}>
{
popupMenu.options.map((option) =>
<div key={option.val}>
<input
className={`${name}-popup-radio`}
type="radio"
id={option.val}
checked={this.state[popupMenu.sets] === option.val}
onClick={this.onPopupOptionClick(popupMenu.sets)}
value={option.val}/>
<label htmlFor={option.val} className={`${name}-popup-radio-label`}>{option.text}</label><br/>
</div>
)
}
{
this.state.reason && <div>
<label htmlFor={'message'} className={`${name}-popup-radio-label`}>
{t('flag_reason')}
</label><br/>
<textarea
className={`${name}-reason-text`}
id="message"
rows={4}
onChange={this.onNoteTextChange}
value={this.state.message}/>
</div>
}
</form>
}
</form>
}
<div className={`${name}-popup-counter`}>
{this.state.step + 1} of {getPopupMenu.length}
<div className={`${name}-popup-counter`}>
{this.state.step + 1} of {getPopupMenu.length}
</div>
{
popupMenu.button && <Button
className={`${name}-popup-button`}
onClick={this.onPopupContinue}>
{popupMenu.button}
</Button>
}
</PopupMenu>
</div>
{
popupMenu.button && <Button
className={`${name}-popup-button`}
onClick={this.onPopupContinue}>
{popupMenu.button}
</Button>
}
</PopupMenu>
}
</div>
}
</div>;
</ClickOutside>
);
}
}
export default onClickOutside(FlagButton);
const styles = {
flaggedIcon: {
color: '#F00'
+1 -1
View File
@@ -5,7 +5,7 @@ const renderer = new marked.Renderer();
// Set link target to `_parent` to work properly in an embed.
renderer.link = (href, title, text) =>
`<a target="_parent" href="${href}" title="${title}">${text}</a>`;
`<a target="_parent" href="${href}" ${title ? `title="${title}"` : ''}>${text}</a>`;
marked.setOptions({renderer});
@@ -5,7 +5,7 @@ import t from 'coral-framework/services/i18n';
const ModerationLink = (props) => props.isAdmin ? (
<div className={styles.moderationLink}>
<a href={`/admin/moderate/${props.assetId}`} target="_blank">
<a href={`/admin/moderate/all/${props.assetId}`} target="_blank">
{t('moderate_this_stream')}
</a>
</div>
@@ -1,12 +1,12 @@
import React, {PropTypes} from 'react';
import onClickOutside from 'react-onclickoutside';
const name = 'coral-plugin-permalinks';
import {Button} from 'coral-ui';
import styles from './styles.css';
import ClickOutside from 'coral-framework/components/ClickOutside';
import t from 'coral-framework/services/i18n';
class PermalinkButton extends React.Component {
export default class PermalinkButton extends React.Component {
static propTypes = {
articleURL: PropTypes.string.isRequired,
@@ -28,7 +28,7 @@ class PermalinkButton extends React.Component {
this.setState({popoverOpen: !this.state.popoverOpen});
}
handleClickOutside () {
handleClickOutside = () => {
this.setState({popoverOpen: false});
}
@@ -49,33 +49,33 @@ class PermalinkButton extends React.Component {
render () {
const {copySuccessful, copyFailure} = this.state;
return (
<div className={`${name}-container`}>
<button
ref={(ref) => this.linkButton = ref}
onClick={this.toggle}
className={`${name}-button`}>
{t('permalink')}
<i className={`${name}-icon material-icons`} aria-hidden={true}>link</i>
</button>
<div
ref={(ref) => this.popover = ref}
className={`${name}-popover ${styles.container} ${this.state.popoverOpen ? 'active' : ''}`}>
<input
className={`${name}-copy-field`}
type='text'
ref={(input) => this.permalinkInput = input}
value={`${this.props.articleURL}#${this.props.commentId}`}
onChange={() => {}} />
<Button className={`${name}-copy-button ${copySuccessful ? styles.success : ''} ${copyFailure ? styles.failure : ''}`}
onClick={this.copyPermalink} >
{!copyFailure && !copySuccessful && 'Copy'}
{copySuccessful && 'Copied'}
{copyFailure && 'Not supported'}
</Button>
<ClickOutside onClickOutside={this.handleClickOutside}>
<div className={`${name}-container`}>
<button
ref={(ref) => this.linkButton = ref}
onClick={this.toggle}
className={`${name}-button`}>
{t('permalink')}
<i className={`${name}-icon material-icons`} aria-hidden={true}>link</i>
</button>
<div
ref={(ref) => this.popover = ref}
className={`${name}-popover ${styles.container} ${this.state.popoverOpen ? 'active' : ''}`}>
<input
className={`${name}-copy-field`}
type='text'
ref={(input) => this.permalinkInput = input}
value={`${this.props.articleURL}#${this.props.commentId}`}
onChange={() => {}} />
<Button className={`${name}-copy-button ${copySuccessful ? styles.success : ''} ${copyFailure ? styles.failure : ''}`}
onClick={this.copyPermalink} >
{!copyFailure && !copySuccessful && 'Copy'}
{copySuccessful && 'Copied'}
{copyFailure && 'Not supported'}
</Button>
</div>
</div>
</div>
</ClickOutside>
);
}
}
export default onClickOutside(PermalinkButton);
+10
View File
@@ -53,6 +53,16 @@
color: #212121;
}
.type--local:hover {
background: #d6d5d5;
color: #212121;
}
.type--local:active {
background: #cccccc;
color: #212121;
}
.type--facebook {
background-color: #4267b2;
border-color: #4267b2;
+20 -17
View File
@@ -2,20 +2,23 @@ import React from 'react';
import styles from './Button.css';
import Icon from './Icon';
const Button = ({cStyle = 'local', children, className, raised = false, full = false, icon = '', ...props}) => (
<button
className={`
${styles.button}
${styles[`type--${cStyle}`]}
${className}
${full ? styles.full : ''}
${raised ? styles.raised : ''}
`}
{...props}
>
{icon && <Icon name={icon} className={styles.icon} />}
{children}
</button>
);
export default Button;
export default class Button extends React.Component {
render() {
const {cStyle = 'local', children, className, raised = false, full = false, icon = '', ...props} = this.props;
return (
<button
className={`
${styles.button}
${styles[`type--${cStyle}`]}
${className}
${full ? styles.full : ''}
${raised ? styles.raised : ''}
`}
{...props}
>
{icon && <Icon name={icon} className={styles.icon} />}
{children}
</button>
);
}
}
-38
View File
@@ -1,38 +0,0 @@
import React, {Component} from 'react';
import Clipboard from 'clipboard';
export default class Copy extends Component {
constructor(props) {
super(props);
this.refCopyButton = this.refCopyButton.bind(this);
}
componentDidMount() {
const clipboard = new Clipboard(this.copyButtonEl);
clipboard.on('success', (e) => {
this.props.onCopy();
e.clearSelection();
});
}
refCopyButton(button) {
this.copyButtonEl = button;
}
render() {
const {children, target = '', text = ''} = this.props;
return (
<span
ref={this.refCopyButton}
data-clipboard-action="copy"
data-clipboard-text={text}
data-clipboard-target={target}
>
{children}
</span>
);
}
}
+4 -5
View File
@@ -1,11 +1,10 @@
import React, {PropTypes} from 'react';
import styles from './Drawer.css';
import onClickOutside from 'react-onclickoutside';
const Drawer = ({children, handleClickOutside}) => {
const Drawer = ({children, onClose}) => {
return (
<div className={styles.drawer}>
<div className={styles.closeButton} onClick={handleClickOutside}>×</div>
<div className={styles.closeButton} onClick={onClose}>×</div>
<div className={styles.content}>
{children}
</div>
@@ -15,7 +14,7 @@ const Drawer = ({children, handleClickOutside}) => {
Drawer.propTypes = {
active: PropTypes.bool,
handleClickOutside: PropTypes.func.isRequired
onClose: PropTypes.func.isRequired
};
export default onClickOutside(Drawer);
export default Drawer;
-1
View File
@@ -24,4 +24,3 @@ export {default as Option} from './components/Option';
export {default as SnackBar} from './components/SnackBar';
export {default as TextArea} from './components/TextArea';
export {default as Drawer} from './components/Drawer';
export {default as Copy} from './components/Copy';