Use JSONRPC in TeamService (needs more testing)
This commit is contained in:
parent
9199961fdb
commit
055d9588bf
@ -1,92 +0,0 @@
|
||||
import {IAPIDef} from '@rondo.dev/common'
|
||||
import {TGetPendingAction, TAsyncAction, PendingAction} from '@rondo.dev/redux'
|
||||
import {IHTTPClient} from '@rondo.dev/http-client'
|
||||
import {ITeam, IUser, IUserInTeam} from '@rondo.dev/common'
|
||||
|
||||
export type TTeamAction =
|
||||
TAsyncAction<ITeam[], 'TEAMS'>
|
||||
| TAsyncAction<ITeam, 'TEAM_CREATE'>
|
||||
| TAsyncAction<ITeam, 'TEAM_UPDATE'>
|
||||
| TAsyncAction<{id: number}, 'TEAM_REMOVE'>
|
||||
| TAsyncAction<IUserInTeam, 'TEAM_USER_ADD'>
|
||||
| TAsyncAction<{userId: number, teamId: number}, 'TEAM_USER_REMOVE'>
|
||||
| TAsyncAction<{teamId: number, usersInTeam: IUserInTeam[]}, 'TEAM_USERS'>
|
||||
| TAsyncAction<IUser | undefined, 'TEAM_USER_FIND'>
|
||||
|
||||
type Action<T extends string> = TGetPendingAction<TTeamAction, T>
|
||||
|
||||
export class TeamActions {
|
||||
constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
|
||||
|
||||
fetchMyTeams = (): Action<'TEAMS'> => {
|
||||
return new PendingAction(this.http.get('/my/teams'), 'TEAMS')
|
||||
}
|
||||
|
||||
createTeam = (team: {name: string}): Action<'TEAM_CREATE'> => {
|
||||
return new PendingAction(this.http.post('/teams', team), 'TEAM_CREATE')
|
||||
}
|
||||
|
||||
updateTeam = ({id, name}: {id: number, name: string})
|
||||
: Action<'TEAM_UPDATE'> => {
|
||||
return new PendingAction(
|
||||
this.http.put('/teams/:id', {name}, {id}),
|
||||
'TEAM_UPDATE',
|
||||
)
|
||||
}
|
||||
|
||||
removeTeam = ({id}: {id: number}): Action<'TEAM_REMOVE'> => {
|
||||
return new PendingAction(
|
||||
this.http.delete('/teams/:id', {}, {id}),
|
||||
'TEAM_REMOVE',
|
||||
)
|
||||
}
|
||||
|
||||
addUser(
|
||||
{userId, teamId, roleId = 1}: {
|
||||
userId: number,
|
||||
teamId: number,
|
||||
roleId: number,
|
||||
})
|
||||
: Action<'TEAM_USER_ADD'> {
|
||||
return new PendingAction(
|
||||
this.http.post('/teams/:teamId/users/:userId', {}, {
|
||||
userId,
|
||||
teamId,
|
||||
}),
|
||||
'TEAM_USER_ADD',
|
||||
)
|
||||
}
|
||||
|
||||
removeUser = (
|
||||
{userId, teamId}: {
|
||||
userId: number,
|
||||
teamId: number,
|
||||
})
|
||||
: Action<'TEAM_USER_REMOVE'> => {
|
||||
return new PendingAction(
|
||||
this.http.delete('/teams/:teamId/users/:userId', {}, {
|
||||
userId,
|
||||
teamId,
|
||||
}),
|
||||
'TEAM_USER_REMOVE',
|
||||
)
|
||||
}
|
||||
|
||||
fetchUsersInTeam = ({teamId}: {teamId: number})
|
||||
: Action<'TEAM_USERS'> => {
|
||||
return new PendingAction(
|
||||
this.http.get('/teams/:teamId/users', {}, {
|
||||
teamId,
|
||||
})
|
||||
.then(usersInTeam => ({teamId, usersInTeam})),
|
||||
'TEAM_USERS',
|
||||
)
|
||||
}
|
||||
|
||||
findUserByEmail = (email: string): Action<'TEAM_USER_FIND'> => {
|
||||
return new PendingAction(
|
||||
this.http.get('/users/emails/:email', {}, {email}),
|
||||
'TEAM_USER_FIND',
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,13 @@
|
||||
import * as Feature from './'
|
||||
// export ReactDOM from 'react-dom'
|
||||
import T from 'react-dom/test-utils'
|
||||
import {TestUtils} from '../test-utils'
|
||||
import { IAPIDef, IUserInTeam, team as Team, user as User } from '@rondo.dev/common'
|
||||
import { HTTPClientMock } from '@rondo.dev/http-client'
|
||||
import { getError } from '@rondo.dev/test-utils'
|
||||
import {IAPIDef, ITeam, IUserInTeam} from '@rondo.dev/common'
|
||||
import React from 'react'
|
||||
import T from 'react-dom/test-utils'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { TestUtils } from '../test-utils'
|
||||
import * as Feature from './'
|
||||
import { createActions, createRemoteClient } from '@rondo.dev/jsonrpc'
|
||||
import createClientMock from '@rondo.dev/jsonrpc/lib/createClientMock'
|
||||
|
||||
const test = new TestUtils()
|
||||
|
||||
@ -16,24 +17,22 @@ describe('TeamConnector', () => {
|
||||
push: jest.fn(),
|
||||
}
|
||||
|
||||
let teamActions!: Feature.TeamActions
|
||||
let http: HTTPClientMock<IAPIDef>
|
||||
const [teamClient, teamClientMock] =
|
||||
createClientMock<Team.ITeamService>(Team.TeamServiceMethods)
|
||||
const [userClient, userClientMock] =
|
||||
createClientMock<User.IUserService>(User.UserServiceMethods)
|
||||
let teamActions!: Team.TeamActions
|
||||
let userActions!: User.UserActions
|
||||
beforeEach(() => {
|
||||
http = new HTTPClientMock<IAPIDef>()
|
||||
teamClientMock.find.mockResolvedValue(teams)
|
||||
teamClientMock.findUsers.mockResolvedValue(users)
|
||||
|
||||
http.mockAdd({
|
||||
method: 'get',
|
||||
url: '/my/teams',
|
||||
}, teams)
|
||||
http.mockAdd({
|
||||
method: 'get',
|
||||
url: '/teams/:teamId/users',
|
||||
params: {
|
||||
teamId: 123,
|
||||
},
|
||||
}, users)
|
||||
teamActions = createActions(teamClient, 'teamService')
|
||||
userActions = createActions(userClient, 'userService')
|
||||
})
|
||||
|
||||
teamActions = new Feature.TeamActions(http)
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
let historyEntries = ['/teams']
|
||||
@ -42,30 +41,41 @@ describe('TeamConnector', () => {
|
||||
reducers: {Team: Feature.Team},
|
||||
select: state => state.Team,
|
||||
})
|
||||
.withComponent(select => Feature.configure(teamActions, select))
|
||||
.withComponent(select => Feature.configure(teamActions, userActions, select))
|
||||
.withJSX((Component, props) =>
|
||||
<MemoryRouter initialEntries={historyEntries}>
|
||||
<Component {...props} />
|
||||
</MemoryRouter>,
|
||||
)
|
||||
|
||||
const teams: ITeam[] = [{id: 100, name: 'my-team', userId: 1}]
|
||||
const teams: Team.Team[] = [{
|
||||
id: 100,
|
||||
name: 'my-team',
|
||||
userId: 1,
|
||||
createDate: '',
|
||||
updateDate: '',
|
||||
userTeams: [],
|
||||
}]
|
||||
|
||||
const users: IUserInTeam[] = [{
|
||||
const users: Team.ITeamUsers = {
|
||||
teamId: 123,
|
||||
usersInTeam: [{
|
||||
teamId: 123,
|
||||
userId: 1,
|
||||
displayName: 'test test',
|
||||
roleId: 1,
|
||||
roleName: 'ADMIN',
|
||||
}]
|
||||
}],
|
||||
}
|
||||
|
||||
it('it fetches user teams on render', async () => {
|
||||
const {node} = createTestProvider().render({
|
||||
const {waitForActions, render} = createTestProvider()
|
||||
const {node} = render({
|
||||
history,
|
||||
location: {} as any,
|
||||
match: {} as any,
|
||||
})
|
||||
await http.wait()
|
||||
await waitForActions()
|
||||
expect(node.innerHTML).toContain('my-team')
|
||||
})
|
||||
|
||||
@ -74,14 +84,17 @@ describe('TeamConnector', () => {
|
||||
historyEntries = ['/teams/new']
|
||||
})
|
||||
|
||||
it('sends a POST request to POST /teams', async () => {
|
||||
const newTeam: Partial<ITeam> = {id: 101, name: 'new-team'}
|
||||
http.mockAdd({
|
||||
method: 'post',
|
||||
url: '/teams',
|
||||
data: {name: 'new-team'},
|
||||
}, newTeam)
|
||||
const {render, store} = createTestProvider()
|
||||
it('creates a new team', async () => {
|
||||
const newTeam: Team.Team = {
|
||||
id: 101,
|
||||
name: 'new-team',
|
||||
createDate: '',
|
||||
updateDate: '',
|
||||
userId: 9,
|
||||
userTeams: [],
|
||||
}
|
||||
teamClientMock.create.mockResolvedValue(newTeam)
|
||||
const {render, store, waitForActions} = createTestProvider()
|
||||
const {node} = render({
|
||||
history,
|
||||
location: {} as any,
|
||||
@ -92,20 +105,16 @@ describe('TeamConnector', () => {
|
||||
.querySelector('input') as HTMLInputElement
|
||||
T.Simulate.change(nameInput, {target: {value: newTeam.name}} as any)
|
||||
T.Simulate.submit(addTeamForm)
|
||||
await http.wait()
|
||||
const {Team} = store.getState()
|
||||
expect(Team.teamIds).toEqual([100, 101])
|
||||
expect(Team.teamsById[101]).toEqual(newTeam)
|
||||
await waitForActions()
|
||||
const state = store.getState()
|
||||
expect(state.Team.teamIds).toEqual([100, 101])
|
||||
expect(state.Team.teamsById[101]).toEqual(newTeam)
|
||||
})
|
||||
|
||||
it('displays an error', async () => {
|
||||
const error = {error: 'An error'}
|
||||
http.mockAdd({
|
||||
method: 'post',
|
||||
url: '/teams',
|
||||
data: {name: 'test'},
|
||||
}, error, 400)
|
||||
const {render} = createTestProvider()
|
||||
teamClientMock.create.mockRejectedValue(new Error('Test Error'))
|
||||
const {render, waitForActions} = createTestProvider()
|
||||
const {node} = render({
|
||||
history,
|
||||
location: {} as any,
|
||||
@ -116,10 +125,10 @@ describe('TeamConnector', () => {
|
||||
.querySelector('input') as HTMLInputElement
|
||||
T.Simulate.change(nameInput, {target: {value: 'test'}} as any)
|
||||
T.Simulate.submit(addTeamForm)
|
||||
const error2 = await getError(http.wait())
|
||||
expect(error2.message).toMatch(/HTTP Status: 400/)
|
||||
const error2 = await getError(waitForActions())
|
||||
expect(error2.message).toMatch(/Test Error/)
|
||||
expect(nameInput.value).toEqual('test')
|
||||
expect(addTeamForm.innerHTML).toMatch(/HTTP Status: 400/)
|
||||
expect(addTeamForm.innerHTML).toMatch(/Test Error/)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,29 +1,21 @@
|
||||
import {Connector} from '../redux/Connector'
|
||||
import {pack, TStateSelector} from '@rondo.dev/redux'
|
||||
import {ITeamState} from './TeamReducer'
|
||||
import {TeamActions} from './TeamActions'
|
||||
import { team, user } from '@rondo.dev/common'
|
||||
import { TReduxed } from '@rondo.dev/jsonrpc'
|
||||
import { bindActionCreators, pack, TStateSelector } from '@rondo.dev/redux'
|
||||
import { TeamManager } from './TeamManager'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import {withRouter} from 'react-router-dom'
|
||||
import { ITeamState } from './TeamReducer'
|
||||
|
||||
export function configure<State>(
|
||||
teamActions: TeamActions,
|
||||
teamActions: team.TeamActions,
|
||||
userActions: user.UserActions,
|
||||
getLocalState: TStateSelector<State, ITeamState>,
|
||||
) {
|
||||
const Component = pack(
|
||||
getLocalState,
|
||||
state => ({...state}),
|
||||
d => ({
|
||||
addUser: bindActionCreators(teamActions.addUser, d),
|
||||
removeUser: bindActionCreators(teamActions.removeUser, d),
|
||||
createTeam: bindActionCreators(teamActions.createTeam, d),
|
||||
updateTeam: bindActionCreators(teamActions.updateTeam, d),
|
||||
removeTeam: bindActionCreators(teamActions.removeTeam, d),
|
||||
fetchMyTeams: bindActionCreators(teamActions.fetchMyTeams, d),
|
||||
fetchUsersInTeam:
|
||||
bindActionCreators(teamActions.fetchUsersInTeam, d),
|
||||
findUserByEmail:
|
||||
bindActionCreators(teamActions.findUserByEmail, d),
|
||||
dispatch => ({
|
||||
teamActions: bindActionCreators(teamActions, dispatch),
|
||||
findUserByEmail: bindActionCreators(
|
||||
userActions.findUserByEmail, dispatch),
|
||||
}),
|
||||
TeamManager,
|
||||
)
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
import React from 'react'
|
||||
import { team as Team } from '@rondo.dev/common'
|
||||
import { Button, Control, Heading, Help, Input } from 'bloomer'
|
||||
import {ITeam} from '@rondo.dev/common'
|
||||
import {TeamActions} from './TeamActions'
|
||||
import {FaPlusSquare, FaCheck, FaEdit} from 'react-icons/fa'
|
||||
import React from 'react'
|
||||
import { FaCheck, FaEdit, FaPlusSquare } from 'react-icons/fa'
|
||||
|
||||
export type TTeamEditorProps = {
|
||||
type: 'add'
|
||||
onAddTeam: TeamActions['createTeam']
|
||||
onAddTeam: Team.TeamActions['create']
|
||||
} | {
|
||||
type: 'update'
|
||||
onUpdateTeam: TeamActions['updateTeam']
|
||||
team: ITeam
|
||||
onUpdateTeam: Team.TeamActions['update']
|
||||
team: Team.Team
|
||||
}
|
||||
|
||||
export interface ITeamEditorState {
|
||||
@ -28,7 +27,7 @@ extends React.PureComponent<TTeamEditorProps, ITeamEditorState> {
|
||||
name: props.type === 'update' ? this.getName(props.team) : '',
|
||||
}
|
||||
}
|
||||
getName(team?: ITeam) {
|
||||
getName(team?: Team.Team) {
|
||||
return team ? team.name : ''
|
||||
}
|
||||
componentWillReceiveProps(nextProps: TTeamEditorProps) {
|
||||
|
||||
@ -1,23 +1,21 @@
|
||||
import React from 'react'
|
||||
import {Button, Panel, PanelHeading, PanelBlock} from 'bloomer'
|
||||
import {FaPlus, FaEdit, FaTimes} from 'react-icons/fa'
|
||||
import {ITeam, TReadonlyRecord} from '@rondo.dev/common'
|
||||
import {Link} from 'react-router-dom'
|
||||
import {TeamActions} from './TeamActions'
|
||||
import {TeamEditor} from './TeamEditor'
|
||||
import { team as Team, TReadonlyRecord } from '@rondo.dev/common';
|
||||
import { Button, Panel, PanelBlock, PanelHeading } from 'bloomer';
|
||||
import React from 'react';
|
||||
import { FaEdit, FaPlus, FaTimes } from 'react-icons/fa';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export interface ITeamListProps {
|
||||
ListButtons?: React.ComponentType<{team: ITeam}>
|
||||
teamsById: TReadonlyRecord<number, ITeam>
|
||||
ListButtons?: React.ComponentType<{team: Team.Team}>
|
||||
teamsById: TReadonlyRecord<number, Team.Team>
|
||||
teamIds: ReadonlyArray<number>
|
||||
onAddTeam: TeamActions['createTeam']
|
||||
onRemoveTeam: TeamActions['removeTeam']
|
||||
onAddTeam: Team.TeamActions['create']
|
||||
onRemoveTeam: Team.TeamActions['remove']
|
||||
}
|
||||
|
||||
export interface ITeamProps {
|
||||
ListButtons?: React.ComponentType<{team: ITeam}>
|
||||
team: ITeam
|
||||
onRemoveTeam: TeamActions['removeTeam']
|
||||
ListButtons?: React.ComponentType<{team: Team.Team}>
|
||||
team: Team.Team
|
||||
onRemoveTeam: Team.TeamActions['remove']
|
||||
}
|
||||
|
||||
export class TeamRow extends React.PureComponent<ITeamProps> {
|
||||
|
||||
@ -1,32 +1,24 @@
|
||||
import React from 'react'
|
||||
import {History, Location} from 'history'
|
||||
import {ITeam, IUserInTeam, TReadonlyRecord} from '@rondo.dev/common'
|
||||
import { IUserInTeam, team as Team, TReadonlyRecord, user as User } from '@rondo.dev/common'
|
||||
import { Panel, PanelBlock, PanelHeading } from 'bloomer'
|
||||
import { History, Location } from 'history'
|
||||
import React from 'react'
|
||||
import { match as Match } from 'react-router'
|
||||
import { Route, Switch } from 'react-router-dom'
|
||||
import {TeamActions} from './TeamActions'
|
||||
import { TeamEditor } from './TeamEditor'
|
||||
import { TeamList } from './TeamList'
|
||||
import { TeamUserList } from './TeamUserList'
|
||||
import {match as Match} from 'react-router'
|
||||
|
||||
export interface ITeamManagerProps {
|
||||
history: History
|
||||
location: Location
|
||||
match: Match<any>
|
||||
|
||||
ListButtons?: React.ComponentType<{team: ITeam}>
|
||||
ListButtons?: React.ComponentType<{team: Team.Team}>
|
||||
|
||||
createTeam: TeamActions['createTeam']
|
||||
updateTeam: TeamActions['updateTeam']
|
||||
removeTeam: TeamActions['removeTeam']
|
||||
teamActions: Team.TeamActions
|
||||
findUserByEmail: User.UserActions['findUserByEmail']
|
||||
|
||||
addUser: TeamActions['addUser']
|
||||
removeUser: TeamActions['removeUser']
|
||||
fetchMyTeams: TeamActions['fetchMyTeams']
|
||||
fetchUsersInTeam: TeamActions['fetchUsersInTeam']
|
||||
findUserByEmail: TeamActions['findUserByEmail']
|
||||
|
||||
teamsById: TReadonlyRecord<number, ITeam>
|
||||
teamsById: TReadonlyRecord<number, Team.Team>
|
||||
teamIds: ReadonlyArray<number>
|
||||
|
||||
userKeysByTeamId: TReadonlyRecord<number, ReadonlyArray<string>>
|
||||
@ -35,10 +27,10 @@ export interface ITeamManagerProps {
|
||||
|
||||
export class TeamManager extends React.PureComponent<ITeamManagerProps> {
|
||||
async componentDidMount() {
|
||||
await this.props.fetchMyTeams()
|
||||
await this.props.teamActions.find()
|
||||
}
|
||||
handleCreateNewTeam = (team: {name: string}) => {
|
||||
const action = this.props.createTeam(team)
|
||||
const action = this.props.teamActions.create(team)
|
||||
action.payload
|
||||
.then(() => this.props.history.push('/teams'))
|
||||
.catch(() => {/* do nothing */})
|
||||
@ -62,8 +54,8 @@ export class TeamManager extends React.PureComponent<ITeamManagerProps> {
|
||||
ListButtons={this.props.ListButtons}
|
||||
teamsById={teamsById}
|
||||
teamIds={this.props.teamIds}
|
||||
onAddTeam={this.props.createTeam}
|
||||
onRemoveTeam={this.props.removeTeam}
|
||||
onAddTeam={this.props.teamActions.create}
|
||||
onRemoveTeam={this.props.teamActions.remove}
|
||||
/>
|
||||
</>
|
||||
}/>
|
||||
@ -79,16 +71,16 @@ export class TeamManager extends React.PureComponent<ITeamManagerProps> {
|
||||
{team && <TeamEditor
|
||||
type='update'
|
||||
team={team}
|
||||
onUpdateTeam={this.props.updateTeam}
|
||||
onUpdateTeam={this.props.teamActions.update}
|
||||
/>}
|
||||
{!team && 'No team loaded'}
|
||||
</PanelBlock>
|
||||
</Panel>
|
||||
{team && <TeamUserList
|
||||
onAddUser={this.props.addUser}
|
||||
onRemoveUser={this.props.removeUser}
|
||||
onAddUser={this.props.teamActions.addUser}
|
||||
onRemoveUser={this.props.teamActions.removeUser}
|
||||
findUserByEmail={this.props.findUserByEmail}
|
||||
fetchUsersInTeam={this.props.fetchUsersInTeam}
|
||||
fetchUsersInTeam={this.props.teamActions.findUsers}
|
||||
team={team}
|
||||
userKeysByTeamId={this.props.userKeysByTeamId}
|
||||
usersByKey={this.props.usersByKey}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import {
|
||||
ITeam, IUserInTeam, TReadonlyRecord, indexBy, without,
|
||||
} from '@rondo.dev/common'
|
||||
import {TTeamAction} from './TeamActions'
|
||||
import {TGetResolvedAction} from '@rondo.dev/redux'
|
||||
import { indexBy, ITeam, IUserInTeam, team as T, TReadonlyRecord, without } from '@rondo.dev/common';
|
||||
import { createReducer } from '@rondo.dev/jsonrpc';
|
||||
|
||||
export interface ITeamState {
|
||||
readonly loading: number
|
||||
readonly error: string
|
||||
|
||||
readonly teamIds: ReadonlyArray<number>
|
||||
@ -15,6 +13,7 @@ export interface ITeamState {
|
||||
}
|
||||
|
||||
const defaultState: ITeamState = {
|
||||
loading: 0,
|
||||
error: '',
|
||||
|
||||
teamIds: [],
|
||||
@ -24,11 +23,56 @@ const defaultState: ITeamState = {
|
||||
usersByKey: {},
|
||||
}
|
||||
|
||||
function removeUser(
|
||||
state: ITeamState,
|
||||
action: TGetResolvedAction<TTeamAction, 'TEAM_USER_REMOVE'>,
|
||||
) {
|
||||
function getUserKey(userInTeam: {userId: number, teamId: number}) {
|
||||
return `${userInTeam.teamId}_${userInTeam.userId}`
|
||||
}
|
||||
|
||||
export const Team = createReducer('teamService', defaultState)
|
||||
.withMapping<T.TeamActions>({
|
||||
create(state, action) {
|
||||
return {
|
||||
teamIds: state.teamIds.indexOf(action.payload.id) >= 0
|
||||
? state.teamIds
|
||||
: [...state.teamIds, action.payload.id],
|
||||
teamsById: {
|
||||
...state.teamsById,
|
||||
[action.payload.id]: action.payload,
|
||||
},
|
||||
}
|
||||
},
|
||||
update(state, action) {
|
||||
return {
|
||||
teamIds: state.teamIds.indexOf(action.payload.id) >= 0
|
||||
? state.teamIds
|
||||
: [...state.teamIds, action.payload.id],
|
||||
teamsById: {
|
||||
...state.teamsById,
|
||||
[action.payload.id]: action.payload,
|
||||
},
|
||||
}
|
||||
},
|
||||
remove(state, action) {
|
||||
return {
|
||||
teamIds: state.teamIds.filter(id => id !== action.payload.id),
|
||||
teamsById: without(state.teamsById, action.payload.id),
|
||||
}
|
||||
},
|
||||
addUser(state, action) {
|
||||
return {
|
||||
userKeysByTeamId: {
|
||||
...state.userKeysByTeamId,
|
||||
[action.payload.teamId]: [
|
||||
...state.userKeysByTeamId[action.payload.teamId],
|
||||
getUserKey(action.payload),
|
||||
],
|
||||
},
|
||||
usersByKey: {
|
||||
...state.usersByKey,
|
||||
[getUserKey(action.payload)]: action.payload,
|
||||
},
|
||||
}
|
||||
},
|
||||
removeUser(state, action) {
|
||||
const {payload} = action
|
||||
const {teamId} = payload
|
||||
const userKey = getUserKey(payload)
|
||||
@ -42,63 +86,20 @@ function removeUser(
|
||||
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: TTeamAction): ITeamState {
|
||||
switch (action.status) {
|
||||
case 'pending':
|
||||
return state
|
||||
case 'rejected':
|
||||
},
|
||||
find(state, action) {
|
||||
return {
|
||||
...state,
|
||||
error: action.payload.message,
|
||||
}
|
||||
case 'resolved':
|
||||
switch (action.type) {
|
||||
case 'TEAMS':
|
||||
return {
|
||||
...state,
|
||||
teamIds: action.payload.map(team => team.id),
|
||||
teamsById: indexBy(action.payload, 'id'),
|
||||
}
|
||||
case 'TEAM_CREATE':
|
||||
case '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,
|
||||
},
|
||||
}
|
||||
case 'TEAM_USER_ADD':
|
||||
return {
|
||||
...state,
|
||||
userKeysByTeamId: {
|
||||
...state.userKeysByTeamId,
|
||||
[action.payload.teamId]: [
|
||||
...state.userKeysByTeamId[action.payload.teamId],
|
||||
getUserKey(action.payload),
|
||||
],
|
||||
findOne(state, action) {
|
||||
throw new Error('TeamReducer#findOne not implemented')
|
||||
},
|
||||
usersByKey: {
|
||||
...state.usersByKey,
|
||||
[getUserKey(action.payload)]: action.payload,
|
||||
},
|
||||
}
|
||||
case 'TEAM_USER_REMOVE':
|
||||
return removeUser(state, action)
|
||||
case 'TEAM_USERS':
|
||||
findUsers(state, action) {
|
||||
const usersByKey = action.payload.usersInTeam
|
||||
.reduce((obj, userInTeam) => {
|
||||
obj[getUserKey(userInTeam)] = userInTeam
|
||||
@ -117,16 +118,5 @@ export function Team(state = defaultState, action: TTeamAction): ITeamState {
|
||||
...usersByKey,
|
||||
},
|
||||
}
|
||||
case 'TEAM_REMOVE':
|
||||
return {
|
||||
...state,
|
||||
teamIds: state.teamIds.filter(id => id !== action.payload.id),
|
||||
teamsById: without(state.teamsById, action.payload.id),
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@ -1,35 +1,33 @@
|
||||
import React from 'react'
|
||||
import {ITeam, IUser, IUserInTeam, TReadonlyRecord} from '@rondo.dev/common'
|
||||
import {TeamActions} from './TeamActions'
|
||||
import {FaUser, FaCheck, FaTimes} from 'react-icons/fa'
|
||||
import { IUser, IUserInTeam, team as Team, TReadonlyRecord, user as User } from '@rondo.dev/common';
|
||||
import { Button, Control, Heading, Help, Input, Panel, PanelBlock, PanelHeading } from 'bloomer';
|
||||
import React from 'react';
|
||||
import { FaCheck, FaTimes, FaUser } from 'react-icons/fa';
|
||||
|
||||
import {
|
||||
Button, Control, Heading, Help, Input, Panel, PanelHeading, PanelBlock
|
||||
} from 'bloomer'
|
||||
|
||||
const EMPTY_ARRAY: ReadonlyArray<string> = []
|
||||
|
||||
export interface ITeamUsersProps {
|
||||
// fetchMyTeams: () => void,
|
||||
fetchUsersInTeam: TeamActions['fetchUsersInTeam']
|
||||
findUserByEmail: TeamActions['findUserByEmail']
|
||||
fetchUsersInTeam: Team.TeamActions['findUsers']
|
||||
findUserByEmail: User.UserActions['findUserByEmail']
|
||||
|
||||
onAddUser: TeamActions['addUser']
|
||||
onRemoveUser: TeamActions['removeUser']
|
||||
onAddUser: Team.TeamActions['addUser']
|
||||
onRemoveUser: Team.TeamActions['removeUser']
|
||||
|
||||
team: ITeam
|
||||
team: Team.Team
|
||||
userKeysByTeamId: TReadonlyRecord<number, ReadonlyArray<string>>
|
||||
usersByKey: TReadonlyRecord<string, IUserInTeam>
|
||||
}
|
||||
|
||||
export interface ITeamUserProps {
|
||||
onRemoveUser: (params: {userId: number, teamId: number}) => void
|
||||
onRemoveUser: (
|
||||
params: {userId: number, teamId: number, roleId: number}) => void
|
||||
user: IUserInTeam
|
||||
}
|
||||
|
||||
export interface IAddUserProps {
|
||||
onAddUser: TeamActions['addUser']
|
||||
onSearchUser: TeamActions['findUserByEmail']
|
||||
onAddUser: Team.TeamActions['addUser']
|
||||
onSearchUser: User.UserActions['findUserByEmail']
|
||||
teamId: number
|
||||
}
|
||||
|
||||
@ -42,7 +40,7 @@ export interface IAddUserState {
|
||||
export class TeamUser extends React.PureComponent<ITeamUserProps> {
|
||||
handleRemoveUser = async () => {
|
||||
const {onRemoveUser, user} = this.props
|
||||
await onRemoveUser(user)
|
||||
await onRemoveUser({...user, roleId: 1})
|
||||
}
|
||||
render() {
|
||||
const {user} = this.props
|
||||
@ -145,7 +143,7 @@ export class TeamUserList extends React.PureComponent<ITeamUsersProps> {
|
||||
}
|
||||
async fetchUsersInTeam(teamId: number) {
|
||||
if (teamId) {
|
||||
await this.props.fetchUsersInTeam({teamId})
|
||||
await this.props.fetchUsersInTeam(teamId)
|
||||
}
|
||||
}
|
||||
render() {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export * from './TeamActions'
|
||||
export * from './TeamConnector'
|
||||
export * from './TeamList'
|
||||
export * from './TeamManager'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import T from 'react-dom/test-utils'
|
||||
import {createStore, TStateSelector} from '@rondo.dev/redux'
|
||||
import {createStore, TStateSelector, WaitMiddleware} from '@rondo.dev/redux'
|
||||
import {Provider} from 'react-redux'
|
||||
import {
|
||||
Action,
|
||||
@ -10,7 +10,10 @@ import {
|
||||
Reducer,
|
||||
ReducersMapObject,
|
||||
combineReducers,
|
||||
Store as ReduxStore,
|
||||
Unsubscribe,
|
||||
} from 'redux'
|
||||
import { format } from 'util'
|
||||
|
||||
interface IRenderParams<State, LocalState> {
|
||||
reducers: ReducersMapObject<State, any>
|
||||
@ -56,8 +59,12 @@ export class TestUtils {
|
||||
) {
|
||||
const {reducers, select} = params
|
||||
|
||||
const waitMiddleware = new WaitMiddleware()
|
||||
const recorder = waitMiddleware.record()
|
||||
|
||||
let store = this.createStore({
|
||||
reducer: this.combineReducers(reducers),
|
||||
extraMiddleware: [waitMiddleware.handle],
|
||||
})()
|
||||
|
||||
const withState = (state: DeepPartial<State>) => {
|
||||
@ -104,6 +111,9 @@ export class TestUtils {
|
||||
store,
|
||||
Component,
|
||||
withJSX,
|
||||
async waitForActions() {
|
||||
await waitMiddleware.waitForRecorded(recorder, 2000)
|
||||
},
|
||||
}
|
||||
|
||||
return self
|
||||
@ -119,4 +129,5 @@ interface ISelf<Props, Store, Component, CreateJSX> {
|
||||
Component: Component
|
||||
withJSX: (localCreateJSX: CreateJSX)
|
||||
=> ISelf<Props, Store, Component, CreateJSX>
|
||||
waitForActions(): Promise<void>
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
// import {ITeam} from './ITeam'
|
||||
import { TReduxed } from '@rondo.dev/jsonrpc'
|
||||
import { keys } from 'ts-transformer-keys'
|
||||
import { Team } from './entities'
|
||||
import { IUserInTeam } from './IUserInTeam'
|
||||
import { keys } from 'ts-transformer-keys'
|
||||
|
||||
export { Team }
|
||||
|
||||
export interface ITeamAddUserParams {
|
||||
teamId: number
|
||||
@ -22,6 +25,11 @@ export interface ITeamUpdateParams {
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ITeamUsers {
|
||||
teamId: number
|
||||
usersInTeam: IUserInTeam[]
|
||||
}
|
||||
|
||||
export interface IContext {
|
||||
userId: number
|
||||
}
|
||||
@ -41,9 +49,10 @@ export interface ITeamService {
|
||||
|
||||
find(): Promise<Team[]>
|
||||
|
||||
findUsers(teamId: number): Promise<IUserInTeam[]>
|
||||
findUsers(teamId: number): Promise<ITeamUsers>
|
||||
|
||||
// TODO add other methods
|
||||
}
|
||||
|
||||
export const TeamServiceMethods = keys<ITeamService>()
|
||||
export type TeamActions = TReduxed<ITeamService, 'teamService'>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { TReduxed } from '@rondo.dev/jsonrpc'
|
||||
import { keys } from 'ts-transformer-keys'
|
||||
import { ICredentials } from './ICredentials'
|
||||
import { IUser } from './IUser'
|
||||
import * as e from './entities'
|
||||
import {keys} from 'ts-transformer-keys'
|
||||
|
||||
export interface IChangePasswordParams {
|
||||
oldPassword: string
|
||||
@ -19,3 +19,4 @@ export interface IUserService {
|
||||
}
|
||||
|
||||
export const UserServiceMethods = keys<IUserService>()
|
||||
export type UserActions = TReduxed<IUserService, 'userService'>
|
||||
|
||||
8
packages/redux/src/bindActionCreators.ts
Normal file
8
packages/redux/src/bindActionCreators.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { bindActionCreators as bind, Dispatch } from 'redux'
|
||||
|
||||
export function bindActionCreators<T extends object>(
|
||||
obj: T,
|
||||
dispatch: Dispatch,
|
||||
): T {
|
||||
return bind(obj as any, dispatch)
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
export * from './actions'
|
||||
export * from './bindActionCreators'
|
||||
export * from './middleware'
|
||||
export * from './store'
|
||||
export * from './pack'
|
||||
|
||||
@ -49,11 +49,10 @@ export class WaitMiddleware {
|
||||
}
|
||||
|
||||
const actionsByName = actions.reduce((obj, type) => {
|
||||
obj[type] = true
|
||||
obj[type] = (obj[type] || 0) + 1
|
||||
return obj
|
||||
}, {} as Record<string, boolean>)
|
||||
// no duplicates here so we cannot use actions.length
|
||||
let count = Object.keys(actionsByName).length
|
||||
}, {} as Record<string, number>)
|
||||
let count = actions.length
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!actions.length) {
|
||||
@ -75,7 +74,7 @@ export class WaitMiddleware {
|
||||
case 'pending':
|
||||
return
|
||||
case 'resolved':
|
||||
actionsByName[action.type] = false
|
||||
actionsByName[action.type]--
|
||||
count--
|
||||
if (count === 0) {
|
||||
resolve()
|
||||
|
||||
@ -137,7 +137,10 @@ export class TeamService implements RPC<t.ITeamService> {
|
||||
})
|
||||
.getMany()
|
||||
|
||||
return userTeams.map(this._mapUserInTeam)
|
||||
return {
|
||||
teamId,
|
||||
usersInTeam: userTeams.map(this._mapUserInTeam),
|
||||
}
|
||||
}
|
||||
|
||||
protected _mapUserInTeam(ut: UserTeam): IUserInTeam {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user