Admin - rework user list/user detail (#80)
* fix UserCommentSchema to no longer exclude contributions (no longer on ProposalSchema) * return more detail for /admin/users/<id> * admin: add UserDetail + refactoring * remove unused state from UserItem.tsx
This commit is contained in:
parent
48912c95cc
commit
c0557a9fa6
|
@ -8,6 +8,7 @@ import store from './store';
|
|||
import Login from 'components/Login';
|
||||
import Home from 'components/Home';
|
||||
import Users from 'components/Users';
|
||||
import UserDetail from 'components/UserDetail';
|
||||
import Emails from 'components/Emails';
|
||||
import Proposals from 'components/Proposals';
|
||||
import ProposalDetail from 'components/ProposalDetail';
|
||||
|
@ -29,7 +30,8 @@ class Routes extends React.Component<Props> {
|
|||
) : (
|
||||
<Switch>
|
||||
<Route path="/" exact={true} component={Home} />
|
||||
<Route path="/users/:id?" exact={true} component={Users} />
|
||||
<Route path="/users/:id" component={UserDetail} />
|
||||
<Route path="/users" component={Users} />
|
||||
<Route path="/proposals/:id" component={ProposalDetail} />
|
||||
<Route path="/proposals" component={Proposals} />
|
||||
<Route path="/emails/:type?" component={Emails} />
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
h1.Back {
|
||||
font-size: 1.5rem !important;
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
top: -0.25rem;
|
||||
font-size: 0.8rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Icon } from 'antd';
|
||||
import './index.less';
|
||||
|
||||
interface Props {
|
||||
to: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const Back: React.SFC<Props> = p => (
|
||||
<h1 className="Back">
|
||||
<Link to={p.to}>
|
||||
<Icon type="arrow-left" />
|
||||
</Link>
|
||||
{p.text}
|
||||
</h1>
|
||||
);
|
||||
|
||||
export default Back;
|
|
@ -7,8 +7,9 @@ import store from 'src/store';
|
|||
import { formatDateSeconds } from 'util/time';
|
||||
import { PROPOSAL_STATUS } from 'src/types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './index.less';
|
||||
import Back from 'components/Back';
|
||||
import Markdown from 'components/Markdown';
|
||||
import './index.less';
|
||||
|
||||
type Props = RouteComponentProps<any>;
|
||||
|
||||
|
@ -155,6 +156,7 @@ class ProposalDetailNaked extends React.Component<Props, State> {
|
|||
|
||||
return (
|
||||
<div className="ProposalDetail">
|
||||
<Back to="/proposals" text="Proposals" />
|
||||
<h1>{p.title}</h1>
|
||||
<Row gutter={16}>
|
||||
{/* MAIN */}
|
||||
|
@ -198,9 +200,9 @@ class ProposalDetailNaked extends React.Component<Props, State> {
|
|||
{/* TEAM */}
|
||||
<Card title="Team" size="small">
|
||||
{p.team.map(t => (
|
||||
<Link key={t.userid} to={`/users/${t.userid}`}>
|
||||
{t.displayName}
|
||||
</Link>
|
||||
<div key={t.userid}>
|
||||
<Link to={`/users/${t.userid}`}>{t.displayName}</Link>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
{/* TODO: contributors here? */}
|
||||
|
|
|
@ -19,7 +19,7 @@ class ProposalItemNaked extends React.Component<Proposal> {
|
|||
const deleteAction = (
|
||||
<Popconfirm
|
||||
onConfirm={this.handleDelete}
|
||||
title="Permanently delete proposal?"
|
||||
title="Are you sure?"
|
||||
okText="delete"
|
||||
cancelText="cancel"
|
||||
>
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import React from 'react';
|
||||
import qs from 'query-string';
|
||||
import { uniq, without } from 'lodash';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { view } from 'react-easy-state';
|
||||
import { Icon, Button, Dropdown, Menu, Tag, List } from 'antd';
|
||||
import { ClickParam } from 'antd/lib/menu';
|
||||
import { RouteComponentProps, withRouter } from 'react-router';
|
||||
import store from 'src/store';
|
||||
import ProposalItem from './ProposalItem';
|
||||
import { PROPOSAL_STATUS, Proposal } from 'src/types';
|
||||
import STATUSES, { getStatusById } from './STATUSES';
|
||||
import { ClickParam } from 'antd/lib/menu';
|
||||
import './index.less';
|
||||
|
||||
interface Query {
|
||||
|
@ -32,34 +31,9 @@ class ProposalsNaked extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const id = Number(this.props.match.params.id);
|
||||
const { proposals, proposalsFetching, proposalsFetched } = store;
|
||||
const { statusFilters } = this.state;
|
||||
|
||||
if (!proposalsFetched) {
|
||||
return 'loading proposals...';
|
||||
}
|
||||
|
||||
if (id) {
|
||||
const singleProposal = proposals.find(p => p.proposalId === id);
|
||||
if (singleProposal) {
|
||||
return (
|
||||
<div className="Proposals">
|
||||
<div className="Proposals-controls">
|
||||
<Link to="/proposals">proposals</Link> <Icon type="right" /> {id}{' '}
|
||||
<Button
|
||||
title="refresh"
|
||||
icon="reload"
|
||||
onClick={() => store.fetchProposals()}
|
||||
/>
|
||||
</div>
|
||||
<ProposalItem key={singleProposal.proposalId} {...singleProposal} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return `could not find proposal: ${id}`;
|
||||
}
|
||||
}
|
||||
const loading = !proposalsFetched || proposalsFetching;
|
||||
|
||||
const statusFilterMenu = (
|
||||
<Menu onClick={this.handleFilterClick}>
|
||||
|
@ -99,16 +73,14 @@ class ProposalsNaked extends React.Component<Props, State> {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{proposalsFetching && 'Fetching proposals...'}
|
||||
{proposalsFetched &&
|
||||
!proposalsFetching && (
|
||||
<List
|
||||
className="Proposals-list"
|
||||
bordered
|
||||
dataSource={proposals}
|
||||
renderItem={(p: Proposal) => <ProposalItem key={p.proposalId} {...p} />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<List
|
||||
className="Proposals-list"
|
||||
bordered
|
||||
dataSource={proposals}
|
||||
loading={loading}
|
||||
renderItem={(p: Proposal) => <ProposalItem key={p.proposalId} {...p} />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
.UserDetail {
|
||||
h1 {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
& > * + * {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-comment {
|
||||
color: black;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
&-deet {
|
||||
position: relative;
|
||||
margin-bottom: 0.9rem;
|
||||
|
||||
& > div {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 0.7rem;
|
||||
position: absolute;
|
||||
bottom: -0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-card,
|
||||
.ant-alert,
|
||||
.ant-collapse {
|
||||
margin-bottom: 16px;
|
||||
|
||||
button + button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
import React from 'react';
|
||||
import { view } from 'react-easy-state';
|
||||
import { RouteComponentProps, withRouter } from 'react-router';
|
||||
import { Row, Col, Card, Button, Collapse, Popconfirm, Avatar, List } from 'antd';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import store from 'src/store';
|
||||
import { Proposal, Comment, Contribution } from 'src/types';
|
||||
import { formatDateSeconds, formatDateMs } from 'util/time';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Back from 'components/Back';
|
||||
import './index.less';
|
||||
import Markdown from 'components/Markdown';
|
||||
|
||||
type Props = RouteComponentProps<any>;
|
||||
|
||||
const STATE = {};
|
||||
|
||||
type State = typeof STATE;
|
||||
|
||||
class UserDetailNaked extends React.Component<Props, State> {
|
||||
state = STATE;
|
||||
rejectInput: null | TextArea = null;
|
||||
componentDidMount() {
|
||||
this.loadDetail();
|
||||
}
|
||||
render() {
|
||||
const id = this.getIdFromQuery();
|
||||
const { userDetail: u, userDetailFetching } = store;
|
||||
|
||||
if (!u || (u && u.userid !== id) || userDetailFetching) {
|
||||
return 'loading user...';
|
||||
}
|
||||
|
||||
const renderDelete = () => (
|
||||
<Popconfirm
|
||||
onConfirm={this.handleDelete}
|
||||
title="Delete user?"
|
||||
okText="delete"
|
||||
cancelText="cancel"
|
||||
>
|
||||
<Button icon="delete" block>
|
||||
Delete
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
|
||||
const renderDeetItem = (name: string, val: any) => (
|
||||
<div className="UserDetail-deet">
|
||||
<span>{name}</span>
|
||||
<div>{(val && val) || 'n/a'}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="UserDetail">
|
||||
<Back to="/users" text="Users" />
|
||||
<h1>
|
||||
<Avatar
|
||||
size="default"
|
||||
shape="square"
|
||||
icon="user"
|
||||
src={(u.avatar && u.avatar.imageUrl) || ''}
|
||||
/>
|
||||
<span>
|
||||
{u.displayName} - {u.emailAddress}
|
||||
</span>
|
||||
</h1>
|
||||
<Row gutter={16}>
|
||||
{/* MAIN */}
|
||||
<Col span={18}>
|
||||
{/* DETAILS */}
|
||||
<Card title="details" size="small">
|
||||
{renderDeetItem('userid', u.userid)}
|
||||
{renderDeetItem('displayName', u.displayName)}
|
||||
{renderDeetItem('emailAddress', u.emailAddress)}
|
||||
{renderDeetItem('title', u.title)}
|
||||
{(u.socialMedias.length > 0 &&
|
||||
u.socialMedias.map((sm, idx) => (
|
||||
<div key={sm.service}>
|
||||
{renderDeetItem(
|
||||
`socialMedias[${idx}]`,
|
||||
<>
|
||||
{sm.service}/{sm.username} <a href={sm.url}>{sm.url}</a>
|
||||
</>,
|
||||
)}
|
||||
</div>
|
||||
))) ||
|
||||
renderDeetItem('socialMedias', '')}
|
||||
{renderDeetItem(
|
||||
'avatar.imageUrl',
|
||||
u.avatar && <a href={u.avatar.imageUrl}>{u.avatar.imageUrl}</a>,
|
||||
)}
|
||||
</Card>
|
||||
<Collapse defaultActiveKey={['proposals', 'contributions']}>
|
||||
{/* PROPOSALS */}
|
||||
<Collapse.Panel
|
||||
key="proposals"
|
||||
header={`proposals (${u.proposals.length})`}
|
||||
>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={u.proposals}
|
||||
renderItem={(p: Proposal) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Link key="view" to={`/proposals/${p.proposalId}`}>
|
||||
view
|
||||
</Link>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta title={p.title} description={p.brief} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
|
||||
{/* CONTRIBUTIONS */}
|
||||
<Collapse.Panel
|
||||
key="contributions"
|
||||
header={`contributions (${u.contributions.length})`}
|
||||
>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={u.contributions}
|
||||
renderItem={(c: Contribution) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<div>
|
||||
<b>{c.amount}</b>
|
||||
ZEC to{' '}
|
||||
<Link to={`/proposals/${c.proposal.proposalId}`}>
|
||||
{c.proposal.title}
|
||||
</Link>{' '}
|
||||
on {formatDateSeconds(c.dateCreated)}
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<>
|
||||
id: {c.id}, status: {c.status}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
|
||||
{/* COMMENTS */}
|
||||
<Collapse.Panel key="comments" header={`comments (${u.comments.length})`}>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={u.comments}
|
||||
renderItem={(c: Comment) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
description={
|
||||
<>
|
||||
<div>
|
||||
on{' '}
|
||||
<Link to={`/proposals/${c.proposalId}`}>
|
||||
{c.proposal && c.proposal.title}
|
||||
</Link>{' '}
|
||||
at {formatDateMs(c.dateCreated)}
|
||||
</div>
|
||||
<Markdown source={c.content} className="UserDetail-comment" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
|
||||
{/* JSON */}
|
||||
<Collapse.Panel key="json" header="json">
|
||||
<pre>{JSON.stringify(u, null, 4)}</pre>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</Col>
|
||||
|
||||
{/* SIDE */}
|
||||
<Col span={6}>
|
||||
{/* ACTIONS */}
|
||||
<Card size="small">{renderDelete()}</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private getIdFromQuery = () => {
|
||||
return Number(this.props.match.params.id);
|
||||
};
|
||||
|
||||
private loadDetail = () => {
|
||||
store.fetchUserDetail(this.getIdFromQuery());
|
||||
};
|
||||
|
||||
private handleDelete = () => {
|
||||
if (!store.userDetail) return;
|
||||
store.deleteUser(store.userDetail.userid);
|
||||
};
|
||||
}
|
||||
|
||||
const UserDetail = withRouter(view(UserDetailNaked));
|
||||
export default UserDetail;
|
|
@ -0,0 +1,2 @@
|
|||
.UserItem {
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { view } from 'react-easy-state';
|
||||
import { Popconfirm, List, Avatar } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
import store from 'src/store';
|
||||
import { User } from 'src/types';
|
||||
import './UserItem.less';
|
||||
|
||||
class UserItemNaked extends React.Component<User> {
|
||||
render() {
|
||||
const p = this.props;
|
||||
|
||||
const deleteAction = (
|
||||
<Popconfirm
|
||||
onConfirm={this.handleDelete}
|
||||
title="Are you sure?"
|
||||
okText="delete"
|
||||
cancelText="cancel"
|
||||
>
|
||||
<div>delete</div>
|
||||
</Popconfirm>
|
||||
);
|
||||
const viewAction = <Link to={`/users/${p.userid}`}>view</Link>;
|
||||
const actions = [viewAction, deleteAction];
|
||||
|
||||
return (
|
||||
<List.Item key={p.userid} className="UserItem" actions={actions}>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<Avatar
|
||||
shape="square"
|
||||
icon="user"
|
||||
src={(p.avatar && p.avatar.imageUrl) || ''}
|
||||
/>
|
||||
}
|
||||
title={p.displayName}
|
||||
description={p.emailAddress}
|
||||
/>
|
||||
</List.Item>
|
||||
);
|
||||
}
|
||||
private handleDelete = () => {
|
||||
store.deleteUser(this.props.userid);
|
||||
};
|
||||
}
|
||||
|
||||
const UserItem = view(UserItemNaked);
|
||||
export default UserItem;
|
|
@ -1,57 +1,12 @@
|
|||
@controls-height: 40px;
|
||||
|
||||
.Users {
|
||||
margin-top: @controls-height + 0.5rem;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
&-controls {
|
||||
height: @controls-height;
|
||||
padding: 0.25rem 1rem;
|
||||
margin-left: -1rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 216px;
|
||||
z-index: 5;
|
||||
background: white;
|
||||
margin-bottom: 0.5rem;
|
||||
& > * {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-user {
|
||||
display: flex;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid rgb(214, 214, 214);
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&-controls {
|
||||
margin: 0 0.5rem 0.5rem 0;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
padding: 0.1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-right: 0.5rem;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
|
||||
& img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& button {
|
||||
cursor: pointer;
|
||||
margin: 0 0.3rem 0 0;
|
||||
outline: none !important;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
&-list {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import React from 'react';
|
||||
import { view } from 'react-easy-state';
|
||||
import { Button, Popover, Icon } from 'antd';
|
||||
import { Button, List } from 'antd';
|
||||
import { RouteComponentProps, withRouter } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import store from 'src/store';
|
||||
import { User } from 'src/types';
|
||||
import Field from 'components/Field';
|
||||
import UserItem from './UserItem';
|
||||
import './index.less';
|
||||
|
||||
type Props = RouteComponentProps<any>;
|
||||
|
@ -16,123 +15,25 @@ class UsersNaked extends React.Component<Props> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const id = parseInt(this.props.match.params.id, 10);
|
||||
const { users, usersFetched } = store;
|
||||
|
||||
if (!usersFetched) {
|
||||
return 'loading users...';
|
||||
}
|
||||
|
||||
if (id) {
|
||||
const singleUser = users.find(u => u.userid === id);
|
||||
if (singleUser) {
|
||||
return (
|
||||
<div className="Users">
|
||||
<div className="Users-controls">
|
||||
<Link to="/users">users</Link> <Icon type="right" /> {id}{' '}
|
||||
<Button title="refresh" icon="reload" onClick={() => store.fetchUsers()} />
|
||||
</div>
|
||||
<UserItem key={singleUser.userid} {...singleUser} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return `could not find user: ${id}`;
|
||||
}
|
||||
}
|
||||
const { users, usersFetched, usersFetching } = store;
|
||||
const loading = !usersFetched || usersFetching;
|
||||
|
||||
return (
|
||||
<div className="Users">
|
||||
<div className="Users-controls">
|
||||
<Button title="refresh" icon="reload" onClick={() => store.fetchUsers()} />
|
||||
</div>
|
||||
{users.length === 0 && <div>no users</div>}
|
||||
{users.length > 0 && users.map(u => <UserItem key={u.userid} {...u} />)}
|
||||
<List
|
||||
className="Users-list"
|
||||
bordered
|
||||
dataSource={users}
|
||||
loading={loading}
|
||||
renderItem={(u: User) => <UserItem key={u.userid} {...u} />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:max-classes-per-file
|
||||
class UserItemNaked extends React.Component<User> {
|
||||
state = {
|
||||
showProposals: false,
|
||||
activeProposal: '',
|
||||
showDelete: false,
|
||||
};
|
||||
render() {
|
||||
const u = this.props;
|
||||
return (
|
||||
<div key={u.userid} className="Users-user">
|
||||
<div>
|
||||
<div className="Users-user-controls">
|
||||
<Popover
|
||||
content={
|
||||
<div>
|
||||
<Button type="primary" onClick={this.handleDelete}>
|
||||
delete {u.emailAddress}
|
||||
</Button>{' '}
|
||||
<Button onClick={() => this.setState({ showDelete: false })}>
|
||||
cancel
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
title="Permanently delete user?"
|
||||
trigger="click"
|
||||
visible={this.state.showDelete}
|
||||
onVisibleChange={showDelete => this.setState({ showDelete })}
|
||||
>
|
||||
<Button icon="delete" shape="circle" size="small" title="delete" />
|
||||
</Popover>
|
||||
{/* TODO: implement silence user on BE */}
|
||||
<Button
|
||||
icon="notification"
|
||||
shape="circle"
|
||||
size="small"
|
||||
title={false ? 'allow commenting' : 'disable commenting'}
|
||||
type={false ? 'danger' : 'default'}
|
||||
disabled={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="Users-user-img">
|
||||
{u.avatar ? <img src={u.avatar.imageUrl} /> : 'n/a'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field title="displayName" value={u.displayName} />
|
||||
<Field title="title" value={u.title} />
|
||||
<Field title="emailAddress" value={u.emailAddress} />
|
||||
<Field title="userid" value={u.userid} />
|
||||
<Field
|
||||
title="avatar.imageUrl"
|
||||
value={(u.avatar && u.avatar.imageUrl) || 'n/a'}
|
||||
/>
|
||||
<Field
|
||||
title={`proposals (${u.proposals.length})`}
|
||||
value={
|
||||
<div className="Users-user-proposals">
|
||||
{u.proposals.map(p => (
|
||||
<div key={p.proposalId}>
|
||||
{p.title} (
|
||||
<Link to={`/proposals/${p.proposalId}`}>{p.proposalId}</Link>)
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Field
|
||||
title={`comments (${u.comments.length})`}
|
||||
value={<div>TODO: comments</div>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private handleDelete = () => {
|
||||
store.deleteUser(this.props.userid);
|
||||
};
|
||||
}
|
||||
const UserItem = view(UserItemNaked);
|
||||
|
||||
const Users = withRouter(view(UsersNaked));
|
||||
export default Users;
|
||||
|
|
|
@ -36,6 +36,11 @@ async function fetchUsers() {
|
|||
return data;
|
||||
}
|
||||
|
||||
async function fetchUserDetail(id: number) {
|
||||
const { data } = await api.get(`/admin/users/${id}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function deleteUser(id: number | string) {
|
||||
const { data } = await api.delete('/admin/users/' + id);
|
||||
return data;
|
||||
|
@ -84,14 +89,20 @@ const app = store({
|
|||
proposalCount: 0,
|
||||
proposalPendingCount: 0,
|
||||
},
|
||||
|
||||
usersFetching: false,
|
||||
usersFetched: false,
|
||||
users: [] as User[],
|
||||
userDetailFetching: false,
|
||||
userDetail: null as null | User,
|
||||
|
||||
proposalsFetching: false,
|
||||
proposalsFetched: false,
|
||||
proposals: [] as Proposal[],
|
||||
proposalDetailFetching: false,
|
||||
proposalDetail: null as null | Proposal,
|
||||
proposalDetailApproving: false,
|
||||
|
||||
emailExamples: {} as { [type: string]: EmailExample },
|
||||
|
||||
removeGeneralError(i: number) {
|
||||
|
@ -141,12 +152,24 @@ const app = store({
|
|||
},
|
||||
|
||||
async fetchUsers() {
|
||||
app.usersFetching = true;
|
||||
try {
|
||||
app.users = await fetchUsers();
|
||||
app.usersFetched = true;
|
||||
} catch (e) {
|
||||
handleApiError(e);
|
||||
}
|
||||
app.usersFetching = false;
|
||||
},
|
||||
|
||||
async fetchUserDetail(id: number) {
|
||||
app.userDetailFetching = true;
|
||||
try {
|
||||
app.userDetail = await fetchUserDetail(id);
|
||||
} catch (e) {
|
||||
handleApiError(e);
|
||||
}
|
||||
app.userDetailFetching = false;
|
||||
},
|
||||
|
||||
async deleteUser(id: string | number) {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// backend
|
||||
export interface SocialMedia {
|
||||
socialMediaLink: string;
|
||||
url: string;
|
||||
service: string;
|
||||
username: string;
|
||||
}
|
||||
export interface Milestone {
|
||||
content: string;
|
||||
|
@ -41,9 +43,20 @@ export interface Proposal {
|
|||
}
|
||||
export interface Comment {
|
||||
commentId: string;
|
||||
dateCreated: string;
|
||||
proposalId: Proposal['proposalId'];
|
||||
proposal?: Proposal;
|
||||
dateCreated: number;
|
||||
content: string;
|
||||
}
|
||||
export interface Contribution {
|
||||
id: number;
|
||||
status: string;
|
||||
txId: null | string;
|
||||
amount: string;
|
||||
dateCreated: number;
|
||||
user: User;
|
||||
proposal: Proposal;
|
||||
}
|
||||
export interface User {
|
||||
accountAddress: string;
|
||||
avatar: null | { imageUrl: string };
|
||||
|
@ -54,6 +67,7 @@ export interface User {
|
|||
userid: number;
|
||||
proposals: Proposal[];
|
||||
comments: Comment[];
|
||||
contributions: Contribution[];
|
||||
}
|
||||
|
||||
export interface EmailExample {
|
||||
|
|
|
@ -5,3 +5,7 @@ const DATE_FMT_STRING = 'MM/DD/YYYY h:mm a';
|
|||
export const formatDateSeconds = (s: number) => {
|
||||
return moment(s * 1000).format(DATE_FMT_STRING);
|
||||
};
|
||||
|
||||
export const formatDateMs = (s: number) => {
|
||||
return moment(s).format(DATE_FMT_STRING);
|
||||
};
|
||||
|
|
|
@ -7,9 +7,16 @@ from flask_cors import CORS, cross_origin
|
|||
from sqlalchemy import func, or_
|
||||
|
||||
from grant.extensions import db
|
||||
from grant.user.models import User, users_schema
|
||||
from grant.proposal.models import Proposal, proposals_schema, proposal_schema, PENDING
|
||||
from grant.comment.models import Comment, comments_schema
|
||||
from grant.user.models import User, users_schema, user_schema
|
||||
from grant.proposal.models import (
|
||||
Proposal,
|
||||
ProposalContribution,
|
||||
proposals_schema,
|
||||
proposal_schema,
|
||||
user_proposal_contributions_schema,
|
||||
PENDING
|
||||
)
|
||||
from grant.comment.models import Comment, comments_schema, user_comments_schema
|
||||
from grant.email.send import generate_email
|
||||
from .example_emails import example_email_args
|
||||
|
||||
|
@ -82,6 +89,9 @@ def stats():
|
|||
}
|
||||
|
||||
|
||||
# USERS
|
||||
|
||||
|
||||
@blueprint.route('/users/<id>', methods=['DELETE'])
|
||||
@endpoint.api()
|
||||
@auth_required
|
||||
|
@ -95,12 +105,28 @@ def delete_user(id):
|
|||
def get_users():
|
||||
users = User.query.all()
|
||||
result = users_schema.dump(users)
|
||||
for user in result:
|
||||
return result
|
||||
|
||||
|
||||
@blueprint.route('/users/<id>', methods=['GET'])
|
||||
@endpoint.api()
|
||||
@auth_required
|
||||
def get_user(id):
|
||||
user_db = User.query.filter(User.id == id).first()
|
||||
if user_db:
|
||||
user = user_schema.dump(user_db)
|
||||
user_proposals = Proposal.query.filter(Proposal.team.any(id=user['userid'])).all()
|
||||
user['proposals'] = proposals_schema.dump(user_proposals)
|
||||
user_comments = Comment.query.filter(Comment.user_id == user['userid']).all()
|
||||
user['comments'] = comments_schema.dump(user_comments)
|
||||
return result
|
||||
user_comments = Comment.get_by_user(user_db)
|
||||
user['comments'] = user_comments_schema.dump(user_comments)
|
||||
contributions = ProposalContribution.get_by_userid(user_db.id)
|
||||
contributions_dump = user_proposal_contributions_schema.dump(contributions)
|
||||
user["contributions"] = contributions_dump
|
||||
return user
|
||||
return {"message": f"Could not find user with id {id}"}, 404
|
||||
|
||||
|
||||
# PROPOSALS
|
||||
|
||||
|
||||
@blueprint.route("/proposals", methods=["GET"])
|
||||
|
@ -151,6 +177,9 @@ def approve_proposal(id, is_approve, reject_reason=None):
|
|||
return {"message": "Not implemented."}, 400
|
||||
|
||||
|
||||
# EMAIL
|
||||
|
||||
|
||||
@blueprint.route('/email/example/<type>', methods=['GET'])
|
||||
@cross_origin(supports_credentials=True)
|
||||
@endpoint.api()
|
||||
|
|
|
@ -74,7 +74,6 @@ class UserCommentSchema(ma.Schema):
|
|||
"ProposalSchema",
|
||||
exclude=[
|
||||
"comments",
|
||||
"contributions",
|
||||
"team",
|
||||
"milestones",
|
||||
"content",
|
||||
|
|
Loading…
Reference in New Issue