Add packages/client/src/team
This commit is contained in:
parent
9aff78b7a9
commit
a628082a73
197
packages/client/src/team/TeamActions.ts
Normal file
197
packages/client/src/team/TeamActions.ts
Normal file
@ -0,0 +1,197 @@
|
||||
import {IAPIDef} from '@rondo/common'
|
||||
import {IAction, IErrorAction, ActionTypes} from '../actions'
|
||||
import {IHTTPClient} from '../http/IHTTPClient'
|
||||
import {ITeam, IUser, IUserInTeam} from '@rondo/common'
|
||||
|
||||
export enum TeamActionKeys {
|
||||
TEAMS = 'TEAMS',
|
||||
TEAMS_PENDING = 'TEAMS_PENDING',
|
||||
TEAMS_REJECTED = 'TEAMS_REJECTED',
|
||||
|
||||
TEAM_CREATE = 'TEAM_CREATE',
|
||||
TEAM_CREATE_PENDING = 'TEAM_CREATE_PENDING',
|
||||
TEAM_CREATE_REJECTED = 'TEAM_CREATE_REJECTED',
|
||||
|
||||
TEAM_UPDATE = 'TEAM_UPDATE',
|
||||
TEAM_UPDATE_PENDING = 'TEAM_UPDATE_PENDING',
|
||||
TEAM_UPDATE_REJECTED = 'TEAM_UPDATE_REJECTED',
|
||||
|
||||
TEAM_REMOVE = 'TEAM_REMOVE',
|
||||
TEAM_REMOVE_PENDING = 'TEAM_REMOVE_PENDING',
|
||||
TEAM_REMOVE_REJECTED = 'TEAM_REMOVE_REJECTED',
|
||||
|
||||
TEAM_USER_ADD = 'TEAM_USER_ADD',
|
||||
TEAM_USER_ADD_PENDING = 'TEAM_USER_ADD_PENDING',
|
||||
TEAM_USER_ADD_REJECTED = 'TEAM_USER_ADD_REJECTED',
|
||||
|
||||
TEAM_USER_REMOVE = 'TEAM_USER_REMOVE',
|
||||
TEAM_USER_REMOVE_PENDING = 'TEAM_USER_REMOVE_PENDING',
|
||||
TEAM_USER_REMOVE_REJECTED = 'TEAM_USER_REMOVE_REJECTED',
|
||||
|
||||
TEAM_USERS = 'TEAM_USERS',
|
||||
TEAM_USERS_PENDING = 'TEAM_USERS_PENDING',
|
||||
TEAM_USERS_REJECTED = 'TEAM_USERS_REJECTED',
|
||||
|
||||
TEAM_USER_FIND = 'TEAM_USER_FIND',
|
||||
TEAM_USER_FIND_PENDING = 'TEAM_USER_FIND_PENDING',
|
||||
TEAM_USER_FIND_REJECTED = 'TEAM_USER_FIND_REJECTED',
|
||||
}
|
||||
|
||||
export class TeamActions {
|
||||
constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
|
||||
|
||||
fetchMyTeams = (): IAction<ITeam[], TeamActionKeys.TEAMS> => {
|
||||
return {
|
||||
payload: this.http.get('/my/teams'),
|
||||
type: TeamActionKeys.TEAMS,
|
||||
}
|
||||
}
|
||||
|
||||
fetchMyTeamsError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAMS_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAMS_REJECTED,
|
||||
}
|
||||
}
|
||||
|
||||
createTeam = (team: {name: string})
|
||||
: IAction<ITeam, TeamActionKeys.TEAM_CREATE> => {
|
||||
return {
|
||||
payload: this.http.post('/teams', team),
|
||||
type: TeamActionKeys.TEAM_CREATE,
|
||||
}
|
||||
}
|
||||
|
||||
createTeamError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_CREATE_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_CREATE_REJECTED,
|
||||
}
|
||||
}
|
||||
|
||||
updateTeam = ({id, name}: {id: number, name: string})
|
||||
: IAction<ITeam, TeamActionKeys.TEAM_UPDATE> => {
|
||||
return {
|
||||
payload: this.http.put('/teams/:id', {name}, {id}),
|
||||
type: TeamActionKeys.TEAM_UPDATE,
|
||||
}
|
||||
}
|
||||
|
||||
updateTeamError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_UPDATE_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_UPDATE_REJECTED,
|
||||
}
|
||||
}
|
||||
|
||||
removeTeam = ({id, name}: {id: number, name: string})
|
||||
: IAction<{}, TeamActionKeys.TEAM_REMOVE> => {
|
||||
return {
|
||||
payload: this.http.delete('/teams/:id', {}, {id}),
|
||||
type: TeamActionKeys.TEAM_REMOVE,
|
||||
}
|
||||
}
|
||||
|
||||
removeTeamError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_REMOVE_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_REMOVE_REJECTED,
|
||||
}
|
||||
}
|
||||
|
||||
addUser(
|
||||
{userId, teamId, roleId = 1}: {
|
||||
userId: number,
|
||||
teamId: number,
|
||||
roleId: number,
|
||||
})
|
||||
: IAction<IUserInTeam, TeamActionKeys.TEAM_USER_ADD> {
|
||||
return {
|
||||
payload: this.http.post('/teams/:teamId/users/:userId', {}, {
|
||||
userId,
|
||||
teamId,
|
||||
}),
|
||||
type: TeamActionKeys.TEAM_USER_ADD,
|
||||
}
|
||||
}
|
||||
|
||||
addUserError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_USER_ADD_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_USER_ADD_REJECTED,
|
||||
}
|
||||
}
|
||||
|
||||
removeUser = (
|
||||
{userId, teamId}: {
|
||||
userId: number,
|
||||
teamId: number,
|
||||
})
|
||||
: IAction<{
|
||||
userId: number,
|
||||
teamId: number,
|
||||
}, TeamActionKeys.TEAM_USER_REMOVE> => {
|
||||
return {
|
||||
payload: this.http.delete('/teams/:teamId/users/:userId', {}, {
|
||||
userId,
|
||||
teamId,
|
||||
}),
|
||||
type: TeamActionKeys.TEAM_USER_REMOVE,
|
||||
}
|
||||
}
|
||||
|
||||
removeUserError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_USER_REMOVE_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_USER_REMOVE_REJECTED,
|
||||
}
|
||||
}
|
||||
|
||||
fetchUsersInTeam = ({teamId}: {teamId: number})
|
||||
: IAction<{
|
||||
teamId: number,
|
||||
usersInTeam: IUserInTeam[]
|
||||
}, TeamActionKeys.TEAM_USERS> => {
|
||||
return {
|
||||
payload: this.http.get('/teams/:teamId/users', {
|
||||
teamId,
|
||||
})
|
||||
.then(usersInTeam => ({teamId, usersInTeam})),
|
||||
type: TeamActionKeys.TEAM_USERS,
|
||||
}
|
||||
}
|
||||
|
||||
fetchUsersInTeamError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_USERS_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_USERS_REJECTED,
|
||||
}
|
||||
}
|
||||
|
||||
findUserByEmail = (email: string)
|
||||
: IAction<IUser | undefined, TeamActionKeys.TEAM_USER_FIND> => {
|
||||
return {
|
||||
payload: this.http.get('/users/emails/:email', {
|
||||
email,
|
||||
}),
|
||||
type: TeamActionKeys.TEAM_USER_FIND,
|
||||
}
|
||||
}
|
||||
|
||||
findUserByEmailError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_USER_FIND_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_USER_FIND_REJECTED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type TeamActionType = ActionTypes<TeamActions>
|
||||
24
packages/client/src/team/TeamConnector.test.ts
Normal file
24
packages/client/src/team/TeamConnector.test.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import * as Feature from './'
|
||||
// export ReactDOM from 'react-dom'
|
||||
// import T from 'react-dom/test-utils'
|
||||
import {HTTPClientMock, TestUtils/*, getError*/} from '../test-utils'
|
||||
import {IAPIDef} from '@rondo/common'
|
||||
|
||||
const test = new TestUtils()
|
||||
|
||||
describe('TeamConnector', () => {
|
||||
|
||||
const http = new HTTPClientMock<IAPIDef>()
|
||||
const teamActions = new Feature.TeamActions(http)
|
||||
|
||||
const createTestProvider = () => test.withProvider({
|
||||
reducers: {Team: Feature.Team},
|
||||
connector: new Feature.TeamConnector(teamActions),
|
||||
select: state => state.Team,
|
||||
})
|
||||
|
||||
it('should render', () => {
|
||||
createTestProvider().render()
|
||||
})
|
||||
|
||||
})
|
||||
32
packages/client/src/team/TeamConnector.ts
Normal file
32
packages/client/src/team/TeamConnector.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import {Connector} from '../redux/Connector'
|
||||
import {ITeamState} from './TeamReducer'
|
||||
import {IStateSelector} from '../redux'
|
||||
import {TeamActions} from './TeamActions'
|
||||
import {TeamManager} from './TeamManager'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
export class TeamConnector extends Connector<ITeamState> {
|
||||
constructor(protected readonly teamActions: TeamActions) {
|
||||
super()
|
||||
}
|
||||
|
||||
connect<State>(getLocalState: IStateSelector<State, ITeamState>) {
|
||||
return this.wrap(
|
||||
getLocalState,
|
||||
state => ({
|
||||
...state,
|
||||
}),
|
||||
d => ({
|
||||
addUser: bindActionCreators(this.teamActions.addUser, d),
|
||||
removeUser: bindActionCreators(this.teamActions.removeUser, d),
|
||||
createTeam: bindActionCreators(this.teamActions.createTeam, d),
|
||||
updateTeam: bindActionCreators(this.teamActions.updateTeam, d),
|
||||
removeTeam: bindActionCreators(this.teamActions.removeTeam, d),
|
||||
fetchMyTeams: bindActionCreators(this.teamActions.fetchMyTeams, d),
|
||||
fetchUsersInTeam:
|
||||
bindActionCreators(this.teamActions.fetchUsersInTeam, d),
|
||||
}),
|
||||
TeamManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
117
packages/client/src/team/TeamList.tsx
Normal file
117
packages/client/src/team/TeamList.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import React from 'react'
|
||||
import {ITeam, ReadonlyRecord} from '@rondo/common'
|
||||
|
||||
export interface ITeamListProps {
|
||||
teamsById: ReadonlyRecord<number, ITeam>,
|
||||
teamIds: ReadonlyArray<number>,
|
||||
onAddTeam: (params: {name: string}) => Promise<void>
|
||||
onRemoveTeam: (params: {teamId: number}) => Promise<void>
|
||||
onUpdateTeam: (params: {id: number, name: string}) => Promise<void>
|
||||
editTeamId: number
|
||||
}
|
||||
|
||||
export interface ITeamProps {
|
||||
team: ITeam
|
||||
editTeamId: number // TODO handle edits via react-router params
|
||||
onRemoveTeam: (params: {teamId: number}) => Promise<void>
|
||||
onUpdateTeam: (params: {id: number, name: string}) => Promise<void>
|
||||
}
|
||||
|
||||
export interface IAddTeamProps {
|
||||
onAddTeam: (params: {name: string}) => Promise<void>
|
||||
onUpdateTeam: (params: {id: number, name: string}) => Promise<void>
|
||||
team?: ITeam
|
||||
}
|
||||
|
||||
export interface IAddTeamState {
|
||||
name: string
|
||||
}
|
||||
|
||||
export class TeamAdd extends React.PureComponent<IAddTeamProps, IAddTeamState> {
|
||||
constructor(props: IAddTeamProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
name: '',
|
||||
}
|
||||
}
|
||||
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const name = event.target.value
|
||||
this.setState({name})
|
||||
}
|
||||
handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
const {team, onAddTeam, onUpdateTeam} = this.props
|
||||
const {name} = this.state
|
||||
if (team) {
|
||||
await onUpdateTeam({id: team.id, name})
|
||||
} else {
|
||||
await onAddTeam({name})
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<form className='team-add' onSubmit={this.handleSubmit}>
|
||||
{this.props.team ? 'Edit team' : 'Add team'}
|
||||
<input
|
||||
type='text'
|
||||
value={this.state.name}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<input
|
||||
type='submit'
|
||||
value='Save'
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class TeamRow extends React.PureComponent<ITeamProps> {
|
||||
handleRemove = async () => {
|
||||
const {onRemoveTeam, team: {id: teamId}} = this.props
|
||||
await onRemoveTeam({teamId})
|
||||
}
|
||||
render() {
|
||||
const {team} = this.props
|
||||
return (
|
||||
<div className='team'>
|
||||
{team.id}
|
||||
{this.props.editTeamId !== team.id
|
||||
? team.name
|
||||
: <TeamAdd
|
||||
onAddTeam={undefined as any}
|
||||
onUpdateTeam={this.props.onUpdateTeam}
|
||||
team={team}
|
||||
/>
|
||||
}
|
||||
<button onClick={this.handleRemove}>Remove</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class TeamList extends React.PureComponent<ITeamListProps> {
|
||||
render() {
|
||||
const {editTeamId, teamIds, teamsById} = this.props
|
||||
|
||||
return (
|
||||
<div className='team-list'>
|
||||
{teamIds.map(teamId => {
|
||||
const team = teamsById[teamId]
|
||||
return (
|
||||
<TeamRow
|
||||
editTeamId={editTeamId}
|
||||
onRemoveTeam={this.props.onRemoveTeam}
|
||||
onUpdateTeam={this.props.onUpdateTeam}
|
||||
team={team}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<TeamAdd
|
||||
onAddTeam={this.props.onAddTeam}
|
||||
onUpdateTeam={undefined as any}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
62
packages/client/src/team/TeamManager.tsx
Normal file
62
packages/client/src/team/TeamManager.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React from 'react'
|
||||
import {ITeam, IUser, IUserInTeam, ReadonlyRecord} from '@rondo/common'
|
||||
import {TeamList} from './TeamList'
|
||||
import {TeamUserList} from './TeamUserList'
|
||||
// import {Route} from 'react-router-dom'
|
||||
|
||||
export interface ITeamManagerProps {
|
||||
createTeam: (params: {name: string}) => Promise<void>
|
||||
updateTeam: (params: {id: number, name: string}) => Promise<void>
|
||||
removeTeam: (params: {teamId: number}) => Promise<void>
|
||||
|
||||
addUser: (params: {userId: number, teamId: number}) => Promise<void>
|
||||
removeUser: (params: {userId: number, teamId: number}) => Promise<void>
|
||||
fetchMyTeams: () => void
|
||||
fetchUsersInTeam: () => void
|
||||
findUserByEmail: (email: string) => Promise<IUser>
|
||||
|
||||
teamsById: ReadonlyRecord<number, ITeam>
|
||||
teamIds: ReadonlyArray<number>
|
||||
|
||||
editTeamId: number
|
||||
|
||||
userKeysByTeamId: ReadonlyRecord<number, ReadonlyArray<string>>
|
||||
usersByKey: ReadonlyRecord<string, IUserInTeam>
|
||||
}
|
||||
|
||||
export class TeamManager extends React.PureComponent<ITeamManagerProps> {
|
||||
async componentDidMount() {
|
||||
await this.props.fetchMyTeams()
|
||||
}
|
||||
render() {
|
||||
// TODO load my teams on first launch
|
||||
// TODO use teamId from route url
|
||||
// TODO use editTeamId from route url
|
||||
const {editTeamId} = this.props
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TeamList
|
||||
editTeamId={this.props.editTeamId}
|
||||
teamsById={this.props.teamsById}
|
||||
teamIds={this.props.teamIds}
|
||||
onAddTeam={this.props.createTeam}
|
||||
onRemoveTeam={this.props.removeTeam}
|
||||
onUpdateTeam={this.props.updateTeam}
|
||||
/>
|
||||
<TeamUserList
|
||||
onAddUser={this.props.addUser}
|
||||
onRemoveUser={this.props.removeUser}
|
||||
findUserByEmail={this.props.findUserByEmail}
|
||||
fetchUsersInTeam={this.props.fetchUsersInTeam}
|
||||
|
||||
teamId={editTeamId}
|
||||
|
||||
userKeysByTeamId={this.props.userKeysByTeamId}
|
||||
usersByKey={this.props.usersByKey}
|
||||
/>
|
||||
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
125
packages/client/src/team/TeamReducer.ts
Normal file
125
packages/client/src/team/TeamReducer.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import {ITeam, IUserInTeam, ReadonlyRecord, indexBy} from '@rondo/common'
|
||||
import {TeamActionKeys, TeamActionType} from './TeamActions'
|
||||
|
||||
export interface ITeamState {
|
||||
readonly error: string
|
||||
|
||||
readonly teamIds: ReadonlyArray<number>
|
||||
readonly teamsById: ReadonlyRecord<number, ITeam>
|
||||
|
||||
readonly userKeysByTeamId: ReadonlyRecord<number, ReadonlyArray<string>>
|
||||
readonly usersByKey: ReadonlyRecord<string, IUserInTeam>
|
||||
}
|
||||
|
||||
const defaultState: ITeamState = {
|
||||
error: '',
|
||||
|
||||
teamIds: [],
|
||||
teamsById: {},
|
||||
|
||||
userKeysByTeamId: {},
|
||||
usersByKey: {},
|
||||
}
|
||||
|
||||
function removeUser(
|
||||
state: ITeamState,
|
||||
action: {
|
||||
payload: {userId: number, teamId: number},
|
||||
type: TeamActionKeys.TEAM_USER_REMOVE,
|
||||
},
|
||||
) {
|
||||
|
||||
const {payload} = action
|
||||
const {teamId} = payload
|
||||
const userKey = getUserKey(payload)
|
||||
|
||||
const userKeysByTeamId = {
|
||||
...state.userKeysByTeamId,
|
||||
[teamId]: state.userKeysByTeamId[teamId].filter(u => u !== userKey),
|
||||
}
|
||||
|
||||
const usersByKey = {...state.usersByKey}
|
||||
delete usersByKey[getUserKey(payload)]
|
||||
|
||||
return {
|
||||
...state,
|
||||
userKeysByTeamId,
|
||||
usersByKey,
|
||||
}
|
||||
}
|
||||
|
||||
function getUserKey(userInTeam: {userId: number, teamId: number}) {
|
||||
return `${userInTeam.teamId}_${userInTeam.userId}`
|
||||
}
|
||||
|
||||
export function Team(state = defaultState, action: TeamActionType): ITeamState {
|
||||
switch (action.type) {
|
||||
case TeamActionKeys.TEAMS:
|
||||
return {
|
||||
...state,
|
||||
teamIds: action.payload.map(team => team.id),
|
||||
teamsById: indexBy(action.payload, 'id'),
|
||||
}
|
||||
case TeamActionKeys.TEAM_CREATE:
|
||||
case TeamActionKeys.TEAM_UPDATE:
|
||||
return {
|
||||
...state,
|
||||
teamIds: state.teamIds.indexOf(action.payload.id) >= 0
|
||||
? state.teamIds
|
||||
: [...state.teamIds, action.payload.id],
|
||||
teamsById: {
|
||||
...state.teamsById,
|
||||
[action.payload.id]: action.payload,
|
||||
},
|
||||
}
|
||||
return state
|
||||
case TeamActionKeys.TEAM_USER_ADD:
|
||||
return {
|
||||
...state,
|
||||
userKeysByTeamId: {
|
||||
...state.userKeysByTeamId,
|
||||
[action.payload.teamId]: [
|
||||
...state.userKeysByTeamId[action.payload.teamId],
|
||||
getUserKey(action.payload),
|
||||
],
|
||||
},
|
||||
usersByKey: {
|
||||
...state.usersByKey,
|
||||
[getUserKey(action.payload)]: action.payload,
|
||||
},
|
||||
}
|
||||
case TeamActionKeys.TEAM_USER_REMOVE:
|
||||
return removeUser(state, action)
|
||||
case TeamActionKeys.TEAM_USERS:
|
||||
const usersByKey = action.payload.usersInTeam
|
||||
.reduce((obj, userInTeam) => {
|
||||
obj[getUserKey(userInTeam)] = userInTeam
|
||||
return obj
|
||||
}, {} as Record<string, IUserInTeam>)
|
||||
|
||||
return {
|
||||
...state,
|
||||
userKeysByTeamId: {
|
||||
...state.userKeysByTeamId,
|
||||
[action.payload.teamId]: action.payload.usersInTeam
|
||||
.map(ut => getUserKey(ut)),
|
||||
},
|
||||
usersByKey: {
|
||||
...state.usersByKey,
|
||||
...usersByKey,
|
||||
},
|
||||
}
|
||||
case TeamActionKeys.TEAM_CREATE_REJECTED:
|
||||
case TeamActionKeys.TEAM_UPDATE_REJECTED:
|
||||
case TeamActionKeys.TEAM_USER_ADD_REJECTED:
|
||||
case TeamActionKeys.TEAM_USER_REMOVE_REJECTED:
|
||||
case TeamActionKeys.TEAM_USERS_REJECTED:
|
||||
return {
|
||||
...state,
|
||||
error: action.error.message,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
return state
|
||||
}
|
||||
143
packages/client/src/team/TeamUserList.tsx
Normal file
143
packages/client/src/team/TeamUserList.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import React from 'react'
|
||||
import {IUser, IUserInTeam, ReadonlyRecord} from '@rondo/common'
|
||||
|
||||
const EMPTY_ARRAY: ReadonlyArray<string> = []
|
||||
|
||||
export interface ITeamUsersProps {
|
||||
// fetchMyTeams: () => void,
|
||||
fetchUsersInTeam: (teamId: number) => void
|
||||
findUserByEmail: (email: string) => Promise<IUser>
|
||||
|
||||
onAddUser: (params: {userId: number, teamId: number}) => Promise<void>
|
||||
onRemoveUser: (params: {userId: number, teamId: number}) => Promise<void>
|
||||
|
||||
teamId: number
|
||||
userKeysByTeamId: ReadonlyRecord<number, ReadonlyArray<string>>
|
||||
usersByKey: ReadonlyRecord<string, IUserInTeam>
|
||||
}
|
||||
|
||||
export interface ITeamUserProps {
|
||||
onRemoveUser: (params: {userId: number, teamId: number}) => void
|
||||
user: IUserInTeam
|
||||
}
|
||||
|
||||
export interface IAddUserProps {
|
||||
onAddUser: (params: {userId: number, teamId: number}) => Promise<void>
|
||||
onSearchUser: (email: string) => Promise<IUser>
|
||||
teamId: number
|
||||
}
|
||||
|
||||
export interface IAddUserState {
|
||||
email: string
|
||||
user?: IUser
|
||||
}
|
||||
|
||||
export class TeamUser extends React.PureComponent<ITeamUserProps> {
|
||||
handleRemoveUser = async () => {
|
||||
const {onRemoveUser, user} = this.props
|
||||
await onRemoveUser(user)
|
||||
}
|
||||
render() {
|
||||
const {user} = this.props
|
||||
// TODO style
|
||||
return (
|
||||
<div className='team-user'>
|
||||
{user.displayName}
|
||||
<button
|
||||
className='team-user-remove'
|
||||
onClick={this.handleRemoveUser}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class AddUser extends React.PureComponent<IAddUserProps, IAddUserState> {
|
||||
constructor(props: IAddUserProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
email: '',
|
||||
user: undefined,
|
||||
}
|
||||
}
|
||||
handleChangeEmail = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const email = event.target.value
|
||||
this.setState({email})
|
||||
}
|
||||
handleAddUser = async (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
const {teamId} = this.props
|
||||
const {email} = this.state
|
||||
const user = await this.props.onSearchUser(email)
|
||||
if (!user) {
|
||||
// TODO handle this better via 404 status code
|
||||
return
|
||||
}
|
||||
await this.props.onAddUser({
|
||||
teamId,
|
||||
userId: user.id,
|
||||
})
|
||||
|
||||
this.setState({email: '', user: undefined})
|
||||
|
||||
// TODO handle failures
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<form onSubmit={this.handleAddUser}>
|
||||
<input
|
||||
onChange={this.handleChangeEmail}
|
||||
placeholder='Email'
|
||||
type='email'
|
||||
value={this.state.email}
|
||||
/>
|
||||
<input type='submit' value='Add' />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class TeamUserList extends React.PureComponent<ITeamUsersProps> {
|
||||
async componentDidMount() {
|
||||
await this.fetchUsersInTeam(this.props.teamId)
|
||||
}
|
||||
async componentWillReceiveProps(nextProps: ITeamUsersProps) {
|
||||
const {teamId} = nextProps
|
||||
if (teamId !== this.props.teamId) {
|
||||
this.fetchUsersInTeam(teamId)
|
||||
}
|
||||
}
|
||||
async fetchUsersInTeam(teamId: number) {
|
||||
if (teamId) {
|
||||
await this.props.fetchUsersInTeam(teamId)
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const userKeysByTeamId = this.props.userKeysByTeamId[this.props.teamId]
|
||||
|| EMPTY_ARRAY
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className='team-user-list'>
|
||||
{userKeysByTeamId.map(key => {
|
||||
const user = this.props.usersByKey[key]
|
||||
return (
|
||||
<TeamUser
|
||||
user={user}
|
||||
onRemoveUser={this.props.onRemoveUser}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<AddUser
|
||||
onAddUser={this.props.onAddUser}
|
||||
onSearchUser={this.props.findUserByEmail}
|
||||
teamId={this.props.teamId}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
6
packages/client/src/team/index.ts
Normal file
6
packages/client/src/team/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './TeamActions'
|
||||
export * from './TeamConnector'
|
||||
export * from './TeamList'
|
||||
export * from './TeamManager'
|
||||
export * from './TeamReducer'
|
||||
export * from './TeamUserList'
|
||||
@ -35,6 +35,15 @@ export interface IAPIDef {
|
||||
}
|
||||
}
|
||||
}
|
||||
'/users/emails/:email': {
|
||||
// TODO exposing search by email might be a security concern
|
||||
'get': {
|
||||
params: {
|
||||
email: string
|
||||
}
|
||||
response: IUser | undefined
|
||||
}
|
||||
}
|
||||
|
||||
// TEAM
|
||||
|
||||
@ -67,6 +76,7 @@ export interface IAPIDef {
|
||||
params: {
|
||||
id: number
|
||||
}
|
||||
response: {id: number}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
packages/common/src/ReadonlyRecord.ts
Normal file
2
packages/common/src/ReadonlyRecord.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export type ReadonlyRecord<K extends string | number | symbol, V> =
|
||||
Readonly<Record<K, V>>
|
||||
@ -9,4 +9,6 @@ export * from './IUser'
|
||||
export * from './IUser'
|
||||
export * from './IUserInTeam'
|
||||
export * from './IUserTeam'
|
||||
export * from './ReadonlyRecord'
|
||||
export * from './URLFormatter'
|
||||
export * from './indexBy'
|
||||
|
||||
@ -36,6 +36,9 @@ describe('passport.promise', () => {
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
async findUserByEmail(email: string) {
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
const authenticator = new Authenticator(userService)
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ describe('UserService', () => {
|
||||
await createUser()
|
||||
const user = await userService.findUserByEmail(username)
|
||||
expect(user).toBeTruthy()
|
||||
expect(user!.password).toBe(undefined)
|
||||
expect(user).not.toHaveProperty('password')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import {IUserInTeam} from '@rondo/common'
|
||||
export interface ITeamService {
|
||||
create(params: {name: string, userId: number}): Promise<Team>
|
||||
|
||||
remove(params: {id: number, userId: number}): Promise<void>
|
||||
remove(params: {id: number, userId: number}): Promise<{id: number}>
|
||||
|
||||
update(params: {id: number, name: string, userId: number}): Promise<Team>
|
||||
|
||||
|
||||
@ -30,6 +30,8 @@ export class TeamService extends BaseService implements ITeamService {
|
||||
|
||||
await this.getRepository(Team)
|
||||
.delete({id})
|
||||
|
||||
return {id}
|
||||
}
|
||||
|
||||
async update({id, name, userId}: {id: number, name: string, userId: number}) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user