Profile edit (#160)
* cleanClone helper * redux/store support for user update * ProfileEdit and supporting UI * ts fix * fix spelling * backend update user basics * delete photo * remove comments * ignore leftover errors from previous update attempts * fix ts unused arg * make update user endpoint params optional * connect ProfileUser for authUser & updateUser * improve cleanClone function
This commit is contained in:
parent
357b4248c7
commit
2d75150dff
|
@ -2,7 +2,7 @@ from animal_case import animalify
|
|||
from flask import Blueprint, g, jsonify
|
||||
from flask_yoloapi import endpoint, parameter
|
||||
|
||||
from .models import User, users_schema, user_schema, db
|
||||
from .models import User, SocialMedia, Avatar, users_schema, user_schema, db
|
||||
from ..email.send import send_email
|
||||
from ..proposal.models import Proposal, proposal_team
|
||||
from ..utils.auth import requires_sm
|
||||
|
@ -74,3 +74,41 @@ def create_user(account_address, email_address, display_name, title):
|
|||
|
||||
result = user_schema.dump(user)
|
||||
return result
|
||||
|
||||
|
||||
@blueprint.route("/<user_identity>", methods=["PUT"])
|
||||
@endpoint.api(
|
||||
parameter('displayName', type=str, required=False),
|
||||
parameter('title', type=str, required=False),
|
||||
parameter('socialMedias', type=list, required=False),
|
||||
parameter('avatar', type=dict, required=False)
|
||||
)
|
||||
def update_user(user_identity, display_name, title, social_medias, avatar):
|
||||
user = User.get_by_email_or_account_address(email_address=user_identity, account_address=user_identity)
|
||||
if not user:
|
||||
return {"message": "User with that address or email not found"}, 404
|
||||
|
||||
if display_name is not None:
|
||||
user.display_name = display_name
|
||||
|
||||
if title is not None:
|
||||
user.title = title
|
||||
|
||||
if social_medias is not None:
|
||||
sm_query = SocialMedia.query.filter_by(user_id=user.id)
|
||||
sm_query.delete()
|
||||
for social_media in social_medias:
|
||||
sm = SocialMedia(social_media_link=social_media.get("link"), user_id=user.id)
|
||||
db.session.add(sm)
|
||||
|
||||
if avatar is not None:
|
||||
Avatar.query.filter_by(user_id=user.id).delete()
|
||||
avatar_link = avatar.get('link')
|
||||
if avatar_link:
|
||||
avatar_obj = Avatar(image_url=avatar_link, user_id=user.id)
|
||||
db.session.add(avatar_obj)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
result = user_schema.dump(user)
|
||||
return result
|
||||
|
|
|
@ -211,3 +211,59 @@ class TestAPI(BaseTestConfig):
|
|||
)
|
||||
|
||||
self.assertEqual(response.status_code, 409)
|
||||
|
||||
def test_update_user_remove_social_and_avatar(self):
|
||||
self.app.post(
|
||||
"/api/v1/proposals/",
|
||||
data=json.dumps(proposal),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
updated_user = copy.deepcopy(team[0])
|
||||
updated_user['displayName'] = 'Billy'
|
||||
updated_user['title'] = 'Commander'
|
||||
updated_user['socialMedias'] = []
|
||||
updated_user['avatar'] = {}
|
||||
|
||||
user_update_resp = self.app.put(
|
||||
"/api/v1/users/{}".format(proposal["team"][0]["accountAddress"]),
|
||||
data=json.dumps(updated_user),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
users_json = user_update_resp.json
|
||||
self.assertFalse(users_json["avatar"])
|
||||
self.assertFalse(len(users_json["socialMedias"]))
|
||||
self.assertEqual(users_json["displayName"], updated_user["displayName"])
|
||||
self.assertEqual(users_json["title"], updated_user["title"])
|
||||
|
||||
def test_update_user(self):
|
||||
self.app.post(
|
||||
"/api/v1/proposals/",
|
||||
data=json.dumps(proposal),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
updated_user = copy.deepcopy(team[0])
|
||||
updated_user['displayName'] = 'Billy'
|
||||
updated_user['title'] = 'Commander'
|
||||
updated_user['socialMedias'] = [
|
||||
{
|
||||
"link": "https://github.com/billyman"
|
||||
}
|
||||
]
|
||||
updated_user['avatar'] = {
|
||||
"link": "https://x.io/avatar.png"
|
||||
}
|
||||
|
||||
user_update_resp = self.app.put(
|
||||
"/api/v1/users/{}".format(proposal["team"][0]["accountAddress"]),
|
||||
data=json.dumps(updated_user),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
users_json = user_update_resp.json
|
||||
self.assertEqual(users_json["avatar"]["imageUrl"], updated_user["avatar"]["link"])
|
||||
self.assertEqual(users_json["socialMedias"][0]["socialMediaLink"], updated_user["socialMedias"][0]["link"])
|
||||
self.assertEqual(users_json["displayName"], updated_user["displayName"])
|
||||
self.assertEqual(users_json["title"], updated_user["title"])
|
||||
|
|
|
@ -64,3 +64,12 @@ export function createUser(payload: {
|
|||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
export function updateUser(user: TeamMember): Promise<{ data: TeamMember }> {
|
||||
return axios
|
||||
.put(`/api/v1/users/${user.ethAddress}`, formatTeamMemberForPost(user))
|
||||
.then(res => {
|
||||
res.data = formatTeamMemberFromGet(res.data);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
@small-query: ~'(max-width: 500px)';
|
||||
|
||||
.ProfileEditShade {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.ProfileEdit {
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
margin: -1rem;
|
||||
background: #fff;
|
||||
border-radius: 0.2rem;
|
||||
|
||||
@media @small-query {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.is-editing {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&-avatar {
|
||||
position: relative;
|
||||
height: 10.5rem;
|
||||
width: 10.5rem;
|
||||
margin-right: 1.25rem;
|
||||
align-self: start;
|
||||
|
||||
@media @small-query {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&-img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
&-change {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #ffffff;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 1rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:hover:focus {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-delete {
|
||||
position: absolute;
|
||||
top: 0.2rem;
|
||||
right: 0.2rem;
|
||||
cursor: pointer;
|
||||
color: #ffffff;
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
&:hover,
|
||||
&:hover:focus {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
flex: 1;
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
margin-right: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-alert {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
import React from 'react';
|
||||
import lodash from 'lodash';
|
||||
import { Input, Form, Col, Row, Button, Icon, Alert } from 'antd';
|
||||
import { SOCIAL_INFO } from 'utils/social';
|
||||
import { SOCIAL_TYPE, TeamMember } from 'types';
|
||||
import { UserState } from 'modules/users/reducers';
|
||||
import { getCreateTeamMemberError } from 'modules/create/utils';
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
import './ProfileEdit.less';
|
||||
|
||||
interface Props {
|
||||
user: UserState;
|
||||
onDone(): void;
|
||||
onEdit(user: TeamMember): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
fields: TeamMember;
|
||||
isChanged: boolean;
|
||||
showError: boolean;
|
||||
}
|
||||
|
||||
export default class ProfileEdit extends React.PureComponent<Props, State> {
|
||||
state: State = {
|
||||
fields: { ...this.props.user } as TeamMember,
|
||||
isChanged: false,
|
||||
showError: false,
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: Props, _: State) {
|
||||
if (
|
||||
prevProps.user.isUpdating &&
|
||||
!this.props.user.isUpdating &&
|
||||
!this.state.showError
|
||||
) {
|
||||
this.setState({ showError: true });
|
||||
}
|
||||
if (
|
||||
prevProps.user.isUpdating &&
|
||||
!this.props.user.isUpdating &&
|
||||
!this.props.user.updateError
|
||||
) {
|
||||
this.props.onDone();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fields } = this.state;
|
||||
const error = getCreateTeamMemberError(fields);
|
||||
const isMissingField =
|
||||
!fields.name || !fields.title || !fields.emailAddress || !fields.ethAddress;
|
||||
const isDisabled = !!error || isMissingField || !this.state.isChanged;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ProfileEdit">
|
||||
<div className="ProfileEdit-avatar">
|
||||
<UserAvatar className="ProfileEdit-avatar-img" user={fields} />
|
||||
<Button
|
||||
className="ProfileEdit-avatar-change"
|
||||
onClick={this.handleChangePhoto}
|
||||
>
|
||||
<Icon
|
||||
className="ProfileEdit-avatar-change-icon"
|
||||
type={fields.avatarUrl ? 'picture' : 'plus-circle'}
|
||||
/>
|
||||
<div>{fields.avatarUrl ? 'Change photo' : 'Add photo'}</div>
|
||||
</Button>
|
||||
{fields.avatarUrl && (
|
||||
<Button
|
||||
className="ProfileEdit-avatar-delete"
|
||||
icon="delete"
|
||||
shape="circle"
|
||||
onClick={this.handleDeletePhoto}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="ProfileEdit-info">
|
||||
<Form
|
||||
className="ProfileEdit-info-form"
|
||||
layout="vertical"
|
||||
onSubmit={this.handleSave}
|
||||
>
|
||||
<Form.Item>
|
||||
<Input
|
||||
name="name"
|
||||
autoComplete="off"
|
||||
placeholder="Display name (Required)"
|
||||
value={fields.name}
|
||||
onChange={this.handleChangeField}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Input
|
||||
name="title"
|
||||
autoComplete="off"
|
||||
placeholder="Title (Required)"
|
||||
value={fields.title}
|
||||
onChange={this.handleChangeField}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Input
|
||||
name="emailAddress"
|
||||
disabled={true}
|
||||
placeholder="Email address (Required)"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
value={fields.emailAddress}
|
||||
onChange={this.handleChangeField}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Input
|
||||
name="ethAddress"
|
||||
disabled={true}
|
||||
autoComplete="ethAddress"
|
||||
placeholder="Ethereum address (Required)"
|
||||
value={fields.ethAddress}
|
||||
onChange={this.handleChangeField}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={12}>
|
||||
{Object.values(SOCIAL_INFO).map(s => (
|
||||
<Col xs={24} sm={12} key={s.type}>
|
||||
<Form.Item>
|
||||
<Input
|
||||
placeholder={`${s.name} account`}
|
||||
autoComplete="off"
|
||||
value={fields.socialAccounts[s.type]}
|
||||
onChange={ev => this.handleSocialChange(ev, s.type)}
|
||||
addonBefore={s.icon}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{!isMissingField &&
|
||||
error && (
|
||||
<Alert
|
||||
type="error"
|
||||
message={error}
|
||||
showIcon
|
||||
style={{ marginBottom: '0.75rem' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Row>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={isDisabled}
|
||||
loading={this.props.user.isUpdating}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
<Button type="ghost" htmlType="button" onClick={this.handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Row>
|
||||
</Form>
|
||||
{this.state.showError &&
|
||||
this.props.user.updateError && (
|
||||
<Alert
|
||||
className="ProfileEdit-alert"
|
||||
message={`There was an error attempting to update your profile. (code ${
|
||||
this.props.user.updateError
|
||||
})`}
|
||||
type="error"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ProfileEditShade" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private handleSave = (evt: React.SyntheticEvent<any>) => {
|
||||
evt.preventDefault();
|
||||
this.props.onEdit(this.state.fields);
|
||||
};
|
||||
|
||||
private handleCancel = () => {
|
||||
this.props.onDone();
|
||||
};
|
||||
|
||||
private handleChangeField = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = ev.currentTarget;
|
||||
const fields = {
|
||||
...this.state.fields,
|
||||
[name as any]: value,
|
||||
};
|
||||
const isChanged = this.isChangedCheck(fields);
|
||||
this.setState({
|
||||
isChanged,
|
||||
fields,
|
||||
});
|
||||
};
|
||||
|
||||
private handleSocialChange = (
|
||||
ev: React.ChangeEvent<HTMLInputElement>,
|
||||
type: SOCIAL_TYPE,
|
||||
) => {
|
||||
const { value } = ev.currentTarget;
|
||||
const fields = {
|
||||
...this.state.fields,
|
||||
socialAccounts: {
|
||||
...this.state.fields.socialAccounts,
|
||||
[type]: value,
|
||||
},
|
||||
};
|
||||
// delete key for empty string
|
||||
if (!value) {
|
||||
delete fields.socialAccounts[type];
|
||||
}
|
||||
const isChanged = this.isChangedCheck(fields);
|
||||
this.setState({
|
||||
isChanged,
|
||||
fields,
|
||||
});
|
||||
};
|
||||
|
||||
private handleChangePhoto = () => {
|
||||
// TODO: Actual file uploading
|
||||
const gender = ['men', 'women'][Math.floor(Math.random() * 2)];
|
||||
const num = Math.floor(Math.random() * 80);
|
||||
const fields = {
|
||||
...this.state.fields,
|
||||
avatarUrl: `https://randomuser.me/api/portraits/${gender}/${num}.jpg`,
|
||||
};
|
||||
const isChanged = this.isChangedCheck(fields);
|
||||
this.setState({
|
||||
isChanged,
|
||||
fields,
|
||||
});
|
||||
};
|
||||
|
||||
private handleDeletePhoto = () => {
|
||||
const fields = lodash.clone(this.state.fields);
|
||||
delete fields.avatarUrl;
|
||||
const isChanged = this.isChangedCheck(fields);
|
||||
this.setState({ isChanged, fields });
|
||||
};
|
||||
|
||||
private isChangedCheck = (a: TeamMember) => {
|
||||
return !lodash.isEqual(a, this.props.user);
|
||||
};
|
||||
}
|
|
@ -51,6 +51,7 @@
|
|||
|
||||
&-social {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
& a {
|
||||
display: block;
|
||||
|
|
|
@ -1,21 +1,59 @@
|
|||
import React from 'react';
|
||||
import { SocialInfo, TeamMember } from 'types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button } from 'antd';
|
||||
import { SocialInfo } from 'types';
|
||||
import { usersActions } from 'modules/users';
|
||||
import { UserState } from 'modules/users/reducers';
|
||||
import { typedKeys } from 'utils/ts';
|
||||
import ProfileEdit from './ProfileEdit';
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
import './ProfileUser.less';
|
||||
import { SOCIAL_INFO, socialAccountToUrl } from 'utils/social';
|
||||
import ShortAddress from 'components/ShortAddress';
|
||||
import { typedKeys } from 'utils/ts';
|
||||
import './ProfileUser.less';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
interface OwnProps {
|
||||
user: TeamMember;
|
||||
user: UserState;
|
||||
}
|
||||
|
||||
export default class Profile extends React.Component<OwnProps> {
|
||||
interface StateProps {
|
||||
authUser: AppState['auth']['user'];
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
updateUser: typeof usersActions['updateUser'];
|
||||
}
|
||||
|
||||
interface State {
|
||||
isEditing: boolean;
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps & DispatchProps;
|
||||
|
||||
class ProfileUser extends React.Component<Props> {
|
||||
state: State = {
|
||||
isEditing: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
authUser,
|
||||
user,
|
||||
user: { socialAccounts },
|
||||
} = this.props;
|
||||
|
||||
const isSelf = !!authUser && authUser.ethAddress === user.ethAddress;
|
||||
|
||||
if (this.state.isEditing) {
|
||||
return (
|
||||
<ProfileEdit
|
||||
user={user}
|
||||
onDone={() => this.setState({ isEditing: false })}
|
||||
onEdit={this.props.updateUser}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ProfileUser">
|
||||
<div className="ProfileUser-avatar">
|
||||
|
@ -38,19 +76,28 @@ export default class Profile extends React.Component<OwnProps> {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ProfileUser-info-social">
|
||||
{typedKeys(SOCIAL_INFO).map(
|
||||
s =>
|
||||
(socialAccounts[s] && (
|
||||
<Social
|
||||
key={s}
|
||||
account={socialAccounts[s] as string}
|
||||
info={SOCIAL_INFO[s]}
|
||||
/>
|
||||
)) ||
|
||||
null,
|
||||
)}
|
||||
</div>
|
||||
{Object.keys(socialAccounts).length > 0 && (
|
||||
<div className="ProfileUser-info-social">
|
||||
{typedKeys(SOCIAL_INFO).map(
|
||||
s =>
|
||||
(socialAccounts[s] && (
|
||||
<Social
|
||||
key={s}
|
||||
account={socialAccounts[s] as string}
|
||||
info={SOCIAL_INFO[s]}
|
||||
/>
|
||||
)) ||
|
||||
null,
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isSelf && (
|
||||
<div>
|
||||
<Button onClick={() => this.setState({ isEditing: true })}>
|
||||
Edit profile
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -64,3 +111,14 @@ const Social = ({ account, info }: { account: string; info: SocialInfo }) => {
|
|||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const connectedProfileUser = connect<StateProps, DispatchProps, {}, AppState>(
|
||||
state => ({
|
||||
authUser: state.auth.user,
|
||||
}),
|
||||
{
|
||||
updateUser: usersActions.updateUser,
|
||||
},
|
||||
)(ProfileUser);
|
||||
|
||||
export default connectedProfileUser;
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { UserProposal, UserComment } from 'types';
|
||||
import { UserProposal, UserComment, TeamMember } from 'types';
|
||||
import types from './types';
|
||||
import { getUser, getProposals } from 'api/api';
|
||||
import { getUser, updateUser as apiUpdateUser, getProposals } from 'api/api';
|
||||
import { Dispatch } from 'redux';
|
||||
import { Proposal } from 'types';
|
||||
import BN from 'bn.js';
|
||||
import { cleanClone } from 'utils/helpers';
|
||||
import { INITIAL_TEAM_MEMBER_STATE } from 'modules/users/reducers';
|
||||
|
||||
export function fetchUser(userFetchId: string) {
|
||||
return async (dispatch: Dispatch<any>) => {
|
||||
|
@ -20,6 +22,22 @@ export function fetchUser(userFetchId: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export function updateUser(user: TeamMember) {
|
||||
const userClone = cleanClone(INITIAL_TEAM_MEMBER_STATE, user);
|
||||
return async (dispatch: Dispatch<any>) => {
|
||||
dispatch({ type: types.UPDATE_USER_PENDING, payload: { user } });
|
||||
try {
|
||||
const { data: updatedUser } = await apiUpdateUser(userClone);
|
||||
dispatch({
|
||||
type: types.UPDATE_USER_FULFILLED,
|
||||
payload: { user: updatedUser },
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch({ type: types.UPDATE_USER_REJECTED, payload: { user, error } });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchUserCreated(userFetchId: string) {
|
||||
return async (dispatch: Dispatch<any>) => {
|
||||
dispatch({ type: types.FETCH_USER_CREATED_PENDING, payload: { userFetchId } });
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import reducers, { UsersState, INITIAL_STATE } from './reducers';
|
||||
import reducers, { UserState, UsersState, INITIAL_STATE } from './reducers';
|
||||
import * as usersActions from './actions';
|
||||
import * as usersTypes from './types';
|
||||
|
||||
export { usersActions, usersTypes, UsersState, INITIAL_STATE };
|
||||
export { usersActions, usersTypes, UsersState, UserState, INITIAL_STATE };
|
||||
|
||||
export default reducers;
|
||||
|
|
|
@ -7,6 +7,8 @@ export interface UserState extends TeamMember {
|
|||
isFetching: boolean;
|
||||
hasFetched: boolean;
|
||||
fetchError: number | null;
|
||||
isUpdating: boolean;
|
||||
updateError: number | null;
|
||||
isFetchingCreated: boolean;
|
||||
hasFetchedCreated: boolean;
|
||||
fetchErrorCreated: number | null;
|
||||
|
@ -25,16 +27,22 @@ export interface UsersState {
|
|||
map: { [index: string]: UserState };
|
||||
}
|
||||
|
||||
export const INITIAL_USER_STATE: UserState = {
|
||||
export const INITIAL_TEAM_MEMBER_STATE: TeamMember = {
|
||||
ethAddress: '',
|
||||
avatarUrl: '',
|
||||
name: '',
|
||||
emailAddress: '',
|
||||
socialAccounts: {},
|
||||
title: '',
|
||||
};
|
||||
|
||||
export const INITIAL_USER_STATE: UserState = {
|
||||
...INITIAL_TEAM_MEMBER_STATE,
|
||||
isFetching: false,
|
||||
hasFetched: false,
|
||||
fetchError: null,
|
||||
isUpdating: false,
|
||||
updateError: null,
|
||||
isFetchingCreated: false,
|
||||
hasFetchedCreated: false,
|
||||
fetchErrorCreated: null,
|
||||
|
@ -58,73 +66,97 @@ export default (state = INITIAL_STATE, action: any) => {
|
|||
const userFetchId = payload && payload.userFetchId;
|
||||
const proposals = payload && payload.proposals;
|
||||
const comments = payload && payload.comments;
|
||||
const errorStatus = payload && payload.error && payload.error.response.status;
|
||||
const errorStatus =
|
||||
(payload &&
|
||||
payload.error &&
|
||||
payload.error.response &&
|
||||
payload.error.response.status) ||
|
||||
999;
|
||||
switch (action.type) {
|
||||
// fetch
|
||||
case types.FETCH_USER_PENDING:
|
||||
return updateState(state, userFetchId, { isFetching: true, fetchError: null });
|
||||
return updateStateFetch(state, userFetchId, { isFetching: true, fetchError: null });
|
||||
case types.FETCH_USER_FULFILLED:
|
||||
return updateState(
|
||||
return updateStateFetch(
|
||||
state,
|
||||
userFetchId,
|
||||
{ isFetching: false, hasFetched: true },
|
||||
payload.user,
|
||||
);
|
||||
case types.FETCH_USER_REJECTED:
|
||||
return updateState(state, userFetchId, {
|
||||
return updateStateFetch(state, userFetchId, {
|
||||
isFetching: false,
|
||||
hasFetched: true,
|
||||
fetchError: errorStatus,
|
||||
});
|
||||
// update
|
||||
case types.UPDATE_USER_PENDING:
|
||||
return updateStateFetch(state, payload.user.ethAddress, {
|
||||
isUpdating: true,
|
||||
updateError: null,
|
||||
});
|
||||
case types.UPDATE_USER_FULFILLED:
|
||||
return updateStateFetch(
|
||||
state,
|
||||
payload.user.ethAddress,
|
||||
{ isUpdating: false },
|
||||
payload.user,
|
||||
);
|
||||
case types.UPDATE_USER_REJECTED:
|
||||
return updateStateFetch(state, payload.user.ethAddress, {
|
||||
isUpdating: false,
|
||||
updateError: errorStatus,
|
||||
});
|
||||
// created proposals
|
||||
case types.FETCH_USER_CREATED_PENDING:
|
||||
return updateState(state, userFetchId, {
|
||||
return updateStateFetch(state, userFetchId, {
|
||||
isFetchingCreated: true,
|
||||
fetchErrorCreated: null,
|
||||
});
|
||||
case types.FETCH_USER_CREATED_FULFILLED:
|
||||
return updateState(state, userFetchId, {
|
||||
return updateStateFetch(state, userFetchId, {
|
||||
isFetchingCreated: false,
|
||||
hasFetchedCreated: true,
|
||||
createdProposals: proposals,
|
||||
});
|
||||
case types.FETCH_USER_CREATED_REJECTED:
|
||||
return updateState(state, userFetchId, {
|
||||
return updateStateFetch(state, userFetchId, {
|
||||
isFetchingCreated: false,
|
||||
hasFetchedCreated: true,
|
||||
fetchErrorCreated: errorStatus,
|
||||
});
|
||||
// funded proposals
|
||||
case types.FETCH_USER_FUNDED_PENDING:
|
||||
return updateState(state, userFetchId, {
|
||||
return updateStateFetch(state, userFetchId, {
|
||||
isFetchingFunded: true,
|
||||
fetchErrorFunded: null,
|
||||
});
|
||||
case types.FETCH_USER_FUNDED_FULFILLED:
|
||||
return updateState(state, userFetchId, {
|
||||
return updateStateFetch(state, userFetchId, {
|
||||
isFetchingFunded: false,
|
||||
hasFetchedFunded: true,
|
||||
fundedProposals: proposals,
|
||||
});
|
||||
case types.FETCH_USER_FUNDED_REJECTED:
|
||||
return updateState(state, userFetchId, {
|
||||
return updateStateFetch(state, userFetchId, {
|
||||
isFetchingFunded: false,
|
||||
hasFetchedFunded: true,
|
||||
fetchErrorFunded: errorStatus,
|
||||
});
|
||||
// comments
|
||||
case types.FETCH_USER_COMMENTS_PENDING:
|
||||
return updateState(state, userFetchId, {
|
||||
return updateStateFetch(state, userFetchId, {
|
||||
isFetchingComments: true,
|
||||
fetchErrorComments: null,
|
||||
});
|
||||
case types.FETCH_USER_COMMENTS_FULFILLED:
|
||||
return updateState(state, userFetchId, {
|
||||
return updateStateFetch(state, userFetchId, {
|
||||
isFetchingComments: false,
|
||||
hasFetchedComments: true,
|
||||
comments,
|
||||
});
|
||||
case types.FETCH_USER_COMMENTS_REJECTED:
|
||||
return updateState(state, userFetchId, {
|
||||
return updateStateFetch(state, userFetchId, {
|
||||
isFetchingComments: false,
|
||||
hasFetchedComments: true,
|
||||
fetchErrorComments: errorStatus,
|
||||
|
@ -134,12 +166,17 @@ export default (state = INITIAL_STATE, action: any) => {
|
|||
}
|
||||
};
|
||||
|
||||
function updateState(state: UsersState, id: string, updates: object, loaded?: UserState) {
|
||||
function updateStateFetch(
|
||||
state: UsersState,
|
||||
id: string,
|
||||
updates: object,
|
||||
loaded?: UserState,
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[id]: lodash.defaultsDeep(updates, loaded, state.map[id] || INITIAL_USER_STATE),
|
||||
[id]: lodash.defaults(updates, loaded, state.map[id] || INITIAL_USER_STATE),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,6 +4,11 @@ enum UsersActions {
|
|||
FETCH_USER_FULFILLED = 'FETCH_USER_FULFILLED',
|
||||
FETCH_USER_REJECTED = 'FETCH_USER_REJECTED',
|
||||
|
||||
UPDATE_USER = 'UPDATE_USER',
|
||||
UPDATE_USER_PENDING = 'UPDATE_USER_PENDING',
|
||||
UPDATE_USER_FULFILLED = 'UPDATE_USER_FULFILLED',
|
||||
UPDATE_USER_REJECTED = 'UPDATE_USER_REJECTED',
|
||||
|
||||
FETCH_USER_CREATED = 'FETCH_USER_CREATED',
|
||||
FETCH_USER_CREATED_PENDING = 'FETCH_USER_CREATED_PENDING',
|
||||
FETCH_USER_CREATED_FULFILLED = 'FETCH_USER_CREATED_FULFILLED',
|
||||
|
|
|
@ -7,7 +7,7 @@ export function formatTeamMemberForPost(user: TeamMember) {
|
|||
title: user.title,
|
||||
accountAddress: user.ethAddress,
|
||||
emailAddress: user.emailAddress,
|
||||
avatar: user.avatarUrl ? { link: user.avatarUrl } : undefined,
|
||||
avatar: user.avatarUrl ? { link: user.avatarUrl } : {},
|
||||
socialMedias: socialAccountsToUrls(user.socialAccounts).map(url => ({
|
||||
link: url,
|
||||
})),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { pick } from 'lodash';
|
||||
import { Comment } from 'types';
|
||||
|
||||
export function isNumeric(n: any) {
|
||||
|
@ -26,6 +27,14 @@ export function findComment(
|
|||
return null;
|
||||
}
|
||||
|
||||
// clone and filter keys by keySource object's keys
|
||||
export function cleanClone<T extends object>(keySource: T, target: Partial<T>) {
|
||||
const sourceKeys = Object.keys(keySource);
|
||||
const fullClone = { ...(target as object) };
|
||||
const clone = pick(fullClone, sourceKeys);
|
||||
return clone as T;
|
||||
}
|
||||
|
||||
export function urlToPublic(url: string) {
|
||||
let withPublicHost = url.match(/^https?:/) ? url : process.env.PUBLIC_HOST_URL + url;
|
||||
if (process.env.NODE_ENV === 'development' && process.env.PUBLIC_HOST_URL) {
|
||||
|
|
Loading…
Reference in New Issue