mirror of
https://github.com/wassname/talk.git
synced 2026-07-06 05:17:19 +08:00
Merge pull request #608 from coralproject/integrate-admin-graphql-framework
Integrate admin into GraphQL framework
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"basePath": "admin"
|
||||
}
|
||||
@@ -1,25 +1,20 @@
|
||||
import React from 'react';
|
||||
import {Router, Route, IndexRedirect, browserHistory} from 'react-router';
|
||||
|
||||
import Stories from 'containers/Stories/Stories';
|
||||
import Configure from 'containers/Configure/Configure';
|
||||
import LayoutContainer from 'containers/LayoutContainer';
|
||||
import InstallContainer from 'containers/Install/InstallContainer';
|
||||
import Configure from 'routes/Configure';
|
||||
import Dashboard from 'routes/Dashboard';
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import {CommunityLayout, Community} from 'routes/Community';
|
||||
import {ModerationLayout, Moderation} from 'routes/Moderation';
|
||||
|
||||
import CommunityLayout from 'containers/Community/CommunityLayout';
|
||||
import CommunityContainer from 'containers/Community/CommunityContainer';
|
||||
|
||||
import ModerationLayout from 'containers/ModerationQueue/ModerationLayout';
|
||||
import ModerationContainer from 'containers/ModerationQueue/ModerationContainer';
|
||||
|
||||
import Dashboard from 'containers/Dashboard/Dashboard';
|
||||
import Layout from 'containers/Layout';
|
||||
|
||||
const routes = (
|
||||
<div>
|
||||
<Route exact path="/admin/install" component={InstallContainer}/>
|
||||
<Route path='/admin' component={LayoutContainer}>
|
||||
<Route exact path="/admin/install" component={Install}/>
|
||||
<Route path='/admin' component={Layout}>
|
||||
<IndexRedirect to='/admin/moderate/all' />
|
||||
<Route path='community' component={CommunityContainer} />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='stories' component={Stories} />
|
||||
<Route path='dashboard' component={Dashboard} />
|
||||
@@ -27,11 +22,11 @@ const routes = (
|
||||
{/* Community Routes */}
|
||||
|
||||
<Route path='community' component={CommunityLayout}>
|
||||
<Route path='flagged' components={CommunityContainer}>
|
||||
<Route path=':id' components={CommunityContainer} />
|
||||
<Route path='flagged' components={Community}>
|
||||
<Route path=':id' components={Community} />
|
||||
</Route>
|
||||
<Route path='people' components={CommunityContainer}>
|
||||
<Route path=':id' components={CommunityContainer} />
|
||||
<Route path='people' components={Community}>
|
||||
<Route path=':id' components={Community} />
|
||||
</Route>
|
||||
<IndexRedirect to='flagged' />
|
||||
</Route>
|
||||
@@ -39,22 +34,22 @@ const routes = (
|
||||
{/* Moderation Routes */}
|
||||
|
||||
<Route path='moderate' component={ModerationLayout}>
|
||||
<Route path='all' components={ModerationContainer}>
|
||||
<Route path=':id' components={ModerationContainer} />
|
||||
<Route path='all' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='accepted' components={ModerationContainer}>
|
||||
<Route path=':id' components={ModerationContainer} />
|
||||
<Route path='accepted' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='premod' components={ModerationContainer}>
|
||||
<Route path=':id' components={ModerationContainer} />
|
||||
<Route path='premod' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='rejected' components={ModerationContainer}>
|
||||
<Route path=':id' components={ModerationContainer} />
|
||||
<Route path='rejected' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='flagged' components={ModerationContainer}>
|
||||
<Route path=':id' components={ModerationContainer} />
|
||||
<Route path='flagged' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path=':id' components={ModerationContainer} />
|
||||
<Route path=':id' components={Moderation} />
|
||||
<IndexRedirect to='premod' />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -5,7 +5,7 @@ export const singleView = () => ({type: actions.SINGLE_VIEW});
|
||||
|
||||
// Ban User Dialog
|
||||
export const showBanUserDialog = (user, commentId, commentStatus, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, commentStatus, showRejectedNote});
|
||||
export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog});
|
||||
export const hideBanUserDialog = () => ({type: actions.HIDE_BANUSER_DIALOG});
|
||||
|
||||
// Suspend User Dialog
|
||||
export const showSuspendUserDialog = (userId, username, commentId, commentStatus) =>
|
||||
@@ -27,3 +27,9 @@ export const hideShortcutsNote = () => {
|
||||
|
||||
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
|
||||
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
|
||||
|
||||
export const setSortOrder = (order) => ({
|
||||
type: actions.SET_SORT_ORDER,
|
||||
order
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './ModerationList.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../containers/ModerationQueue/helpers/moderationQueueActionsMap';
|
||||
import {menuActionsMap} from '../routes/Moderation/helpers/moderationQueueActionsMap';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
|
||||
@@ -7,3 +7,4 @@ export const SHOW_SUSPEND_USER_DIALOG = 'SHOW_SUSPEND_USER_DIALOG';
|
||||
export const HIDE_SUSPEND_USER_DIALOG = 'HIDE_SUSPEND_USER_DIALOG';
|
||||
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
|
||||
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
|
||||
export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const Loading = () => (
|
||||
<h1> {t('loading_results')}</h1>
|
||||
);
|
||||
|
||||
export default Loading;
|
||||
@@ -1,79 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import styles from './Community.css';
|
||||
import {setRole, setCommenterStatus} from '../../actions/community';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class Table extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.onRoleChange = this.onRoleChange.bind(this);
|
||||
}
|
||||
|
||||
onRoleChange (id, role) {
|
||||
this.props.dispatch(setRole(id, role));
|
||||
}
|
||||
|
||||
onCommenterStatusChange (id, status) {
|
||||
this.props.dispatch(setCommenterStatus(id, status));
|
||||
}
|
||||
|
||||
render () {
|
||||
const {headers, commenters, onHeaderClickHandler} = this.props;
|
||||
|
||||
return (
|
||||
<table className={`mdl-data-table ${styles.dataTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, i) =>(
|
||||
<th
|
||||
key={i}
|
||||
className="mdl-data-table__cell--non-numeric"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{commenters.map((row, i)=> (
|
||||
<tr key={i}>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.username}
|
||||
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.created_at}
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField label={'Select me'} value={row.status || ''}
|
||||
className={styles.selectField}
|
||||
label={t('community.status')}
|
||||
onChange={(status) => this.onCommenterStatusChange(row.id, status)}>
|
||||
<Option value={'ACTIVE'}>{t('community.active')}</Option>
|
||||
<Option value={'BANNED'}>{t('community.banned')}</Option>
|
||||
</SelectField>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField label={'Select me'} value={row.roles[0] || ''}
|
||||
className={styles.selectField}
|
||||
label={t('community.role')}
|
||||
onChange={(role) => this.onRoleChange(row.id, role)}>
|
||||
<Option value={''}>.</Option>
|
||||
<Option value={'STAFF'}>{t('community.staff')}</Option>
|
||||
<Option value={'MODERATOR'}>{t('community.moderator')}</Option>
|
||||
<Option value={'ADMIN'}>{t('community.admin')}</Option>
|
||||
</SelectField>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((state) => ({commenters: state.community.get('accounts')}))(Table);
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Dashboard.css';
|
||||
import {compose} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import {getMetrics} from 'coral-admin/src/graphql/queries';
|
||||
import FlagWidget from './FlagWidget';
|
||||
import ActivityWidget from './ActivityWidget';
|
||||
import CountdownTimer from 'coral-admin/src/components/CountdownTimer';
|
||||
|
||||
import {Spinner} from 'coral-ui';
|
||||
|
||||
class Dashboard extends React.Component {
|
||||
|
||||
reloadData = () => {
|
||||
this.props.data.refetch();
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
if (this.props.data && this.props.data.loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const {data: {assetsByActivity, assetsByFlag}} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CountdownTimer handleTimeout={this.reloadData} />
|
||||
<div className={styles.Dashboard}>
|
||||
<FlagWidget assets={assetsByFlag} />
|
||||
<ActivityWidget assets={assetsByActivity} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
settings: state.settings.toJS(),
|
||||
moderation: state.moderation.toJS()
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
getMetrics
|
||||
)(Dashboard);
|
||||
@@ -1,101 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import styles from './style.css';
|
||||
import {Wizard, WizardNav} from 'coral-ui';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
|
||||
import {
|
||||
goToStep,
|
||||
nextStep,
|
||||
submitUser,
|
||||
checkInstall,
|
||||
previousStep,
|
||||
finishInstall,
|
||||
submitSettings,
|
||||
updateUserFormData,
|
||||
updateSettingsFormData,
|
||||
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 {
|
||||
componentDidMount() {
|
||||
const {checkInstall} = this.props;
|
||||
checkInstall(() => {
|
||||
this.context.router.push('/admin');
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {install} = this.props;
|
||||
|
||||
return (
|
||||
<Layout restricted={true}>
|
||||
<div className={styles.Install}>
|
||||
{
|
||||
!install.alreadyInstalled ? (
|
||||
<div>
|
||||
<h2>Welcome to the Coral Project</h2>
|
||||
{ install.step !== 0 ? <WizardNav items={install.navItems} currentStep={install.step} icon='check'/> : null }
|
||||
<Wizard currentStep={install.step} {...this.props}>
|
||||
<InitialStep/>
|
||||
<AddOrganizationName/>
|
||||
<CreateYourAccount/>
|
||||
<PermittedDomainsStep/>
|
||||
<FinalStep/>
|
||||
</Wizard>
|
||||
</div>
|
||||
) : (
|
||||
<div>Talk is already installed</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InstallContainer.contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
install: state.install.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
nextStep: () => dispatch(nextStep()),
|
||||
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));
|
||||
},
|
||||
handleUserChange: (e) => {
|
||||
const {name, value} = e.currentTarget;
|
||||
dispatch(updateUserFormData(name, value));
|
||||
},
|
||||
handleSettingsSubmit: (e) => {
|
||||
e.preventDefault();
|
||||
dispatch(submitSettings());
|
||||
},
|
||||
handleUserSubmit: (e) => {
|
||||
e.preventDefault();
|
||||
dispatch(submitUser());
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(InstallContainer);
|
||||
@@ -1,185 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import styles from './Stories.css';
|
||||
import {connect} from 'react-redux';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {fetchAssets, updateAssetState} from '../../actions/assets';
|
||||
|
||||
import {Link} from 'react-router';
|
||||
|
||||
import {Pager, Icon} from 'coral-ui';
|
||||
import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl';
|
||||
import EmptyCard from 'coral-admin/src/components/EmptyCard';
|
||||
|
||||
const limit = 25;
|
||||
|
||||
class Stories extends Component {
|
||||
|
||||
state = {
|
||||
search: '',
|
||||
sort: 'desc',
|
||||
filter: 'all',
|
||||
statusMenus: {},
|
||||
timer: null,
|
||||
page: 0
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchAssets(0, limit, '', this.state.sortBy);
|
||||
}
|
||||
|
||||
onSettingChange = (setting) => (e) => {
|
||||
let options = this.state;
|
||||
this.setState({[setting]: e.target.value});
|
||||
options[setting] = e.target.value;
|
||||
this.props.fetchAssets(0, limit, options.search, options.sort, options.filter);
|
||||
}
|
||||
|
||||
onSearchChange = (e) => {
|
||||
const search = e.target.value;
|
||||
this.setState((prevState) => {
|
||||
prevState.search = search;
|
||||
clearTimeout(prevState.timer);
|
||||
const fetchAssets = this.props.fetchAssets;
|
||||
prevState.timer = setTimeout(() => {
|
||||
fetchAssets(0, limit, search, this.state.sort, this.state.filter);
|
||||
}, 350);
|
||||
return prevState;
|
||||
});
|
||||
}
|
||||
|
||||
renderDate = (date) => {
|
||||
const d = new Date(date);
|
||||
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
onStatusClick = (closeStream, id, statusMenuOpen) => () => {
|
||||
if (statusMenuOpen) {
|
||||
this.setState((prev) => {
|
||||
prev.statusMenus[id] = false;
|
||||
return prev;
|
||||
});
|
||||
this.props.updateAssetState(id, closeStream ? Date.now() : null)
|
||||
.then(() => {
|
||||
const {search, sort, filter, page} = this.state;
|
||||
this.props.fetchAssets(page, limit, search, sort, filter);
|
||||
});
|
||||
} else {
|
||||
this.setState((prev) => {
|
||||
prev.statusMenus[id] = true;
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderTitle = (title, {id}) => <Link to={`/admin/moderate/${id}`}>{title}</Link>
|
||||
|
||||
renderStatus = (closedAt, {id}) => {
|
||||
const closed = closedAt && new Date(closedAt).getTime() < Date.now();
|
||||
const statusMenuOpen = this.state.statusMenus[id];
|
||||
return <div className={styles.statusMenu}>
|
||||
<div
|
||||
className={closed ? styles.statusMenuClosed : styles.statusMenuOpen}
|
||||
onClick={this.onStatusClick(closed, id, statusMenuOpen)}>
|
||||
{!statusMenuOpen && <Icon className={styles.statusMenuIcon} name='keyboard_arrow_down'/>}
|
||||
{closed ? t('streams.closed') : t('streams.open')}
|
||||
</div>
|
||||
{
|
||||
statusMenuOpen &&
|
||||
<div
|
||||
className={!closed ? styles.statusMenuClosed : styles.statusMenuOpen}
|
||||
onClick={this.onStatusClick(!closed, id, statusMenuOpen)}>
|
||||
{!closed ? t('streams.closed') : t('streams.open')}
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
|
||||
onPageClick = (page) => {
|
||||
this.setState({page});
|
||||
const {search, sort, filter} = this.state;
|
||||
this.props.fetchAssets((page - 1) * limit, limit, search, sort, filter);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {search, sort, filter} = this.state;
|
||||
const {assets} = this.props;
|
||||
|
||||
const assetsIds = assets.ids.map((id) => assets.byId[id]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<div className={styles.searchBox}>
|
||||
<Icon name='search' className={styles.searchIcon}/>
|
||||
<input
|
||||
type='text'
|
||||
value={search}
|
||||
className={styles.searchBoxInput}
|
||||
onChange={this.onSearchChange}
|
||||
placeholder={t('streams.search')}/>
|
||||
</div>
|
||||
<div className={styles.optionHeader}>{t('streams.filter_streams')}</div>
|
||||
<div className={styles.optionDetail}>{t('streams.stream_status')}</div>
|
||||
<RadioGroup
|
||||
name='status filter'
|
||||
value={filter}
|
||||
childContainer='div'
|
||||
onChange={this.onSettingChange('filter')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value='all'>{t('streams.all')}</Radio>
|
||||
<Radio value='open'>{t('streams.open')}</Radio>
|
||||
<Radio value='closed'>{t('streams.closed')}</Radio>
|
||||
</RadioGroup>
|
||||
<div className={styles.optionHeader}>{t('streams.sort_by')}</div>
|
||||
<RadioGroup
|
||||
name='sort by'
|
||||
value={sort}
|
||||
childContainer='div'
|
||||
onChange={this.onSettingChange('sort')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value='desc'>{t('streams.newest')}</Radio>
|
||||
<Radio value='asc'>{t('streams.oldest')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{
|
||||
assetsIds.length
|
||||
? <div className={styles.mainContent}>
|
||||
<DataTable className={styles.streamsTable} rows={assetsIds} onClick={this.goToModeration}>
|
||||
<TableHeader name="title" cellFormatter={this.renderTitle}>{t('streams.article')}</TableHeader>
|
||||
<TableHeader name="publication_date" cellFormatter={this.renderDate}>
|
||||
{t('streams.pubdate')}
|
||||
</TableHeader>
|
||||
<TableHeader name="closedAt" cellFormatter={this.renderStatus} className={styles.status}>
|
||||
{t('streams.status')}
|
||||
</TableHeader>
|
||||
</DataTable>
|
||||
<Pager
|
||||
totalPages={Math.ceil((assets.count || 0) / limit)}
|
||||
page={this.state.page}
|
||||
onNewPageHandler={this.onPageClick} />
|
||||
</div>
|
||||
: <EmptyCard>{t('streams.empty_result')}</EmptyCard>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({assets}) => {
|
||||
return {
|
||||
assets: assets.toJS()
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
fetchAssets: (...args) => {
|
||||
dispatch(fetchAssets.apply(this, args));
|
||||
},
|
||||
updateAssetState: (...args) => dispatch(updateAssetState.apply(this, args))
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Stories);
|
||||
@@ -1,12 +0,0 @@
|
||||
fragment metrics on Asset {
|
||||
id
|
||||
title
|
||||
url
|
||||
author
|
||||
created_at
|
||||
commentCount
|
||||
action_summaries {
|
||||
actionCount
|
||||
actionableItemCount
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
fragment commentView on Comment {
|
||||
id
|
||||
body
|
||||
created_at
|
||||
status
|
||||
user {
|
||||
id
|
||||
name: username
|
||||
status
|
||||
}
|
||||
asset {
|
||||
id
|
||||
title
|
||||
url
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
actions {
|
||||
... on FlagAction {
|
||||
reason
|
||||
message
|
||||
user {
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import {add} from 'coral-framework/services/graphqlRegistry';
|
||||
const queues = ['all', 'premod', 'flagged', 'accepted', 'rejected'];
|
||||
|
||||
const extension = {
|
||||
mutations: {
|
||||
SetUserStatus: () => ({
|
||||
refetchQueries: ['CoralAdmin_Community'],
|
||||
}),
|
||||
RejectUsername: () => ({
|
||||
refetchQueries: ['CoralAdmin_Community'],
|
||||
}),
|
||||
SetCommentStatus: ({variables: {commentId, status}}) => ({
|
||||
updateQueries: {
|
||||
CoralAdmin_Moderation: (oldData) => {
|
||||
const comment = queues.reduce((comment, queue) => {
|
||||
return comment ? comment : oldData[queue].find((c) => c.id === commentId);
|
||||
}, null);
|
||||
|
||||
let accepted = oldData.accepted;
|
||||
let acceptedCount = oldData.acceptedCount;
|
||||
let rejected = oldData.rejected;
|
||||
let rejectedCount = oldData.rejectedCount;
|
||||
|
||||
if (status !== comment.status) {
|
||||
if (status === 'ACCEPTED') {
|
||||
comment.status = 'ACCEPTED';
|
||||
acceptedCount++;
|
||||
accepted = [comment, ...accepted];
|
||||
}
|
||||
else if (status === 'REJECTED') {
|
||||
comment.status = 'REJECTED';
|
||||
rejectedCount++;
|
||||
rejected = [comment, ...rejected];
|
||||
}
|
||||
}
|
||||
|
||||
const premod = oldData.premod.filter((c) => c.id !== commentId);
|
||||
const flagged = oldData.flagged.filter((c) => c.id !== commentId);
|
||||
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
|
||||
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
|
||||
|
||||
if (status === 'REJECTED') {
|
||||
accepted = oldData.accepted.filter((c) => c.id !== commentId);
|
||||
acceptedCount = accepted.length < oldData.accepted.length ? oldData.acceptedCount - 1 : oldData.acceptedCount;
|
||||
}
|
||||
else if (status === 'ACCEPTED') {
|
||||
rejected = oldData.rejected.filter((c) => c.id !== commentId);
|
||||
rejectedCount = rejected.length < oldData.rejected.length ? oldData.rejectedCount - 1 : oldData.rejectedCount;
|
||||
}
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
premodCount: Math.max(0, premodCount),
|
||||
flaggedCount: Math.max(0, flaggedCount),
|
||||
acceptedCount: Math.max(0, acceptedCount),
|
||||
rejectedCount: Math.max(0, rejectedCount),
|
||||
premod,
|
||||
flagged,
|
||||
accepted,
|
||||
rejected,
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
add(extension);
|
||||
@@ -1,153 +0,0 @@
|
||||
import {graphql} from 'react-apollo';
|
||||
import SET_USER_STATUS from './setUserStatus.graphql';
|
||||
import SET_COMMENT_STATUS from './setCommentStatus.graphql';
|
||||
import SUSPEND_USER from './suspendUser.graphql';
|
||||
import REJECT_USERNAME from './rejectUsername.graphql';
|
||||
|
||||
export const banUser = graphql(SET_USER_STATUS, {
|
||||
props: ({mutate}) => ({
|
||||
banUser: ({userId}) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
userId,
|
||||
status: 'BANNED'
|
||||
},
|
||||
refetchQueries: ['Users']
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
export const setUserStatus = graphql(SET_USER_STATUS, {
|
||||
props: ({mutate}) => ({
|
||||
approveUser: ({userId}) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
userId,
|
||||
status: 'APPROVED'
|
||||
},
|
||||
refetchQueries: ['Users']
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export const suspendUser = graphql(SUSPEND_USER, {
|
||||
props: ({mutate}) => ({
|
||||
suspendUser: (input) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export const rejectUsername = graphql(REJECT_USERNAME, {
|
||||
props: ({mutate}) => ({
|
||||
rejectUsername: (input) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
refetchQueries: ['Users']
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const views = ['all', 'premod', 'flagged', 'accepted', 'rejected'];
|
||||
export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
|
||||
props: ({mutate}) => ({
|
||||
acceptComment: ({commentId}) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
commentId,
|
||||
status: 'ACCEPTED'
|
||||
},
|
||||
updateQueries: {
|
||||
ModQueue: (oldData) => {
|
||||
const comment = views.reduce((comment, view) => {
|
||||
return comment ? comment : oldData[view].find((c) => c.id === commentId);
|
||||
}, null);
|
||||
let accepted;
|
||||
let acceptedCount = oldData.acceptedCount;
|
||||
|
||||
// if the comment was already in the Approved queue, don't re-add it
|
||||
if (comment.status === 'ACCEPTED') {
|
||||
accepted = [...oldData.accepted];
|
||||
} else {
|
||||
comment.status = 'ACCEPTED';
|
||||
acceptedCount++;
|
||||
accepted = [comment, ...oldData.accepted];
|
||||
}
|
||||
|
||||
const premod = oldData.premod.filter((c) => c.id !== commentId);
|
||||
const flagged = oldData.flagged.filter((c) => c.id !== commentId);
|
||||
const rejected = oldData.rejected.filter((c) => c.id !== commentId);
|
||||
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
|
||||
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
|
||||
const rejectedCount = rejected.length < oldData.rejected.length ? oldData.rejectedCount - 1 : oldData.rejectedCount;
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
premodCount: Math.max(0, premodCount),
|
||||
flaggedCount: Math.max(0, flaggedCount),
|
||||
acceptedCount: Math.max(0, acceptedCount),
|
||||
rejectedCount: Math.max(0, rejectedCount),
|
||||
premod,
|
||||
flagged,
|
||||
accepted,
|
||||
rejected,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
rejectComment: ({commentId}) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
commentId,
|
||||
status: 'REJECTED'
|
||||
},
|
||||
updateQueries: {
|
||||
ModQueue: (oldData) => {
|
||||
const comment = views.reduce((comment, view) => {
|
||||
return comment ? comment : oldData[view].find((c) => c.id === commentId);
|
||||
}, null);
|
||||
let rejected;
|
||||
let rejectedCount = oldData.rejectedCount;
|
||||
|
||||
// if the item was already in the Rejected queue, don't put it in again
|
||||
if (comment.status === 'REJECTED') {
|
||||
rejected = oldData.rejected;
|
||||
} else {
|
||||
comment.status = 'REJECTED';
|
||||
rejectedCount++;
|
||||
rejected = [comment, ...oldData.rejected];
|
||||
}
|
||||
|
||||
const premod = oldData.premod.filter((c) => c.id !== commentId);
|
||||
const flagged = oldData.flagged.filter((c) => c.id !== commentId);
|
||||
const accepted = oldData.accepted.filter((c) => c.id !== commentId);
|
||||
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
|
||||
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
|
||||
const acceptedCount = accepted.length < oldData.accepted.length ? oldData.acceptedCount - 1 : oldData.acceptedCount;
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
premodCount: Math.max(0, premodCount),
|
||||
flaggedCount: Math.max(0, flaggedCount),
|
||||
acceptedCount: Math.max(0, acceptedCount),
|
||||
rejectedCount: Math.max(0, rejectedCount),
|
||||
premod,
|
||||
flagged,
|
||||
accepted,
|
||||
rejected
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
mutation rejectUsername($input: RejectUsernameInput!) {
|
||||
rejectUsername(input: $input) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mutation setCommentStatus($commentId: ID!, $status: COMMENT_STATUS!){
|
||||
setCommentStatus(id: $commentId, status: $status) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mutation setUserStatus($userId: ID!, $status: USER_STATUS!) {
|
||||
setUserStatus(id: $userId, status: $status) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mutation suspendUser($input: SuspendUserInput!) {
|
||||
suspendUser(input: $input) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
query Assets {
|
||||
assets {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
query Counts ($asset_id: ID) {
|
||||
allCount: commentCount(query: {
|
||||
asset_id: $asset_id
|
||||
})
|
||||
acceptedCount: commentCount(query: {
|
||||
statuses: [ACCEPTED],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
premodCount: commentCount(query: {
|
||||
statuses: [PREMOD],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
rejectedCount: commentCount(query: {
|
||||
statuses: [REJECTED],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
flaggedCount: commentCount(query: {
|
||||
action_type: FLAG,
|
||||
asset_id: $asset_id,
|
||||
statuses: [NONE, PREMOD]
|
||||
})
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import {graphql} from 'react-apollo';
|
||||
|
||||
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
|
||||
import MOD_QUEUE_LOAD_MORE from './loadMore.graphql';
|
||||
import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
|
||||
import METRICS from './metricsQuery.graphql';
|
||||
import USER_DETAIL from './userDetail.graphql';
|
||||
import GET_QUEUE_COUNTS from './getQueueCounts.graphql';
|
||||
|
||||
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
|
||||
options: ({params: {id = null}}) => {
|
||||
return {
|
||||
variables: {
|
||||
asset_id: id,
|
||||
sort: 'REVERSE_CHRONOLOGICAL'
|
||||
}
|
||||
};
|
||||
},
|
||||
props: ({ownProps: {params: {id = null}}, data}) => ({
|
||||
data,
|
||||
modQueueResort: modQueueResort(id, data.fetchMore),
|
||||
loadMore: loadMore(data.fetchMore)
|
||||
})
|
||||
});
|
||||
|
||||
export const getMetrics = graphql(METRICS, {
|
||||
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
|
||||
|
||||
return {
|
||||
variables: {
|
||||
from: dashboardWindowStart,
|
||||
to: dashboardWindowEnd
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const loadMore = (fetchMore) => ({limit = 10, cursor, sort, tab, asset_id}) => {
|
||||
let variables = {
|
||||
limit,
|
||||
cursor,
|
||||
sort,
|
||||
asset_id
|
||||
};
|
||||
switch(tab) {
|
||||
case 'all':
|
||||
variables.statuses = null;
|
||||
break;
|
||||
case 'accepted':
|
||||
variables.statuses = ['ACCEPTED'];
|
||||
break;
|
||||
case 'premod':
|
||||
variables.statuses = ['PREMOD'];
|
||||
break;
|
||||
case 'flagged':
|
||||
variables.statuses = ['NONE', 'PREMOD'];
|
||||
variables.action_type = 'FLAG';
|
||||
break;
|
||||
case 'rejected':
|
||||
variables.statuses = ['REJECTED'];
|
||||
break;
|
||||
}
|
||||
return fetchMore({
|
||||
query: MOD_QUEUE_LOAD_MORE,
|
||||
variables,
|
||||
updateQuery: (oldData, {fetchMoreResult:{comments}}) => {
|
||||
return {
|
||||
...oldData,
|
||||
[tab]: [
|
||||
...oldData[tab],
|
||||
...comments
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const modUserFlaggedQuery = graphql(MOD_USER_FLAGGED_QUERY, {
|
||||
options: ({params: {action_type = 'FLAG'}}) => {
|
||||
return {
|
||||
variables: {
|
||||
action_type: action_type
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const modQueueResort = (id, fetchMore) => (sort) => {
|
||||
return fetchMore({
|
||||
query: MOD_QUEUE_QUERY,
|
||||
variables: {
|
||||
asset_id: id,
|
||||
sort
|
||||
},
|
||||
updateQuery: (oldData, {fetchMoreResult:{data}}) => data
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserDetail = graphql(USER_DETAIL, {
|
||||
options: ({id}) => {
|
||||
return {
|
||||
variables: {author_id: id}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const getQueueCounts = graphql(GET_QUEUE_COUNTS, {
|
||||
options: ({params: {id = null}}) => {
|
||||
return {
|
||||
pollInterval: 5000,
|
||||
variables: {
|
||||
asset_id: id
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
query LoadMoreModQueue($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) {
|
||||
...commentView
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
#import "../fragments/assetMetricsView.graphql"
|
||||
|
||||
query Metrics ($from: Date!, $to: Date!) {
|
||||
assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
|
||||
...metrics
|
||||
}
|
||||
assetsByActivity: assetMetrics(from: $from, to: $to, sort: ACTIVITY) {
|
||||
...metrics
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
|
||||
all: comments(query: {
|
||||
statuses: [NONE, PREMOD, ACCEPTED, REJECTED],
|
||||
asset_id: $asset_id,
|
||||
sort: $sort
|
||||
}) {
|
||||
...commentView
|
||||
}
|
||||
accepted: comments(query: {
|
||||
statuses: [ACCEPTED],
|
||||
asset_id: $asset_id,
|
||||
sort: $sort
|
||||
}) {
|
||||
...commentView
|
||||
}
|
||||
premod: comments(query: {
|
||||
statuses: [PREMOD],
|
||||
asset_id: $asset_id,
|
||||
sort: $sort
|
||||
}) {
|
||||
...commentView
|
||||
}
|
||||
flagged: comments(query: {
|
||||
action_type: FLAG,
|
||||
asset_id: $asset_id,
|
||||
statuses: [NONE, PREMOD],
|
||||
sort: $sort
|
||||
}) {
|
||||
...commentView
|
||||
}
|
||||
rejected: comments(query: {
|
||||
statuses: [REJECTED],
|
||||
asset_id: $asset_id,
|
||||
sort: $sort
|
||||
}) {
|
||||
...commentView
|
||||
}
|
||||
assets: assets {
|
||||
id
|
||||
title
|
||||
url
|
||||
}
|
||||
allCount: commentCount(query: {
|
||||
asset_id: $asset_id
|
||||
})
|
||||
acceptedCount: commentCount(query: {
|
||||
statuses: [ACCEPTED],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
premodCount: commentCount(query: {
|
||||
statuses: [PREMOD],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
rejectedCount: commentCount(query: {
|
||||
statuses: [REJECTED],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
flaggedCount: commentCount(query: {
|
||||
action_type: FLAG,
|
||||
asset_id: $asset_id,
|
||||
statuses: [NONE, PREMOD]
|
||||
})
|
||||
settings {
|
||||
organizationName
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
query Users ($action_type: ACTION_TYPE) {
|
||||
users (query:{action_type: $action_type}){
|
||||
id
|
||||
username
|
||||
status
|
||||
roles
|
||||
actions{
|
||||
id
|
||||
created_at
|
||||
... on FlagAction {
|
||||
reason
|
||||
message
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
query UserDetail ($author_id: ID!) {
|
||||
user(id: $author_id) {
|
||||
id
|
||||
username
|
||||
created_at
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
}
|
||||
totalComments: commentCount(query: {author_id: $author_id})
|
||||
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import store from './services/store';
|
||||
import App from './components/App';
|
||||
|
||||
import 'react-mdl/extra/material.js';
|
||||
import './graphql';
|
||||
import {loadPluginsTranslations} from 'coral-framework/helpers/plugins';
|
||||
|
||||
loadPluginsTranslations();
|
||||
|
||||
@@ -10,6 +10,7 @@ const initialState = fromJS({
|
||||
userDetailId: null,
|
||||
banDialog: false,
|
||||
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show',
|
||||
sortOrder: 'REVERSE_CHRONOLOGICAL',
|
||||
suspendUserDialog: {
|
||||
show: false,
|
||||
userId: null,
|
||||
@@ -64,6 +65,8 @@ export default function moderation (state = initialState, action) {
|
||||
return state.set('userDetailId', action.userId);
|
||||
case actions.HIDE_USER_DETAIL:
|
||||
return state.set('userDetailId', null);
|
||||
case actions.SET_SORT_ORDER:
|
||||
return state.set('sortOrder', action.order);
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import styles from '../Community.css';
|
||||
import styles from './Community.css';
|
||||
import BanUserButton from './BanUserButton';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../../../containers/ModerationQueue/helpers/moderationQueueActionsMap';
|
||||
import {menuActionsMap} from '../../Moderation/helpers/moderationQueueActionsMap';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
+22
-74
@@ -1,55 +1,26 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {compose} from 'react-apollo';
|
||||
|
||||
import {modUserFlaggedQuery} from 'coral-admin/src/graphql/queries';
|
||||
import {banUser, setUserStatus, rejectUsername} from 'coral-admin/src/graphql/mutations';
|
||||
|
||||
import {
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
newPage,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
hideSuspendUserDialog
|
||||
} from '../../actions/community';
|
||||
|
||||
import CommunityMenu from './components/CommunityMenu';
|
||||
import BanUserDialog from './components/BanUserDialog';
|
||||
import SuspendUserDialog from './components/SuspendUserDialog';
|
||||
|
||||
import CommunityMenu from './CommunityMenu';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import People from './People';
|
||||
import FlaggedAccounts from './FlaggedAccounts';
|
||||
|
||||
class CommunityContainer extends Component {
|
||||
export default class Community extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
state = {
|
||||
searchValue: '',
|
||||
timer: null
|
||||
};
|
||||
|
||||
this.state = {
|
||||
searchValue: '',
|
||||
timer: null
|
||||
};
|
||||
|
||||
this.onKeyDownHandler = this.onKeyDownHandler.bind(this);
|
||||
this.onSearchChange = this.onSearchChange.bind(this);
|
||||
this.onHeaderClickHandler = this.onHeaderClickHandler.bind(this);
|
||||
this.onNewPageHandler = this.onNewPageHandler.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.fetchAccounts({});
|
||||
}
|
||||
|
||||
onKeyDownHandler(e) {
|
||||
onKeyDownHandler = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
onSearchChange(e) {
|
||||
onSearchChange = (e) => {
|
||||
const value = e.target.value;
|
||||
this.setState((prevState) => {
|
||||
prevState.searchValue = value;
|
||||
@@ -62,6 +33,16 @@ class CommunityContainer extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
onHeaderClickHandler = (sort) => {
|
||||
this.props.updateSorting(sort);
|
||||
this.search();
|
||||
}
|
||||
|
||||
onNewPageHandler = (page) => {
|
||||
this.props.newPage(page);
|
||||
this.search({page});
|
||||
}
|
||||
|
||||
search(query = {}) {
|
||||
const {community} = this.props;
|
||||
|
||||
@@ -71,21 +52,10 @@ class CommunityContainer extends Component {
|
||||
asc: community.ascPeople,
|
||||
...query
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
onHeaderClickHandler(sort) {
|
||||
this.props.dispatch(updateSorting(sort));
|
||||
this.search();
|
||||
}
|
||||
|
||||
onNewPageHandler(page) {
|
||||
this.props.dispatch(newPage(page));
|
||||
this.search({page});
|
||||
}
|
||||
|
||||
getTabContent(searchValue, props) {
|
||||
const {community, data} = props;
|
||||
const {community, root: {users}} = props;
|
||||
const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path;
|
||||
|
||||
if (activeTab === 'people') {
|
||||
@@ -108,9 +78,7 @@ class CommunityContainer extends Component {
|
||||
return (
|
||||
<div>
|
||||
<FlaggedAccounts
|
||||
commenters={data.users}
|
||||
isFetching={data.loading}
|
||||
error={data.error}
|
||||
commenters={users}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
approveUser={props.approveUser}
|
||||
rejectUsername={props.rejectUsername}
|
||||
@@ -134,7 +102,6 @@ class CommunityContainer extends Component {
|
||||
|
||||
render() {
|
||||
const {searchValue} = this.state;
|
||||
|
||||
const tab = this.getTabContent(searchValue, this.props);
|
||||
|
||||
return (
|
||||
@@ -148,22 +115,3 @@ class CommunityContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
community: state.community.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
fetchAccounts: (query) => dispatch(fetchAccounts(query)),
|
||||
showBanUserDialog: (user) => dispatch(showBanUserDialog(user)),
|
||||
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
|
||||
showSuspendUserDialog: (user) => dispatch(showSuspendUserDialog(user)),
|
||||
hideSuspendUserDialog: () => dispatch(hideSuspendUserDialog())
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
modUserFlaggedQuery,
|
||||
banUser,
|
||||
setUserStatus,
|
||||
rejectUsername
|
||||
)(CommunityContainer);
|
||||
+3
-5
@@ -2,20 +2,18 @@ import React from 'react';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './Community.css';
|
||||
import Loading from './Loading';
|
||||
import EmptyCard from 'coral-admin/src/components/EmptyCard';
|
||||
import User from './components/User';
|
||||
import User from './User';
|
||||
|
||||
const FlaggedAccounts = ({...props}) => {
|
||||
const {commenters, isFetching} = props;
|
||||
const hasResults = !isFetching && commenters && !!commenters.length;
|
||||
const {commenters} = props;
|
||||
const hasResults = commenters && !!commenters.length;
|
||||
|
||||
// if (commenter.status === 'PENDING' && commenter.actions.length > 0) {
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.mainFlaggedContent}>
|
||||
{ isFetching && <Loading /> }
|
||||
{
|
||||
hasResults
|
||||
? commenters.map((commenter, index) => {
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Community.css';
|
||||
import Table from './Table';
|
||||
import Table from '../containers/Table';
|
||||
import {Pager, Icon} from 'coral-ui';
|
||||
import EmptyCard from '../../components/EmptyCard';
|
||||
import EmptyCard from '../../../components/EmptyCard';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const tableHeaders = [
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import styles from '../components/Community.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onCommenterStatusChange}) => (
|
||||
<table className={`mdl-data-table ${styles.dataTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, i) =>(
|
||||
<th
|
||||
key={i}
|
||||
className="mdl-data-table__cell--non-numeric"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{commenters.map((row, i)=> (
|
||||
<tr key={i}>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.username}
|
||||
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.created_at}
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField label={'Select me'} value={row.status || ''}
|
||||
className={styles.selectField}
|
||||
label={t('community.status')}
|
||||
onChange={(status) => onCommenterStatusChange(row.id, status)}>
|
||||
<Option value={'ACTIVE'}>{t('community.active')}</Option>
|
||||
<Option value={'BANNED'}>{t('community.banned')}</Option>
|
||||
</SelectField>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField label={'Select me'} value={row.roles[0] || ''}
|
||||
className={styles.selectField}
|
||||
label={t('community.role')}
|
||||
onChange={(role) => onRoleChange(row.id, role)}>
|
||||
<Option value={''}>.</Option>
|
||||
<Option value={'STAFF'}>{t('community.staff')}</Option>
|
||||
<Option value={'MODERATOR'}>{t('community.moderator')}</Option>
|
||||
<Option value={'ADMIN'}>{t('community.admin')}</Option>
|
||||
</SelectField>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import styles from '../Community.css';
|
||||
import styles from './Community.css';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {Spinner} from 'coral-ui';
|
||||
|
||||
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
|
||||
import {
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
newPage,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
hideSuspendUserDialog
|
||||
} from '../../../actions/community';
|
||||
|
||||
import Community from '../components/Community';
|
||||
|
||||
class CommunityContainer extends Component {
|
||||
|
||||
componentWillMount() {
|
||||
this.props.fetchAccounts({});
|
||||
}
|
||||
|
||||
approveUser = ({userId}) => {
|
||||
return this.props.setUserStatus({userId, status: 'APPROVED'});
|
||||
}
|
||||
|
||||
banUser = ({userId}) => {
|
||||
return this.props.setUserStatus({userId, status: 'BANNED'});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (!('users' in this.props.root)) {
|
||||
return <div><Spinner/></div>;
|
||||
}
|
||||
return (
|
||||
<Community {...this.props} approveUser={this.approveUser} banUser={this.banUser}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const withCommunityQuery = withQuery(gql`
|
||||
query CoralAdmin_Community($action_type: ACTION_TYPE) {
|
||||
users(query:{action_type: $action_type}){
|
||||
id
|
||||
username
|
||||
status
|
||||
roles
|
||||
actions{
|
||||
id
|
||||
created_at
|
||||
... on FlagAction {
|
||||
reason
|
||||
message
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {
|
||||
options: ({params: {action_type = 'FLAG'}}) => {
|
||||
return {
|
||||
variables: {
|
||||
action_type: action_type
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
community: state.community.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
fetchAccounts,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
hideSuspendUserDialog,
|
||||
updateSorting,
|
||||
newPage,
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withCommunityQuery,
|
||||
withSetUserStatus,
|
||||
withRejectUsername,
|
||||
)(CommunityContainer);
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose} from 'react-apollo';
|
||||
import {setRole, setCommenterStatus} from '../../../actions/community';
|
||||
import Table from '../components/Table';
|
||||
|
||||
class TableContainer extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Table
|
||||
{...this.props}
|
||||
onRoleChange={this.props.setRole}
|
||||
onCommenterStatusChange={this.props.setCommenterStatus}
|
||||
commenters={this.props.commenters}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
commenters: state.community.get('accounts'),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
setCommenterStatus,
|
||||
setRole,
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
)(TableContainer);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export {default as Community} from './containers/Community';
|
||||
export {default as CommunityLayout} from './components/CommunityLayout';
|
||||
+11
-34
@@ -1,12 +1,4 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
fetchSettings,
|
||||
updateSettings,
|
||||
saveSettingsToServer,
|
||||
updateWordlist,
|
||||
updateDomainlist
|
||||
} from '../../actions/settings';
|
||||
|
||||
import {Button, List, Item, Card, Spinner} from 'coral-ui';
|
||||
import styles from './Configure.css';
|
||||
@@ -16,45 +8,36 @@ import TechSettings from './TechSettings';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
|
||||
class Configure extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
export default class Configure extends Component {
|
||||
|
||||
this.state = {
|
||||
activeSection: 'stream',
|
||||
changed: false,
|
||||
errors: {}
|
||||
};
|
||||
|
||||
this.changeSection = this.changeSection.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
this.props.dispatch(fetchSettings());
|
||||
}
|
||||
state = {
|
||||
activeSection: 'stream',
|
||||
changed: false,
|
||||
errors: {}
|
||||
};
|
||||
|
||||
saveSettings = () => {
|
||||
this.props.dispatch(saveSettingsToServer());
|
||||
this.props.saveSettingsToServer();
|
||||
this.setState({changed: false});
|
||||
}
|
||||
|
||||
changeSection(activeSection) {
|
||||
changeSection = (activeSection) => {
|
||||
this.setState({activeSection});
|
||||
}
|
||||
|
||||
onChangeWordlist = (listName, list) => {
|
||||
this.setState({changed: true});
|
||||
this.props.dispatch(updateWordlist(listName, list));
|
||||
this.props.updateWordlist(listName, list);
|
||||
}
|
||||
|
||||
onChangeDomainlist = (listName, list) => {
|
||||
this.setState({changed: true});
|
||||
this.props.dispatch(updateDomainlist(listName, list));
|
||||
this.props.updateDomainlist(listName, list);
|
||||
}
|
||||
|
||||
onSettingUpdate = (setting) => {
|
||||
this.setState({changed: true});
|
||||
this.props.dispatch(updateSettings(setting));
|
||||
this.props.updateSettings(setting);
|
||||
}
|
||||
|
||||
// Sets an arbitrary error string and a boolean state.
|
||||
@@ -175,9 +158,3 @@ class Configure extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth.toJS(),
|
||||
settings: state.settings.toJS()
|
||||
});
|
||||
export default connect(mapStateToProps)(Configure);
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './Configure.css';
|
||||
import {Button, Card} from 'coral-ui';
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose} from 'react-apollo';
|
||||
import {
|
||||
fetchSettings,
|
||||
updateSettings,
|
||||
saveSettingsToServer,
|
||||
updateWordlist,
|
||||
updateDomainlist
|
||||
} from '../../../actions/settings';
|
||||
|
||||
import Configure from '../components/Configure';
|
||||
|
||||
class ConfigureContainer extends Component {
|
||||
componentWillMount = () => {
|
||||
this.props.fetchSettings();
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Configure {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth.toJS(),
|
||||
settings: state.settings.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
fetchSettings,
|
||||
updateSettings,
|
||||
saveSettingsToServer,
|
||||
updateWordlist,
|
||||
updateDomainlist
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
)(ConfigureContainer);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export {default} from './containers/Configure';
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from 'coral-admin/src/containers/Dashboard/Dashboard.css';
|
||||
import styles from './Dashboard.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import FlagWidget from './FlagWidget';
|
||||
import ActivityWidget from './ActivityWidget';
|
||||
import CountdownTimer from './CountdownTimer';
|
||||
import styles from './Dashboard.css';
|
||||
|
||||
export default ({root: {assetsByActivity, assetsByFlag}, reloadData}) => (
|
||||
<div>
|
||||
<CountdownTimer handleTimeout={reloadData} />
|
||||
<div className={styles.Dashboard}>
|
||||
<FlagWidget assets={assetsByFlag} />
|
||||
<ActivityWidget assets={assetsByActivity} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import Dashboard from '../components/Dashboard';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
|
||||
import {Spinner} from 'coral-ui';
|
||||
|
||||
class DashboardContainer extends React.Component {
|
||||
|
||||
reloadData = () => {
|
||||
this.props.data.refetch();
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.props.data.loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
return <Dashboard {...this.props} reloadData={this.reloadData} />;
|
||||
}
|
||||
}
|
||||
|
||||
export const witDashboardQuery = withQuery(gql`
|
||||
query CoralAdmin_Dashboard($from: Date!, $to: Date!) {
|
||||
assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
|
||||
...CoralAdmin_Metrics
|
||||
}
|
||||
assetsByActivity: assetMetrics(from: $from, to: $to, sort: ACTIVITY) {
|
||||
...CoralAdmin_Metrics
|
||||
}
|
||||
}
|
||||
fragment CoralAdmin_Metrics on Asset {
|
||||
id
|
||||
title
|
||||
url
|
||||
author
|
||||
created_at
|
||||
commentCount
|
||||
action_summaries {
|
||||
actionCount
|
||||
actionableItemCount
|
||||
}
|
||||
}
|
||||
`, {
|
||||
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
|
||||
return {
|
||||
variables: {
|
||||
from: dashboardWindowStart,
|
||||
to: dashboardWindowEnd
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
settings: state.settings.toJS(),
|
||||
moderation: state.moderation.toJS()
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
witDashboardQuery,
|
||||
)(DashboardContainer);
|
||||
@@ -0,0 +1 @@
|
||||
export {default} from './containers/Dashboard.js';
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, {Component} from 'react';
|
||||
import styles from './style.css';
|
||||
import {Wizard, WizardNav} from 'coral-ui';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
|
||||
import InitialStep from './Steps/InitialStep';
|
||||
import AddOrganizationName from './Steps/AddOrganizationName';
|
||||
import CreateYourAccount from './Steps/CreateYourAccount';
|
||||
import PermittedDomainsStep from './Steps/PermittedDomainsStep';
|
||||
import FinalStep from './Steps/FinalStep';
|
||||
|
||||
export default class Install extends Component {
|
||||
handleDomainsChange = (value) => {
|
||||
this.props.updatePermittedDomains(value);
|
||||
};
|
||||
|
||||
handleSettingsChange = ({currentTarget: {name, value}}) => {
|
||||
this.props.updateSettingsFormData(name, value);
|
||||
};
|
||||
|
||||
handleUserChange = ({currentTarget: {name, value}}) => {
|
||||
this.props.updateUserFormData(name, value);
|
||||
};
|
||||
|
||||
handleSettingsSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.submitSettings();
|
||||
};
|
||||
|
||||
handleUserSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.submitUser();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {install} = this.props;
|
||||
|
||||
return (
|
||||
<Layout restricted={true}>
|
||||
<div className={styles.Install}>
|
||||
{
|
||||
!install.alreadyInstalled ? (
|
||||
<div>
|
||||
<h2>Welcome to the Coral Project</h2>
|
||||
{ install.step !== 0 ? <WizardNav items={install.navItems} currentStep={install.step} icon='check'/> : null }
|
||||
<Wizard
|
||||
currentStep={install.step}
|
||||
nextStep={this.props.nextStep}
|
||||
previousStep={this.props.previousStep}
|
||||
goToStep={this.props.goToStep}
|
||||
>
|
||||
<InitialStep/>
|
||||
<AddOrganizationName
|
||||
install={install}
|
||||
handleSettingsChange={this.handleSettingsChange}
|
||||
handleSettingsSubmit={this.handleSettingsSubmit}
|
||||
/>
|
||||
<CreateYourAccount
|
||||
install={install}
|
||||
handleUserChange={this.handleUserChange}
|
||||
handleUserSubmit={this.handleUserSubmit}
|
||||
/>
|
||||
<PermittedDomainsStep
|
||||
install={install}
|
||||
handleDomainsChange={this.handleDomainsChange}
|
||||
finishInstall={this.props.finishInstall}
|
||||
/>
|
||||
<FinalStep/>
|
||||
</Wizard>
|
||||
</div>
|
||||
) : (
|
||||
<div>Talk is already installed</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose} from 'react-apollo';
|
||||
import Install from '../components/Install';
|
||||
|
||||
import {
|
||||
goToStep,
|
||||
nextStep,
|
||||
submitUser,
|
||||
checkInstall,
|
||||
previousStep,
|
||||
finishInstall,
|
||||
submitSettings,
|
||||
updateUserFormData,
|
||||
updateSettingsFormData,
|
||||
updatePermittedDomains
|
||||
} from '../../../actions/install';
|
||||
|
||||
class InstallContainer extends Component {
|
||||
componentDidMount() {
|
||||
const {checkInstall} = this.props;
|
||||
checkInstall(() => {
|
||||
this.context.router.push('/admin');
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Install {...this.props}/>;
|
||||
}
|
||||
}
|
||||
|
||||
InstallContainer.contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
install: state.install.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
goToStep,
|
||||
nextStep,
|
||||
submitUser,
|
||||
checkInstall,
|
||||
previousStep,
|
||||
finishInstall,
|
||||
submitSettings,
|
||||
updateUserFormData,
|
||||
updateSettingsFormData,
|
||||
updatePermittedDomains
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
)(InstallContainer);
|
||||
@@ -0,0 +1 @@
|
||||
export {default} from './containers/Install.js';
|
||||
+26
-6
@@ -39,8 +39,8 @@ const Comment = ({
|
||||
}
|
||||
|
||||
// since words are checked against word boundaries on the backend,
|
||||
// this should be the behavior on the front end as well.
|
||||
// currently the highlighter plugin does not support this out of the box.
|
||||
// should be the behavior on the front end as well.
|
||||
// currently the highlighter plugin does not support out of the box.
|
||||
const searchWords = [...suspectWords, ...bannedWords]
|
||||
.filter((w) => {
|
||||
return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body);
|
||||
@@ -82,7 +82,12 @@ const Comment = ({
|
||||
{t('comment.banned_user')}
|
||||
</span>
|
||||
: null}
|
||||
<Slot fill="adminCommentInfoBar" comment={comment} />
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminCommentInfoBar"
|
||||
comment={comment}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {comment.asset.title}
|
||||
@@ -104,7 +109,12 @@ const Comment = ({
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
</p>
|
||||
<Slot fill="adminCommentContent" comment={comment} />
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminCommentContent"
|
||||
comment={comment}
|
||||
/>
|
||||
<div className={styles.sideActions}>
|
||||
{links
|
||||
? <span className={styles.hasLinks}>
|
||||
@@ -135,12 +145,22 @@ const Comment = ({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Slot fill="adminSideActions" comment={comment} />
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminSideActions"
|
||||
comment={comment}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Slot fill="adminCommentDetailArea" comment={comment} />
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminCommentDetailArea"
|
||||
comment={comment}
|
||||
/>
|
||||
</div>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
+35
-134
@@ -1,50 +1,24 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose} from 'react-apollo';
|
||||
import * as notification from 'coral-admin/src/services/notification';
|
||||
import key from 'keymaster';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import styles from './components/styles.css';
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import styles from './styles.css';
|
||||
|
||||
import {modQueueQuery, getQueueCounts} from '../../graphql/queries';
|
||||
import {banUser, setCommentStatus, suspendUser} from '../../graphql/mutations';
|
||||
|
||||
import {fetchSettings} from 'actions/settings';
|
||||
import {updateAssets} from 'actions/assets';
|
||||
import {
|
||||
toggleModal,
|
||||
singleView,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
hideSuspendUserDialog,
|
||||
hideShortcutsNote,
|
||||
viewUserDetail,
|
||||
hideUserDetail
|
||||
} from 'actions/moderation';
|
||||
|
||||
import {Spinner} from 'coral-ui';
|
||||
import BanUserDialog from './components/BanUserDialog';
|
||||
import SuspendUserDialog from './components/SuspendUserDialog';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import ModerationQueue from './ModerationQueue';
|
||||
import ModerationMenu from './components/ModerationMenu';
|
||||
import ModerationHeader from './components/ModerationHeader';
|
||||
import NotFoundAsset from './components/NotFoundAsset';
|
||||
import ModerationKeysModal from '../../components/ModerationKeysModal';
|
||||
import UserDetail from './UserDetail';
|
||||
import ModerationMenu from './ModerationMenu';
|
||||
import ModerationHeader from './ModerationHeader';
|
||||
import NotFoundAsset from './NotFoundAsset';
|
||||
import ModerationKeysModal from '../../../components/ModerationKeysModal';
|
||||
import UserDetail from '../containers/UserDetail';
|
||||
|
||||
class ModerationContainer extends Component {
|
||||
export default class Moderation extends Component {
|
||||
state = {
|
||||
selectedIndex: 0,
|
||||
sort: 'REVERSE_CHRONOLOGICAL'
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const {toggleModal, singleView} = this.props;
|
||||
|
||||
this.props.fetchSettings();
|
||||
key('s', () => singleView());
|
||||
key('shift+/', () => toggleModal(true));
|
||||
key('esc', () => toggleModal(false));
|
||||
@@ -54,6 +28,10 @@ class ModerationContainer extends Component {
|
||||
key('t', this.moderate(true));
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.toggleModal(false);
|
||||
}
|
||||
|
||||
moderate = (accept) => () => {
|
||||
const {acceptComment, rejectComment} = this.props;
|
||||
const {selectedIndex} = this.state;
|
||||
@@ -69,9 +47,9 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
|
||||
getComments = () => {
|
||||
const {data, route} = this.props;
|
||||
const {root, route} = this.props;
|
||||
const activeTab = route.path === ':id' ? 'premod' : route.path;
|
||||
return data[activeTab];
|
||||
return root[activeTab];
|
||||
}
|
||||
|
||||
select = (next) => () => {
|
||||
@@ -92,38 +70,6 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
selectSort = (sort) => {
|
||||
this.setState({sort});
|
||||
this.props.modQueueResort(sort);
|
||||
}
|
||||
|
||||
suspendUser = async (args) => {
|
||||
this.props.hideSuspendUserDialog();
|
||||
try {
|
||||
const result = await this.props.suspendUser(args);
|
||||
if (result.data.suspendUser.errors) {
|
||||
throw result.data.suspendUser.errors;
|
||||
}
|
||||
notification.success(
|
||||
t('suspenduser.notify_suspend_until',
|
||||
this.props.moderation.suspendUserDialog.username,
|
||||
timeago(args.until)),
|
||||
);
|
||||
const {commentStatus, commentId} = this.props.moderation.suspendUserDialog;
|
||||
if (commentStatus !== 'REJECTED') {
|
||||
return this.props.rejectComment({commentId})
|
||||
.then((result) => {
|
||||
if (result.data.setCommentStatus.errors) {
|
||||
throw result.data.setCommentStatus.errors;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
notification.showMutationErrors(err);
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
key.unbind('s');
|
||||
key.unbind('shift+/');
|
||||
@@ -145,28 +91,13 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {updateAssets} = this.props;
|
||||
if(!isEqual(nextProps.data.assets, this.props.data.assets)) {
|
||||
updateAssets(nextProps.data.assets);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {data, moderation, settings, assets, onClose, viewUserDetail, hideUserDetail, ...props} = this.props;
|
||||
const {root, moderation, settings, assets, viewUserDetail, hideUserDetail, ...props} = this.props;
|
||||
const providedAssetId = this.props.params.id;
|
||||
const activeTab = this.props.route.path === ':id' ? 'premod' : this.props.route.path;
|
||||
|
||||
let asset;
|
||||
|
||||
if (!('premodCount' in data)) {
|
||||
return <div><Spinner/></div>;
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
return <div>Error</div>;
|
||||
}
|
||||
|
||||
if (providedAssetId) {
|
||||
asset = assets.find((asset) => asset.id === this.props.params.id);
|
||||
|
||||
@@ -175,23 +106,23 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const comments = data[activeTab];
|
||||
const comments = root[activeTab];
|
||||
let activeTabCount;
|
||||
switch(activeTab) {
|
||||
case 'all':
|
||||
activeTabCount = data.allCount;
|
||||
activeTabCount = root.allCount;
|
||||
break;
|
||||
case 'accepted':
|
||||
activeTabCount = data.acceptedCount;
|
||||
activeTabCount = root.acceptedCount;
|
||||
break;
|
||||
case 'premod':
|
||||
activeTabCount = data.premodCount;
|
||||
activeTabCount = root.premodCount;
|
||||
break;
|
||||
case 'flagged':
|
||||
activeTabCount = data.flaggedCount;
|
||||
activeTabCount = root.flaggedCount;
|
||||
break;
|
||||
case 'rejected':
|
||||
activeTabCount = data.rejectedCount;
|
||||
activeTabCount = root.rejectedCount;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -200,15 +131,17 @@ class ModerationContainer extends Component {
|
||||
<ModerationHeader asset={asset} />
|
||||
<ModerationMenu
|
||||
asset={asset}
|
||||
allCount={data.allCount}
|
||||
acceptedCount={data.acceptedCount}
|
||||
premodCount={data.premodCount}
|
||||
rejectedCount={data.rejectedCount}
|
||||
flaggedCount={data.flaggedCount}
|
||||
selectSort={this.selectSort}
|
||||
sort={this.state.sort}
|
||||
allCount={root.allCount}
|
||||
acceptedCount={root.acceptedCount}
|
||||
premodCount={root.premodCount}
|
||||
rejectedCount={root.rejectedCount}
|
||||
flaggedCount={root.flaggedCount}
|
||||
selectSort={this.props.setSortOrder}
|
||||
sort={this.props.moderation.sortOrder}
|
||||
/>
|
||||
<ModerationQueue
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
currentAsset={asset}
|
||||
comments={comments}
|
||||
activeTab={activeTab}
|
||||
@@ -222,7 +155,7 @@ class ModerationContainer extends Component {
|
||||
rejectComment={props.rejectComment}
|
||||
loadMore={props.loadMore}
|
||||
assetId={providedAssetId}
|
||||
sort={this.state.sort}
|
||||
sort={this.props.moderation.sortOrder}
|
||||
commentCount={activeTabCount}
|
||||
currentUserId={this.props.auth.user.id}
|
||||
viewUserDetail={viewUserDetail}
|
||||
@@ -242,15 +175,15 @@ class ModerationContainer extends Component {
|
||||
open={moderation.suspendUserDialog.show}
|
||||
username={moderation.suspendUserDialog.username}
|
||||
userId={moderation.suspendUserDialog.userId}
|
||||
organizationName={data.settings.organizationName}
|
||||
organizationName={root.settings.organizationName}
|
||||
onCancel={props.hideSuspendUserDialog}
|
||||
onPerform={this.suspendUser}
|
||||
onPerform={this.props.suspendUser}
|
||||
/>
|
||||
<ModerationKeysModal
|
||||
hideShortcutsNote={props.hideShortcutsNote}
|
||||
shortcutsNoteVisible={moderation.shortcutsNoteVisible}
|
||||
open={moderation.modalOpen}
|
||||
onClose={onClose}/>
|
||||
onClose={this.onClose}/>
|
||||
{moderation.userDetailId && (
|
||||
<UserDetail
|
||||
id={moderation.userDetailId}
|
||||
@@ -261,35 +194,3 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
moderation: state.moderation.toJS(),
|
||||
settings: state.settings.toJS(),
|
||||
auth: state.auth.toJS(),
|
||||
assets: state.assets.get('assets')
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onClose: () => dispatch(toggleModal(false)),
|
||||
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
|
||||
...bindActionCreators({
|
||||
toggleModal,
|
||||
singleView,
|
||||
updateAssets,
|
||||
fetchSettings,
|
||||
showBanUserDialog,
|
||||
hideShortcutsNote,
|
||||
showSuspendUserDialog,
|
||||
hideSuspendUserDialog,
|
||||
viewUserDetail,
|
||||
hideUserDetail,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
setCommentStatus,
|
||||
getQueueCounts,
|
||||
banUser,
|
||||
suspendUser,
|
||||
modQueueQuery,
|
||||
)(ModerationContainer);
|
||||
+7
-6
@@ -1,13 +1,12 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
|
||||
import Comment from './components/Comment';
|
||||
import styles from './components/styles.css';
|
||||
import EmptyCard from '../../components/EmptyCard';
|
||||
import {actionsMap} from './helpers/moderationQueueActionsMap';
|
||||
import Comment from '../containers/Comment';
|
||||
import styles from './styles.css';
|
||||
import EmptyCard from '../../../components/EmptyCard';
|
||||
import {actionsMap} from '../helpers/moderationQueueActionsMap';
|
||||
import LoadMore from './LoadMore';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
import LoadMore from './components/LoadMore';
|
||||
|
||||
class ModerationQueue extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
@@ -54,6 +53,8 @@ class ModerationQueue extends React.Component {
|
||||
? comments.map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
return <Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
key={i}
|
||||
index={i}
|
||||
comment={comment}
|
||||
+10
-15
@@ -1,14 +1,13 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Button, Drawer} from 'coral-ui';
|
||||
import styles from './UserDetail.css';
|
||||
import {compose} from 'react-apollo';
|
||||
import {getUserDetail} from 'coral-admin/src/graphql/queries';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
class UserDetail extends React.Component {
|
||||
export default class UserDetail extends React.Component {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired
|
||||
hideUserDetail: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
copyPermalink = () => {
|
||||
@@ -22,13 +21,7 @@ class UserDetail extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {data, hideUserDetail} = this.props;
|
||||
|
||||
if (!('user' in data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {user, totalComments, rejectedComments} = data;
|
||||
const {root: {user, totalComments, rejectedComments}, hideUserDetail} = this.props;
|
||||
const localProfile = user.profiles.find((p) => p.provider === 'local');
|
||||
let profile;
|
||||
if (localProfile) {
|
||||
@@ -47,7 +40,12 @@ class UserDetail extends React.Component {
|
||||
<h3>{user.username}</h3>
|
||||
<Button className={styles.copyButton} onClick={this.copyPermalink}>Copy</Button>
|
||||
{profile && <input className={styles.profileEmail} readOnly type="text" ref={(ref) => this.profile = ref} value={profile} />}
|
||||
<Slot fill="userProfile" user={user} />
|
||||
<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>
|
||||
@@ -69,6 +67,3 @@ class UserDetail extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
getUserDetail
|
||||
)(UserDetail);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user