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