Refactor action definitions to type less

This commit is contained in:
Jerko Steiner 2019-03-22 13:26:58 +08:00
parent f2e44f477c
commit 8f8c3b6c9c
20 changed files with 186 additions and 276 deletions

View File

@ -1,28 +0,0 @@
// Get the type of promise
// https://www.typescriptlang.org/docs/handbook/advanced-types.html
// section: Type inference in conditional types
type Unpacked<T> = T extends Promise<infer U> ? U : T
import {IAction} from './IAction'
import {IErrorAction} from './IErrorAction'
// Also from TypeScript handbook:
// https://www.typescriptlang.org/docs/handbook/advanced-types.html
type FunctionProperties<T> = {
[K in keyof T]:
T[K] extends (...args: any[]) => IAction<any, string>
? {
payload: Unpacked<ReturnType<T[K]>['payload']>,
type: Unpacked<ReturnType<T[K]>['type']>,
}
: T[K] extends (...args: any[]) => IErrorAction<string>
? {
error: Error,
type: Unpacked<ReturnType<T[K]>['type']>,
}
: never
}
// https://stackoverflow.com/questions/48305190/
// Is there an automatic way to create a discriminated union for all interfaces
// in a namespace?
export type ActionTypes<T> = FunctionProperties<T>[keyof FunctionProperties<T>]

View File

@ -0,0 +1,6 @@
import {IAction} from './IAction'
export type GetAction<MyTypes, T extends string> =
MyTypes extends IAction<T>
? MyTypes
: never

View File

@ -1,7 +1,3 @@
// Maybe this won't be necessary after this is merged:
// https://github.com/Microsoft/TypeScript/pull/29478
export interface IAction<T = any, ActionType extends string = string> {
payload: Promise<T> | T,
export interface IAction<ActionType extends string> {
type: ActionType
}

View File

@ -0,0 +1,12 @@
import {IPendingAction} from './IPendingAction'
import {IResolvedAction} from './IResolvedAction'
import {IRejectedAction} from './IRejectedAction'
export type IAsyncAction<T,
PendingActionType extends string,
ResolvedActionType extends string,
RejectedActionType extends string
> =
IPendingAction<T, PendingActionType>
| IResolvedAction<T, ResolvedActionType>
| IRejectedAction<RejectedActionType>

View File

@ -1,4 +0,0 @@
export interface IErrorAction<ActionType extends string> {
error: Error,
type: ActionType
}

View File

@ -0,0 +1,6 @@
import {IAction} from './IAction'
export interface IPendingAction<T, ActionType extends string> extends
IAction<ActionType> {
payload: Promise<T>
}

View File

@ -0,0 +1,6 @@
import {IAction} from './IAction'
export interface IRejectedAction<ActionType extends string> extends
IAction<ActionType> {
error: Error
}

View File

@ -0,0 +1,6 @@
import {IAction} from './IAction'
export interface IResolvedAction<T, ActionType extends string> extends
IAction<ActionType> {
payload: T
}

View File

@ -1,13 +0,0 @@
// Get the type of promise
// https://www.typescriptlang.org/docs/handbook/advanced-types.html
// section: Type inference in conditional types
type Unpacked<T> = T extends Promise<infer U> ? U : T
type FunctionProperties<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? {
payload: Unpacked<ReturnType<T[K]>['payload']>,
type: Unpacked<ReturnType<T[K]>['type']>,
}: never
}
export type UnionType<T> = FunctionProperties<T>[keyof FunctionProperties<T>]

View File

@ -1,4 +1,6 @@
export * from './ActionTypes'
export * from './GetAction'
export * from './IAction'
export * from './IErrorAction'
export * from './UnionType'
export * from './IAsyncAction'
export * from './IPendingAction'
export * from './IRejectedAction'
export * from './IResolvedAction'

View File

@ -1,4 +1,4 @@
import {IAction, IErrorAction, ActionTypes} from '../actions'
import {GetAction, IAsyncAction, IResolvedAction} from '../actions'
import {IAPIDef, ICredentials, INewUser, IUser} from '@rondo/common'
import {IHTTPClient} from '../http/IHTTPClient'
@ -18,56 +18,51 @@ export enum LoginActionKeys {
LOGIN_REDIRECT_SET = 'LOGIN_REDIRECT_SET',
}
export type LoginActionType =
IAsyncAction<IUser,
'LOGIN_PENDING',
'LOGIN_RESOLVED',
'LOGIN_REJECTED'>
| IAsyncAction<unknown,
'LOGIN_LOGOUT_PENDING',
'LOGIN_LOGOUT_RESOLVED',
'LOGIN_LOGOUT_REJECTED'>
| IAsyncAction<IUser,
'LOGIN_REGISTER_PENDING',
'LOGIN_REGISTER_RESOLVED',
'LOGIN_REGISTER_REJECTED'>
| IResolvedAction<{redirectTo: string}, 'LOGIN_REDIRECT_SET'>
type Action<T extends string> = GetAction<LoginActionType, T>
export class LoginActions {
constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
logIn = (credentials: ICredentials)
: IAction<IUser, LoginActionKeys.LOGIN> => {
logIn = (credentials: ICredentials): Action<'LOGIN_PENDING'> => {
return {
payload: this.http.post('/auth/login', credentials),
type: LoginActionKeys.LOGIN,
type: 'LOGIN_PENDING',
}
}
logInError = (error: Error)
: IErrorAction<LoginActionKeys.LOGIN_REJECTED> => {
return {
error,
type: LoginActionKeys.LOGIN_REJECTED,
}
}
logOut = (): IAction<unknown, LoginActionKeys.LOGIN_LOG_OUT> => {
logOut = (): Action<'LOGIN_LOGOUT_PENDING'> => {
return {
payload: this.http.get('/auth/logout'),
type: LoginActionKeys.LOGIN_LOG_OUT,
type: 'LOGIN_LOGOUT_PENDING',
}
}
register = (profile: INewUser):
IAction<IUser, LoginActionKeys.LOGIN_REGISTER> => {
register = (profile: INewUser): Action<'LOGIN_REGISTER_PENDING'> => {
return {
payload: this.http.post('/auth/register', profile),
type: LoginActionKeys.LOGIN_REGISTER,
type: 'LOGIN_REGISTER_PENDING',
}
}
registerError = (error: Error)
: IErrorAction<LoginActionKeys.LOGIN_REGISTER_REJECTED> => {
return {
error,
type: LoginActionKeys.LOGIN_REGISTER_REJECTED,
}
}
setRedirectTo = (redirectTo: string)
: IAction<{redirectTo: string}, LoginActionKeys.LOGIN_REDIRECT_SET> => {
setRedirectTo = (redirectTo: string): Action<'LOGIN_REDIRECT_SET'> => {
return {
payload: {redirectTo},
type: LoginActionKeys.LOGIN_REDIRECT_SET,
}
}
}
// This makes it very easy to write reducer code.
export type LoginActionType = ActionTypes<LoginActions>

View File

@ -1,5 +1,5 @@
import {IUser} from '@rondo/common'
import {LoginActionKeys, LoginActionType} from './LoginActions'
import {LoginActionType} from './LoginActions'
export interface ILoginState {
readonly error?: string
@ -18,17 +18,17 @@ export function Login(
action: LoginActionType,
): ILoginState {
switch (action.type) {
case LoginActionKeys.LOGIN:
case 'LOGIN_RESOLVED':
return {...state, user: action.payload, error: ''}
case LoginActionKeys.LOGIN_LOG_OUT:
case 'LOGIN_LOGOUT_RESOLVED':
return {...state, user: undefined}
case LoginActionKeys.LOGIN_REJECTED:
case 'LOGIN_REJECTED':
return {...state, error: action.error.message}
case LoginActionKeys.LOGIN_REGISTER:
case 'LOGIN_REGISTER_RESOLVED':
return {...state, user: action.payload, error: ''}
case LoginActionKeys.LOGIN_REGISTER_REJECTED:
case 'LOGIN_REGISTER_REJECTED':
return {...state, error: action.error.message}
case LoginActionKeys.LOGIN_REDIRECT_SET:
case 'LOGIN_REDIRECT_SET':
return {...state, redirectTo: action.payload.redirectTo}
default:
return state

View File

@ -10,7 +10,7 @@ export interface IComponentProps<Data> {
}
export interface IFormHOCProps<Data> {
onSubmit: (props: Data) => IAction<any, any>
onSubmit: (props: Data) => IAction<any>
// TODO figure out what would happen if the underlying child component
// would have the same required property as the HOC, like onSuccess?
onSuccess?: () => void

View File

@ -26,21 +26,21 @@ describe('PromiseMiddleware', () => {
expect(store.getState().slice(1)).toEqual([action])
})
it('dispatches pending and fulfilled action', async () => {
it('dispatches pending and resolved action', async () => {
const value = 123
const type = 'TEST'
const action = {
payload: Promise.resolve(value),
type,
type: `${type}_PENDING`,
}
const result = store.dispatch(action)
expect(result).toBe(action)
await result.payload
expect(store.getState().slice(1)).toEqual([{
type: `${type}_PENDING`,
...action,
}, {
payload: value,
type,
type: type + '_RESOLVED',
}])
})
@ -49,14 +49,14 @@ describe('PromiseMiddleware', () => {
const type = 'TEST'
const action = {
payload: Promise.reject(error),
type,
type: `${type}_PENDING`,
}
const result = store.dispatch(action)
expect(result).toBe(action)
const err = await getError(result.payload)
expect(err).toBe(error)
expect(store.getState().slice(1)).toEqual([{
type: `${type}_PENDING`,
...action,
}, {
error,
type: `${type}_REJECTED`,

View File

@ -11,7 +11,7 @@ function isPromise(value: any): value is Promise<any> {
*
* If `action.payload` is a `Promise`, it will be handled by this class. It
* differs from other promise middlewares for redux because by default it does
* not add an extension to action dispatched after a promise is fulfilled. This
* not add an extension to action dispatched after a promise is resolved. This
* makes it easier to infer types from the API endpoints so they can be used in
* both Action creators and Reducers.
*
@ -20,36 +20,42 @@ function isPromise(value: any): value is Promise<any> {
* const middleware = applyMiddleware(new PromiseMiddleware().handle)
*/
export class PromiseMiddleware {
protected regexp: RegExp
constructor(
readonly pendingExtension = '_PENDING',
readonly fulfilledExtension = '',
readonly resolvedExtension = '_RESOLVED',
readonly rejectedExtension = '_REJECTED',
) {
assert(
this.pendingExtension !== this.fulfilledExtension &&
this.fulfilledExtension !== this.rejectedExtension &&
this.pendingExtension !== this.resolvedExtension &&
this.resolvedExtension !== this.rejectedExtension &&
this.pendingExtension !== this.rejectedExtension,
'Pending, fulfilled and rejected extensions must be unique')
'Pending, resolved and rejected extensions must be unique')
this.regexp = new RegExp(pendingExtension + '$')
}
handle: Middleware = store => next => (action: AnyAction) => {
const {payload, type} = action
if (!isPromise(payload)) {
// Propagate this action. Only attach listeners to the promise.
next(action)
if (!isPromise(payload)) {
return
}
store.dispatch({type: type + this.pendingExtension})
const strippedType = type.replace(this.regexp, '')
payload
.then(result => {
store.dispatch({
payload: result,
type,
type: strippedType + this.resolvedExtension,
})
})
.catch(err => {
store.dispatch({
error: err,
type: type + this.rejectedExtension,
type: strippedType + this.rejectedExtension,
})
})

View File

@ -1,105 +1,73 @@
import {IAPIDef} from '@rondo/common'
import {IAction, IErrorAction, ActionTypes} from '../actions'
import {GetAction, IAsyncAction} 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',
export type TeamActionType =
IAsyncAction<ITeam[],
'TEAMS_PENDING',
'TEAMS_RESOLVED',
'TEAMS_REJECTED'>
| IAsyncAction<ITeam,
'TEAM_CREATE_PENDING',
'TEAM_CREATE_RESOLVED',
'TEAM_CREATE_REJECTED'>
| IAsyncAction<ITeam,
'TEAM_UPDATE_PENDING',
'TEAM_UPDATE_RESOLVED',
'TEAM_UPDATE_REJECTED'>
| IAsyncAction<{},
'TEAM_REMOVE_PENDING',
'TEAM_REMOVE_RESOLVED',
'TEAM_REMOVE_REJECTED'>
| IAsyncAction<IUserInTeam,
'TEAM_USER_ADD_PENDING',
'TEAM_USER_ADD_RESOLVED',
'TEAM_USER_ADD_REJECTED'>
| IAsyncAction<{userId: number, teamId: number},
'TEAM_USER_REMOVE_PENDING',
'TEAM_USER_REMOVE_RESOLVED',
'TEAM_USER_REMOVE_REJECTED'>
| IAsyncAction<{teamId: number, usersInTeam: IUserInTeam[]},
'TEAM_USERS_PENDING',
'TEAM_USERS_RESOLVED',
'TEAM_USERS_REJECTED'>
| IAsyncAction<IUser | undefined,
'TEAM_USER_FIND_PENDING',
'TEAM_USER_FIND_RESOLVED',
'TEAM_USER_FIND_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',
}
type Action<T extends string> = GetAction<TeamActionType, T>
export class TeamActions {
constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
fetchMyTeams = (): IAction<ITeam[], TeamActionKeys.TEAMS> => {
fetchMyTeams = (): Action<'TEAMS_PENDING'> => {
return {
payload: this.http.get('/my/teams'),
type: TeamActionKeys.TEAMS,
type: 'TEAMS_PENDING',
}
}
fetchMyTeamsError = (error: Error)
: IErrorAction<TeamActionKeys.TEAMS_REJECTED> => {
return {
error,
type: TeamActionKeys.TEAMS_REJECTED,
}
}
createTeam = (team: {name: string})
: IAction<ITeam, TeamActionKeys.TEAM_CREATE> => {
createTeam = (team: {name: string}): Action<'TEAM_CREATE_PENDING'> => {
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,
type: 'TEAM_CREATE_PENDING',
}
}
updateTeam = ({id, name}: {id: number, name: string})
: IAction<ITeam, TeamActionKeys.TEAM_UPDATE> => {
: Action<'TEAM_UPDATE_PENDING'> => {
return {
payload: this.http.put('/teams/:id', {name}, {id}),
type: TeamActionKeys.TEAM_UPDATE,
type: 'TEAM_UPDATE_PENDING',
}
}
updateTeamError = (error: Error)
: IErrorAction<TeamActionKeys.TEAM_UPDATE_REJECTED> => {
return {
error,
type: TeamActionKeys.TEAM_UPDATE_REJECTED,
}
}
removeTeam = ({id}: {id: number})
: IAction<{}, TeamActionKeys.TEAM_REMOVE> => {
removeTeam = ({id}: {id: number}): Action<'TEAM_REMOVE_PENDING'> => {
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,
type: 'TEAM_REMOVE_PENDING',
}
}
@ -109,21 +77,13 @@ export class TeamActions {
teamId: number,
roleId: number,
})
: IAction<IUserInTeam, TeamActionKeys.TEAM_USER_ADD> {
: Action<'TEAM_USER_ADD_PENDING'> {
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,
type: 'TEAM_USER_ADD_PENDING',
}
}
@ -132,66 +92,33 @@ export class TeamActions {
userId: number,
teamId: number,
})
: IAction<{
userId: number,
teamId: number,
}, TeamActionKeys.TEAM_USER_REMOVE> => {
: Action<'TEAM_USER_REMOVE_PENDING'> => {
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,
type: 'TEAM_USER_REMOVE_PENDING',
}
}
fetchUsersInTeam = ({teamId}: {teamId: number})
: IAction<{
teamId: number,
usersInTeam: IUserInTeam[]
}, TeamActionKeys.TEAM_USERS> => {
: Action<'TEAM_USERS_PENDING'> => {
return {
payload: this.http.get('/teams/:teamId/users', {
teamId,
})
.then(usersInTeam => ({teamId, usersInTeam})),
type: TeamActionKeys.TEAM_USERS,
type: 'TEAM_USERS_PENDING',
}
}
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> => {
findUserByEmail = (email: string): Action<'TEAM_USER_FIND_PENDING'> => {
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,
type: 'TEAM_USER_FIND_PENDING',
}
}
}
export type TeamActionType = ActionTypes<TeamActions>

View File

@ -1,26 +1,26 @@
import React from 'react'
import {IAction} from '../actions'
import {ITeam, ReadonlyRecord} from '@rondo/common'
import {TeamActions} from './TeamActions'
export interface ITeamListProps {
teamsById: ReadonlyRecord<number, ITeam>,
teamIds: ReadonlyArray<number>,
onAddTeam: (params: {name: string}) => IAction
onRemoveTeam: (params: {id: number}) => IAction
onUpdateTeam: (params: {id: number, name: string}) => IAction
onAddTeam: TeamActions['createTeam']
onRemoveTeam: TeamActions['removeTeam']
onUpdateTeam: TeamActions['updateTeam']
editTeamId: number
}
export interface ITeamProps {
team: ITeam
editTeamId: number // TODO handle edits via react-router params
onRemoveTeam: (params: {id: number}) => IAction
onUpdateTeam: (params: {id: number, name: string}) => IAction
onRemoveTeam: TeamActions['removeTeam']
onUpdateTeam: TeamActions['updateTeam']
}
export interface IAddTeamProps {
onAddTeam: (params: {name: string}) => IAction
onUpdateTeam: (params: {id: number, name: string}) => IAction
onAddTeam: TeamActions['createTeam']
onUpdateTeam: TeamActions['updateTeam']
team?: ITeam
}

View File

@ -1,19 +1,19 @@
import React from 'react'
import {IAction} from '../actions'
import {ITeam, IUser, IUserInTeam, ReadonlyRecord} from '@rondo/common'
import {ITeam, IUserInTeam, ReadonlyRecord} from '@rondo/common'
import {TeamList} from './TeamList'
import {TeamUserList} from './TeamUserList'
import {TeamActions} from './TeamActions'
export interface ITeamManagerProps {
createTeam: (params: {name: string}) => IAction
updateTeam: (params: {id: number, name: string}) => IAction
removeTeam: (params: {id: number}) => IAction
createTeam: TeamActions['createTeam']
updateTeam: TeamActions['updateTeam']
removeTeam: TeamActions['removeTeam']
addUser: (params: {userId: number, teamId: number, roleId: number}) => IAction
removeUser: (params: {userId: number, teamId: number}) => IAction
fetchMyTeams: () => IAction
fetchUsersInTeam: (params: {teamId: number}) => IAction
findUserByEmail: (email: string) => IAction<IUser | undefined>
addUser: TeamActions['addUser']
removeUser: TeamActions['removeUser']
fetchMyTeams: TeamActions['fetchMyTeams']
fetchUsersInTeam: TeamActions['fetchUsersInTeam']
findUserByEmail: TeamActions['findUserByEmail']
teamsById: ReadonlyRecord<number, ITeam>
teamIds: ReadonlyArray<number>

View File

@ -1,5 +1,6 @@
import {ITeam, IUserInTeam, ReadonlyRecord, indexBy} from '@rondo/common'
import {TeamActionKeys, TeamActionType} from './TeamActions'
import {TeamActionType} from './TeamActions'
import {GetAction} from '../actions'
export interface ITeamState {
readonly error: string
@ -23,10 +24,7 @@ const defaultState: ITeamState = {
function removeUser(
state: ITeamState,
action: {
payload: {userId: number, teamId: number},
type: TeamActionKeys.TEAM_USER_REMOVE,
},
action: GetAction<TeamActionType, 'TEAM_USER_REMOVE_RESOLVED'>,
) {
const {payload} = action
@ -54,14 +52,14 @@ function getUserKey(userInTeam: {userId: number, teamId: number}) {
export function Team(state = defaultState, action: TeamActionType): ITeamState {
switch (action.type) {
case TeamActionKeys.TEAMS:
case 'TEAMS_RESOLVED':
return {
...state,
teamIds: action.payload.map(team => team.id),
teamsById: indexBy(action.payload, 'id'),
}
case TeamActionKeys.TEAM_CREATE:
case TeamActionKeys.TEAM_UPDATE:
case 'TEAM_CREATE_RESOLVED':
case 'TEAM_UPDATE_RESOLVED':
return {
...state,
teamIds: state.teamIds.indexOf(action.payload.id) >= 0
@ -73,7 +71,7 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
},
}
return state
case TeamActionKeys.TEAM_USER_ADD:
case 'TEAM_USER_ADD_RESOLVED':
return {
...state,
userKeysByTeamId: {
@ -88,9 +86,9 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
[getUserKey(action.payload)]: action.payload,
},
}
case TeamActionKeys.TEAM_USER_REMOVE:
case 'TEAM_USER_REMOVE_RESOLVED':
return removeUser(state, action)
case TeamActionKeys.TEAM_USERS:
case 'TEAM_USERS_RESOLVED':
const usersByKey = action.payload.usersInTeam
.reduce((obj, userInTeam) => {
obj[getUserKey(userInTeam)] = userInTeam
@ -109,11 +107,11 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
...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:
case 'TEAM_CREATE_REJECTED':
case 'TEAM_UPDATE_REJECTED':
case 'TEAM_USER_ADD_REJECTED':
case 'TEAM_USER_REMOVE_REJECTED':
case 'TEAM_USERS_REJECTED':
return {
...state,
error: action.error.message,

View File

@ -1,17 +1,16 @@
import React from 'react'
import {IUser, IUserInTeam, ReadonlyRecord} from '@rondo/common'
import {IAction} from '../actions'
import {TeamActions} from './TeamActions'
const EMPTY_ARRAY: ReadonlyArray<string> = []
export interface ITeamUsersProps {
// fetchMyTeams: () => void,
fetchUsersInTeam: (params: {teamId: number}) => IAction
findUserByEmail: (email: string) => IAction
fetchUsersInTeam: TeamActions['fetchUsersInTeam']
findUserByEmail: TeamActions['findUserByEmail']
onAddUser: (params: {userId: number, teamId: number, roleId: number})
=> IAction<IUserInTeam>
onRemoveUser: (params: {userId: number, teamId: number}) => IAction
onAddUser: TeamActions['addUser']
onRemoveUser: TeamActions['removeUser']
teamId: number
userKeysByTeamId: ReadonlyRecord<number, ReadonlyArray<string>>
@ -24,12 +23,8 @@ export interface ITeamUserProps {
}
export interface IAddUserProps {
onAddUser: (params: {
userId: number,
teamId: number,
roleId: number,
}) => IAction<IUserInTeam>
onSearchUser: (email: string) => IAction<IUser>
onAddUser: TeamActions['addUser']
onSearchUser: TeamActions['findUserByEmail']
teamId: number
}