Styling, refactor and instant update after banning and unbanning from community

This commit is contained in:
Belen Curcio
2017-12-11 11:33:26 -03:00
parent 1d12c3629a
commit f005d1e5f8
12 changed files with 367 additions and 325 deletions
@@ -44,6 +44,7 @@
}
&:hover, &:active, &:focus {
background-color: #e2e2e2;
border-color: #616161;
}
&[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active {
background-color: #262626;
@@ -9,14 +9,13 @@ import ApproveButton from './ApproveButton';
import AccountHistory from './AccountHistory';
import {Slot} from 'coral-framework/components';
import UserDetailCommentList from '../components/UserDetailCommentList';
import {getReliability} from 'coral-framework/utils/user';
import {getReliability, isSuspended, isBanned} from 'coral-framework/utils/user';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import ClickOutside from 'coral-framework/components/ClickOutside';
import {Icon, Drawer, Spinner, TabBar, Tab, TabContent, TabPane} from 'coral-ui';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import UserInfoTooltip from './UserInfoTooltip';
import get from 'lodash/get';
class UserDetail extends React.Component {
@@ -91,12 +90,9 @@ class UserDetail extends React.Component {
getActionMenuLabel() {
const {root: {user}} = this.props;
const banned = get(user, 'state.status.banned.status');
const suspensionUntil = get(user, 'state.status.suspension.until');
if (banned) {
if (isBanned(user)) {
return 'Banned';
} else if (suspensionUntil && new Date(suspensionUntil) > new Date()) {
} else if (isSuspended(user)) {
return 'Suspended';
}
@@ -132,13 +128,8 @@ class UserDetail extends React.Component {
rejectedPercent = 0;
}
const banned = get(user, 'state.status.banned.status');
const suspensionUntil = get(user, 'state.status.suspension.until');
const suspended =
user &&
suspensionUntil &&
new Date(suspensionUntil) > new Date();
const banned = isBanned(user);
const suspended = isSuspended(user);
return (
<ClickOutside onClickOutside={hideUserDetail}>
+40
View File
@@ -3,8 +3,48 @@ import {mapLeaves} from 'coral-framework/utils';
export default {
mutations: {
BanUser: ({variables: {input: {id: userId}}}) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
const updated = update(prev, {
users: {
nodes: {
$apply: (nodes) => nodes.map((node) => {
if (node.id === userId) {
node.state.status.banned.status = true;
}
return node;
})
},
},
});
return updated;
}
}
}),
UnBanUser: ({variables: {input: {id: userId}}}) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
const updated = update(prev, {
users: {
nodes: {
$apply: (nodes) => nodes.map((node) => {
if (node.id === userId) {
node.state.status.banned.status = false;
}
return node;
})
},
},
});
return updated;
},
CoralAdmin_Moderation: (prev) => {
const updated = update(prev, {
@@ -12,7 +12,11 @@ class Community extends Component {
const activeTab = route.path === ':id' ? 'flagged' : route.path;
if (activeTab === 'people') {
return <People community={community} />;
return <People
community={community}
data={this.props.data}
root={this.props.root}
/>;
}
return (
@@ -1,27 +1,60 @@
import React from 'react';
import cn from 'classnames';
import styles from './styles.css';
import Table from './Table';
import {Icon} from 'coral-ui';
import {Icon, Dropdown, Option} from 'coral-ui';
import EmptyCard from '../../../components/EmptyCard';
import t from 'coral-framework/services/i18n';
import LoadMore from '../../../components/LoadMore';
import PropTypes from 'prop-types';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import {isSuspended, isBanned} from 'coral-framework/utils/user';
import moment from 'moment';
const headers = [
{
title: t('community.username_and_email'),
field: 'username'
},
{
title: t('community.account_creation_date'),
field: 'created_at'
},
{
title: t('community.status'),
field: 'status'
},
{
title: t('community.newsroom_role'),
field: 'role'
}
];
const getActionMenuLabel = (user) => {
if (isBanned(user)) {
return 'Banned';
} else if (isSuspended(user)) {
return 'Suspended';
}
return '';
};
const People = (props) => {
const {
users = [],
searchValue,
onSearchChange,
onHeaderClickHandler,
onPageChange,
totalPages,
page,
users = [],
setUserRole,
viewUserDetail,
loadMore,
unSuspendUser,
unBanUser,
showSuspendUserDialog,
showBanUserDialog,
} = props;
const hasResults = !!users.length;
const hasResults = !!users.nodes.length;
return (
<div className={cn(styles.container, 'talk-admin-community-people-container')}>
@@ -32,7 +65,7 @@ const People = (props) => {
id="commenters-search"
type="text"
className={styles.searchBoxInput}
value={searchValue}
defaultValue=''
onChange={onSearchChange}
placeholder={t('streams.search')}
/>
@@ -41,19 +74,88 @@ const People = (props) => {
<div className={styles.mainContent}>
{
hasResults
? <Table
users={users}
setUserRole={setUserRole}
viewUserDetail={viewUserDetail}
onHeaderClickHandler={onHeaderClickHandler}
pageCount={totalPages}
onPageChange={onPageChange}
page={page}
unBanUser={props.unBanUser}
unSuspendUser={props.unSuspendUser}
showSuspendUserDialog={props.showSuspendUserDialog}
showBanUserDialog={props.showBanUserDialog}
/>
? <div>
<div>
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) =>(
<th
key={i}
className={cn('mdl-data-table__cell--non-numeric', styles.header)}
scope="col" >
{header.title}
</th>
))}
</tr>
</thead>
<tbody>
{users.nodes.map((user, i)=> (
<tr key={i} className="talk-admin-community-people-row">
<td className="mdl-data-table__cell--non-numeric">
<button onClick={() => {viewUserDetail(user.id);}} className={cn(styles.username, styles.button)}>{user.username}</button>
<span className={styles.email}>{user.profiles.map(({id}) => id)}</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
{moment(new Date(user.created_at)).format('MMMM Do YYYY, h:mm:ss a')}
</td>
<td className="mdl-data-table__cell--non-numeric">
<ActionsMenu
icon="person"
className={cn(styles.actionsMenu, 'talk-admin-community-people-dd-status')}
buttonClassNames={cn(styles.actionsMenuButton, {
[styles.actionsMenuSuspended]: isSuspended(user),
[styles.actionsMenuBanned]: isBanned(user),
}, 'talk-admin-user-detail-actions-button')}
label={getActionMenuLabel(user)} >
{isSuspended(user) ? <ActionsMenuItem
onClick={() => unSuspendUser({id: user.id})}>
Remove Suspension
</ActionsMenuItem> : <ActionsMenuItem
onClick={() => showSuspendUserDialog({
userId: user.id,
username: user.username,
})}>
Suspend User
</ActionsMenuItem>}
{isBanned(user) ? <ActionsMenuItem
onClick={() => unBanUser({id: user.id})}>
Remove Ban
</ActionsMenuItem> : <ActionsMenuItem
onClick={() => showBanUserDialog({
userId: user.id,
username: user.username,
})}>
Ban User
</ActionsMenuItem>}
</ActionsMenu>
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-role"
value={user.role}
placeholder={t('community.role')}
onChange={(role) => setUserRole(user.id, role)}>
<Option value={'COMMENTER'} label={t('community.commenter')} />
<Option value={'STAFF'} label={t('community.staff')} />
<Option value={'MODERATOR'} label={t('community.moderator')} />
<Option value={'ADMIN'} label={t('community.admin')} />
</Dropdown>
</td>
</tr>
))}
</tbody>
</table>
</div>
<LoadMore
loadMore={loadMore}
showLoadMore={users.hasNextPage}
/>
</div>
: <EmptyCard>{t('community.no_results')}</EmptyCard>
}
</div>
@@ -62,19 +164,15 @@ const People = (props) => {
};
People.propTypes = {
onHeaderClickHandler: PropTypes.func,
users: PropTypes.array,
page: PropTypes.number.isRequired,
searchValue: PropTypes.string,
onSearchChange: PropTypes.func,
totalPages: PropTypes.number,
onPageChange: PropTypes.func,
users: PropTypes.object.isRequired,
onSearchChange: PropTypes.func.isRequired,
setUserRole: PropTypes.func.isRequired,
viewUserDetail: PropTypes.func.isRequired,
unBanUser: PropTypes.func.isRequired,
unSuspendUser: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
loadMore: PropTypes.func.isRequired,
};
export default People;
@@ -1,69 +0,0 @@
.dataTable {
width: 100%;
border-left: none;
border-right: none;
}
th.header {
font-size: 1.1em;
}
th.header:hover {
cursor: pointer;
}
th.header:nth-child(2), th.header:nth-child(3) {
width: 100px;
}
.button {
composes: buttonReset from 'coral-framework/styles/reset.css';
&:hover {
background-color: #D0D0D0;
}
}
.username, .email {
max-width: 215px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.email {
display: block;
}
.selectField {
position: relative;
width: 150px;
height: 36px;
background: #2c2c2c;
padding: 10px 15px;
box-sizing: border-box;
color: white;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
> div {
padding: 0;
}
i {
position: absolute;
top: 7px;
right: 7px;
}
input {
padding: 0;
font-size: 13px;
letter-spacing: 0.7px;
font-weight: 400;
}
&:hover {
cursor: pointer;
}
}
@@ -1,145 +0,0 @@
import React from 'react';
import styles from './Table.css';
import t from 'coral-framework/services/i18n';
import PropTypes from 'prop-types';
import {Paginate, Dropdown, Option} from 'coral-ui';
import cn from 'classnames';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import {isSuspended, isBanned} from 'coral-framework/utils/user';
const headers = [
{
title: t('community.username_and_email'),
field: 'username'
},
{
title: t('community.account_creation_date'),
field: 'created_at'
},
{
title: t('community.status'),
field: 'status'
},
{
title: t('community.newsroom_role'),
field: 'role'
}
];
const Table = (props) => {
const {
users,
setUserRole,
onHeaderClickHandler,
viewUserDetail,
pageCount,
page,
onPageChange,
unSuspendUser,
unBanUser,
showSuspendUserDialog,
showBanUserDialog,
} = props;
return (
<div>
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) =>(
<th
key={i}
className={cn('mdl-data-table__cell--non-numeric', styles.header)}
scope="col"
onClick={() => onHeaderClickHandler({field: header.field})}>
{header.title}
</th>
))}
</tr>
</thead>
<tbody>
{users.map((user, i)=> (
<tr key={i} className="talk-admin-community-people-row">
<td className="mdl-data-table__cell--non-numeric">
<button onClick={() => {viewUserDetail(user.id);}} className={cn(styles.username, styles.button)}>{user.username}</button>
<span className={styles.email}>{user.profiles.map(({id}) => id)}</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
{user.created_at}
</td>
<td className="mdl-data-table__cell--non-numeric">
<ActionsMenu
icon="person"
className={cn(styles.actionsMenu, 'talk-admin-community-people-dd-status')}
buttonClassNames={cn({
[styles.actionsMenuSuspended]: isSuspended(user),
[styles.actionsMenuBanned]: isBanned(user),
}, 'talk-admin-user-detail-actions-button')} >
{isSuspended(user) ? <ActionsMenuItem
onClick={() => unSuspendUser({id: user.id})}>
Remove Suspension
</ActionsMenuItem> : <ActionsMenuItem
onClick={() => showSuspendUserDialog({
userId: user.id,
username: user.username,
})}>
Suspend User
</ActionsMenuItem>}
{isBanned(user) ? <ActionsMenuItem
onClick={() => unBanUser({id: user.id})}>
Remove Ban
</ActionsMenuItem> : <ActionsMenuItem
onClick={() => showBanUserDialog({
userId: user.id,
username: user.username,
})}>
Ban User
</ActionsMenuItem>}
</ActionsMenu>
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-role"
value={user.role}
placeholder={t('community.role')}
onChange={(role) => setUserRole(user.id, role)}>
<Option value={'COMMENTER'} label={t('community.commenter')} />
<Option value={'STAFF'} label={t('community.staff')} />
<Option value={'MODERATOR'} label={t('community.moderator')} />
<Option value={'ADMIN'} label={t('community.admin')} />
</Dropdown>
</td>
</tr>
))}
</tbody>
</table>
<Paginate
pageCount={pageCount}
page={page - 1}
onPageChange={onPageChange}
/>
</div>
);
};
Table.propTypes = {
users: PropTypes.array,
onHeaderClickHandler: PropTypes.func.isRequired,
setUserRole: PropTypes.func.isRequired,
viewUserDetail: PropTypes.func.isRequired,
pageCount: PropTypes.number.isRequired,
page: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
unBanUser: PropTypes.func.isRequired,
unSuspendUser: PropTypes.func.isRequired,
};
export default Table;
@@ -240,3 +240,97 @@
}
}
/* ========================================================================== */
/* TABLE */
/* ========================================================================== */
.dataTable {
width: 100%;
border-left: none;
border-right: none;
}
th.header {
font-size: 1.1em;
}
th.header:hover {
cursor: pointer;
}
th.header:nth-child(2), th.header:nth-child(3) {
width: 100px;
}
.button {
composes: buttonReset from 'coral-framework/styles/reset.css';
&:hover {
background-color: #D0D0D0;
}
}
.username, .email {
max-width: 215px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.email {
display: block;
}
.selectField {
position: relative;
width: 150px;
height: 36px;
background: #2c2c2c;
padding: 10px 15px;
box-sizing: border-box;
color: white;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
> div {
padding: 0;
}
i {
position: absolute;
top: 7px;
right: 7px;
}
input {
padding: 0;
font-size: 13px;
letter-spacing: 0.7px;
font-weight: 400;
}
&:hover {
cursor: pointer;
}
}
.actionsMenu {
font-size: 1em;
}
.actionsMenu .actionsMenuButton {
-webkit-transform: scale(1);
transform: scale(1);
font-size: 0.98em;
}
.actionsMenuSuspended {
background-color: #F29336;
border-color: #F29336;
color: white;
}
.actionsMenuBanned {
background-color: #E45241;
border-color: #E45241;
color: white;
}
@@ -39,9 +39,9 @@ const withData = withQuery(gql`
__typename
}
}
${People.fragments.root}
${FlaggedAccounts.fragments.root}
${FlaggedUser.fragments.root}
${People.fragments.root}
${FlaggedUser.fragments.me}
`, {
options: {
@@ -9,59 +9,29 @@ import {withUnBanUser, withUnSuspendUser, withSetUserRole} from 'coral-framework
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {viewUserDetail} from '../../../actions/userDetail';
import {
fetchUsers,
updateSorting,
setPage,
hideRejectUsernameDialog,
setSearchValue,
} from '../../../actions/community';
import {appendNewNodes} from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import {Spinner} from 'coral-ui';
class PeopleContainer extends React.Component {
timer = null;
fetchUsers = (query = {}) => {
const {community} = this.props;
this.props.fetchUsers({
value: community.searchValue,
field: community.fieldPeople,
asc: community.ascPeople,
...query
});
}
componentWillMount() {
this.fetchUsers();
}
onKeyDownHandler = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.fetchUsers();
// this.fetchUsers();
}
}
onSearchChange = (e) => {
const value = e.target.value;
console.log(value);
this.props.setSearchValue(value);
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.fetchUsers({value});
}, 350);
}
// clearTimeout(this.timer);
onHeaderClickHandler = (sort) => {
this.props.updateSorting(sort);
this.fetchUsers();
}
onPageChange = ({selected}) => {
const page = selected + 1;
this.props.setPage(page);
this.fetchUsers({page});
// this.timer = setTimeout(() => {
// // this.fetchUsers({value});
// }, 350);
}
setUserRole = async (id, role) => {
@@ -69,51 +39,92 @@ class PeopleContainer extends React.Component {
await this.fetchUsers();
}
loadMore = () => {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
limit: 5,
cursor: this.props.root.users.endCursor,
},
updateQuery: (previous, {fetchMoreResult:{users}}) => {
const updated = update(previous, {
flaggedUsers: {
nodes: {
$apply: (nodes) => appendNewNodes(nodes, users.nodes),
},
hasNextPage: {$set: users.hasNextPage},
endCursor: {$set: users.endCursor},
},
});
return updated;
},
});
};
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (this.props.data.loading) {
return <div><Spinner/></div>;
}
return <People
users={this.props.community.users}
searchValue={this.props.community.searchValue}
onSearchChange={this.onSearchChange}
onHeaderClickHandler={this.onHeaderClickHandler}
onPageChange={this.onPageChange}
totalPages={this.props.community.totalPagesPeople}
setUserRole={this.setUserRole}
page={this.props.community.pagePeople}
onSearchChange={this.onSearchChange}
viewUserDetail={this.props.viewUserDetail}
setUserRole={this.setUserRole}
showSuspendUserDialog={this.props.showSuspendUserDialog}
showBanUserDialog={this.props.showBanUserDialog}
unBanUser={this.props.unBanUser}
unSuspendUser={this.props.unBanUser}
data={this.props.data}
root={this.props.root}
users={this.props.root.users}
loadMore={this.loadMore}
/>;
}
}
PeopleContainer.propTypes = {
setPage: PropTypes.func,
fetchUsers: PropTypes.func,
updateSorting: PropTypes.func,
community: PropTypes.object,
setUserRole: PropTypes.func.isRequired,
unBanUser: PropTypes.func.isRequired,
unSuspendUser: PropTypes.func.isRequired,
setSearchValue: PropTypes.func.isRequired,
viewUserDetail: PropTypes.func.isRequired,
community: PropTypes.object,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
};
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
setPage,
fetchUsers,
updateSorting,
hideRejectUsernameDialog,
viewUserDetail,
setSearchValue,
showSuspendUserDialog,
showBanUserDialog,
}, dispatch);
const LOAD_MORE_QUERY = gql`
query TalkAdminCommunity_People_LoadMoreUsers($limit: Int, $cursor: Cursor) {
users(query: {}){
hasNextPage
endCursor
nodes {
__typename
id
username
created_at
profiles {
id
provider
}
}
}
}
`;
export default compose(
connect(null, mapDispatchToProps),
withSetUserRole,
@@ -128,10 +139,26 @@ export default compose(
nodes {
__typename
id
username
role
created_at
profiles {
id
provider
}
state {
status {
banned {
status
}
suspension {
until
}
}
}
}
}
}
`,
}),
)(PeopleContainer);
+1
View File
@@ -150,6 +150,7 @@
box-shadow: none;
color: white;
background-color: #616161;
border-color: transparent;
}
> .icon {
+6 -6
View File
@@ -1,19 +1,19 @@
.dropdown {
position: relative;
width: 150px;
height: 36px;
height: 34px;
background: #2c2c2c;
box-sizing: border-box;
color: white;
border-radius: 2px;
border-radius: 3px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
line-height: 1.4;
font-size: 13px;
font-size: 0.98em;
border-radius: 3px;
}
.toggle {
padding: 10px 15px;
padding: 8px 15px;
cursor: pointer;
outline: none;
@@ -58,5 +58,5 @@
right: 15px;
font-size: 1.2rem;
vertical-align: middle;
top: 13px;
top: 11px;
}