Use default action type and add status to async actions

Since TypeScript can infer types based on string types, it has became
easier to define a single constant of an action that returns a promise,
and then dispatch actions with different statuses: pending, resolved,
and/or rejected. This seems like more typing at first, but in the long
run it will become easier to write generic reducer methods, and the
names of reduced types will not have to be repeated every time.

For example, previously we had to write:

    type MyActionTypes =
      IAsyncAction<IUser, 'LOGIN_PENDING', 'LOGIN_RESOLVED', 'LOGIN_REJECTED'>
      | ...

And now we can write:

    type MyActionTypes =
      IAsyncAction<IUser, 'LOGIN'>
      | ...
This commit is contained in:
Jerko Steiner 2019-04-01 15:20:41 +08:00
parent 61e7e8db4f
commit 8b6f90235e
18 changed files with 253 additions and 277 deletions

View File

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

View File

@ -0,0 +1,6 @@
import {IAsyncAction} from './IAsyncAction'
export type GetPendingAction<MyTypes, T extends string> =
MyTypes extends IAsyncAction<infer U, T> & {status: 'pending'}
? MyTypes
: never

View File

@ -0,0 +1,6 @@
import {IAsyncAction} from './IAsyncAction'
export type GetResolvedAction<MyTypes, T extends string> =
MyTypes extends IAsyncAction<infer U, T> & {status: 'resolved'}
? MyTypes
: never

View File

@ -1,3 +1,4 @@
export interface IAction<ActionType extends string> { export interface IAction<T, ActionType extends string> {
payload: T
type: ActionType type: ActionType
} }

View File

@ -2,11 +2,9 @@ import {IPendingAction} from './IPendingAction'
import {IResolvedAction} from './IResolvedAction' import {IResolvedAction} from './IResolvedAction'
import {IRejectedAction} from './IRejectedAction' import {IRejectedAction} from './IRejectedAction'
export type IAsyncAction<T, export type IAsyncStatus = 'pending' | 'resolved' | 'rejected'
PendingActionType extends string,
ResolvedActionType extends string, export type IAsyncAction<T, ActionType extends string> =
RejectedActionType extends string IPendingAction<T, ActionType>
> = | IResolvedAction<T, ActionType>
IPendingAction<T, PendingActionType> | IRejectedAction<ActionType>
| IResolvedAction<T, ResolvedActionType>
| IRejectedAction<RejectedActionType>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
import {IPendingAction} from './IPendingAction'
export class PendingAction<T, ActionType extends string> {
readonly status = 'pending'
constructor(
readonly payload: T,
readonly type: ActionType,
) {}
}

View File

@ -1,6 +1,9 @@
export * from './GetAction' export * from './GetAction'
export * from './GetResolvedAction'
export * from './GetPendingAction'
export * from './IAction' export * from './IAction'
export * from './IAsyncAction' export * from './IAsyncAction'
export * from './IPendingAction' export * from './IPendingAction'
export * from './IRejectedAction' export * from './IRejectedAction'
export * from './IResolvedAction' export * from './IResolvedAction'
export * from './PendingAction'

View File

@ -48,37 +48,31 @@ export class CRUDReducer<T extends ICRUDIdable> {
readonly resolvedExtension = '_RESOLVED', readonly resolvedExtension = '_RESOLVED',
readonly rejectedExtension = '_REJECTED', readonly rejectedExtension = '_REJECTED',
) { ) {
const defaultMethodStatus = this.getDefaultMethodStatus()
this.defaultState = { this.defaultState = {
ids: [], ids: [],
byId: {}, byId: {},
status: { status: {
post: { post: defaultMethodStatus,
error: '', put: defaultMethodStatus,
isLoading: false, delete: defaultMethodStatus,
}, get: defaultMethodStatus,
put: { getMany: defaultMethodStatus,
error: '',
isLoading: false,
},
delete: {
error: '',
isLoading: false,
},
get: {
error: '',
isLoading: false,
},
getMany: {
error: '',
isLoading: false,
},
}, },
} }
this.actionTypes = this.getActionTypes() this.actionTypes = this.getActionTypes()
} }
getDefaultMethodStatus(): ICRUDMethodStatus {
return {
error: '',
isLoading: false,
}
}
protected getPromiseActionNames(type: string) { protected getPromiseActionNames(type: string) {
return { return {
pending: type + this.pendingExtension, pending: type + this.pendingExtension,

View File

@ -1,4 +1,4 @@
import {GetAction, IResolvedAction} from '../actions' import {GetAction, IAction} from '../actions'
export interface ICrumbLink { export interface ICrumbLink {
name: string name: string
@ -11,7 +11,7 @@ export interface ICrumbs {
} }
export type CrumbsActionType = export type CrumbsActionType =
IResolvedAction<ICrumbs, 'BREADCRUMBS_SET'> IAction<ICrumbs, 'BREADCRUMBS_SET'>
type Action<T extends string> = GetAction<CrumbsActionType, T> type Action<T extends string> = GetAction<CrumbsActionType, T>

View File

@ -1,68 +1,43 @@
import {GetAction, IAsyncAction, IResolvedAction} from '../actions' import {GetAction, IAsyncAction, IAction, PendingAction} from '../actions'
import {IAPIDef, ICredentials, INewUser, IUser} from '@rondo/common' import {IAPIDef, ICredentials, INewUser, IUser} from '@rondo/common'
import {IHTTPClient} from '../http/IHTTPClient' import {IHTTPClient} from '../http/IHTTPClient'
export enum LoginActionKeys {
LOGIN = 'LOGIN',
LOGIN_PENDING = 'LOGIN_PENDING',
LOGIN_REJECTED = 'LOGIN_REJECTED',
LOGIN_LOG_OUT = 'LOGIN_LOG_OUT',
LOGIN_LOG_OUT_PENDING = 'LOGIN_LOG_OUT_PENDING',
LOGIN_LOG_OUT_REJECTED = 'LOGIN_LOG_OUT_REJECTED',
LOGIN_REGISTER = 'LOGIN_REGISTER',
LOGIN_REGISTER_PENDING = 'LOGIN_REGISTER_PENDING',
LOGIN_REGISTER_REJECTED = 'LOGIN_REGISTER_REJECTED',
LOGIN_REDIRECT_SET = 'LOGIN_REDIRECT_SET',
}
export type LoginActionType = export type LoginActionType =
IAsyncAction<IUser, IAsyncAction<IUser, 'LOGIN'>
'LOGIN_PENDING', | IAsyncAction<unknown, 'LOGIN_LOGOUT'>
'LOGIN_RESOLVED', | IAsyncAction<IUser, 'LOGIN_REGISTER'>
'LOGIN_REJECTED'> | IAction<{redirectTo: string}, 'LOGIN_REDIRECT_SET'>
| 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> type Action<T extends string> = GetAction<LoginActionType, T>
export class LoginActions { export class LoginActions {
constructor(protected readonly http: IHTTPClient<IAPIDef>) {} constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
logIn = (credentials: ICredentials): Action<'LOGIN_PENDING'> => { logIn = (credentials: ICredentials) => {
return { return new PendingAction(
payload: this.http.post('/auth/login', credentials), this.http.post('/auth/login', credentials),
type: 'LOGIN_PENDING', 'LOGIN',
} )
} }
logOut = (): Action<'LOGIN_LOGOUT_PENDING'> => { logOut = () => {
return { return new PendingAction(
payload: this.http.get('/auth/logout'), this.http.get('/auth/logout'),
type: 'LOGIN_LOGOUT_PENDING', 'LOGIN_LOGOUT',
} )
} }
register = (profile: INewUser): Action<'LOGIN_REGISTER_PENDING'> => { register = (profile: INewUser) => {
return { return new PendingAction(
payload: this.http.post('/auth/register', profile), this.http.post('/auth/register', profile),
type: 'LOGIN_REGISTER_PENDING', 'LOGIN_REGISTER',
} )
} }
setRedirectTo = (redirectTo: string): Action<'LOGIN_REDIRECT_SET'> => { setRedirectTo = (redirectTo: string): Action<'LOGIN_REDIRECT_SET'> => {
return { return {
payload: {redirectTo}, payload: {redirectTo},
type: LoginActionKeys.LOGIN_REDIRECT_SET, type: 'LOGIN_REDIRECT_SET',
} }
} }
} }

View File

@ -2,13 +2,15 @@ import {IUser} from '@rondo/common'
import {LoginActionType} from './LoginActions' import {LoginActionType} from './LoginActions'
export interface ILoginState { export interface ILoginState {
readonly error?: string readonly error: string
readonly isLoading: boolean
readonly user?: IUser readonly user?: IUser
readonly redirectTo: string readonly redirectTo: string
} }
const defaultState: ILoginState = { const defaultState: ILoginState = {
error: undefined, error: '',
isLoading: false,
user: undefined, user: undefined,
redirectTo: '/', redirectTo: '/',
} }
@ -18,19 +20,33 @@ export function Login(
action: LoginActionType, action: LoginActionType,
): ILoginState { ): ILoginState {
switch (action.type) { switch (action.type) {
case 'LOGIN_RESOLVED': // sync actions
return {...state, user: action.payload, error: ''}
case 'LOGIN_LOGOUT_RESOLVED':
return {...state, user: undefined}
case 'LOGIN_REJECTED':
return {...state, error: action.error.message}
case 'LOGIN_REGISTER_RESOLVED':
return {...state, user: action.payload, error: ''}
case 'LOGIN_REGISTER_REJECTED':
return {...state, error: action.error.message}
case 'LOGIN_REDIRECT_SET': case 'LOGIN_REDIRECT_SET':
return {...state, redirectTo: action.payload.redirectTo} return {...state, redirectTo: action.payload.redirectTo}
default: default:
// async actions
switch (action.status) {
case 'pending':
return {
...state,
isLoading: true,
}
case 'rejected':
return {
...state,
isLoading: false,
error: action.payload.message,
}
case 'resolved':
switch (action.type) {
case 'LOGIN':
return {...state, user: action.payload, error: ''}
case 'LOGIN_LOGOUT':
return {...state, user: undefined}
case 'LOGIN_REGISTER':
return {...state, user: action.payload, error: ''}
}
}
return state return state
} }
} }

View File

@ -4,13 +4,6 @@ import {getError} from '../test-utils'
describe('PromiseMiddleware', () => { describe('PromiseMiddleware', () => {
describe('constructor', () => {
it('throws an error when action types are the same', () => {
expect(() => new PromiseMiddleware('a', 'a', 'a')).toThrowError()
expect(new PromiseMiddleware('a', 'b', 'c')).toBeTruthy()
})
})
let store!: Store let store!: Store
beforeEach(() => { beforeEach(() => {
const middleware = new PromiseMiddleware() const middleware = new PromiseMiddleware()
@ -22,8 +15,11 @@ describe('PromiseMiddleware', () => {
it('does nothing when payload is not a promise', () => { it('does nothing when payload is not a promise', () => {
const action = {type: 'test'} const action = {type: 'test'}
store.dispatch(action) const result = store.dispatch(action)
expect(store.getState().slice(1)).toEqual([action]) expect(result).toBe(action)
expect(store.getState().slice(1)).toEqual([{
type: action.type,
}])
}) })
it('dispatches pending and resolved action', async () => { it('dispatches pending and resolved action', async () => {
@ -31,16 +27,21 @@ describe('PromiseMiddleware', () => {
const type = 'TEST' const type = 'TEST'
const action = { const action = {
payload: Promise.resolve(value), payload: Promise.resolve(value),
type: `${type}_PENDING`, type,
} }
const result = store.dispatch(action) const result = store.dispatch(action)
expect(result).toBe(action) expect(result).toEqual({
...action,
status: 'pending',
})
await result.payload await result.payload
expect(store.getState().slice(1)).toEqual([{ expect(store.getState().slice(1)).toEqual([{
...action, ...action,
status: 'pending',
}, { }, {
payload: value, payload: value,
type: type + '_RESOLVED', status: 'resolved',
type,
}]) }])
}) })
@ -49,17 +50,23 @@ describe('PromiseMiddleware', () => {
const type = 'TEST' const type = 'TEST'
const action = { const action = {
payload: Promise.reject(error), payload: Promise.reject(error),
type: `${type}_PENDING`, type,
} }
const result = store.dispatch(action) const result = store.dispatch(action)
expect(result).toBe(action) expect(result).toEqual({
...action,
status: 'pending',
})
const err = await getError(result.payload) const err = await getError(result.payload)
expect(err).toBe(error) expect(err).toBe(error)
expect(store.getState().slice(1)).toEqual([{ expect(store.getState().slice(1)).toEqual([{
...action, payload: action.payload,
status: 'pending',
type,
}, { }, {
error, error,
type: `${type}_REJECTED`, status: 'rejected',
type,
}]) }])
}) })

View File

@ -20,45 +20,34 @@ function isPromise(value: any): value is Promise<any> {
* const middleware = applyMiddleware(new PromiseMiddleware().handle) * const middleware = applyMiddleware(new PromiseMiddleware().handle)
*/ */
export class PromiseMiddleware { export class PromiseMiddleware {
protected regexp: RegExp
constructor(
readonly pendingExtension = '_PENDING',
readonly resolvedExtension = '_RESOLVED',
readonly rejectedExtension = '_REJECTED',
) {
assert(
this.pendingExtension !== this.resolvedExtension &&
this.resolvedExtension !== this.rejectedExtension &&
this.pendingExtension !== this.rejectedExtension,
'Pending, resolved and rejected extensions must be unique')
this.regexp = new RegExp(pendingExtension + '$')
}
handle: Middleware = store => next => (action: AnyAction) => { handle: Middleware = store => next => (action: AnyAction) => {
const {payload, type} = action const {payload, type} = action
// Propagate this action. Only attach listeners to the promise.
next(action)
if (!isPromise(payload)) { if (!isPromise(payload)) {
return return next(action)
} }
const pendingAction = {
const strippedType = type.replace(this.regexp, '') ...action,
status: 'pending',
}
// Propagate this action. Only attach listeners to the promise.
next(pendingAction)
payload payload
.then(result => { .then(result => {
store.dispatch({ store.dispatch({
payload: result, payload: result,
type: strippedType + this.resolvedExtension, status: 'resolved',
type,
}) })
}) })
.catch(err => { .catch(err => {
store.dispatch({ store.dispatch({
error: err, error: err,
type: strippedType + this.rejectedExtension, status: 'rejected',
type,
}) })
}) })
return action return pendingAction
} }
} }

View File

@ -1,74 +1,44 @@
import {IAPIDef} from '@rondo/common' import {IAPIDef} from '@rondo/common'
import {GetAction, IAsyncAction} from '../actions' import {GetPendingAction, IAsyncAction, PendingAction} from '../actions'
import {IHTTPClient} from '../http/IHTTPClient' import {IHTTPClient} from '../http/IHTTPClient'
import {ITeam, IUser, IUserInTeam} from '@rondo/common' import {ITeam, IUser, IUserInTeam} from '@rondo/common'
export type TeamActionType = export type TeamActionType =
IAsyncAction<ITeam[], IAsyncAction<ITeam[], 'TEAMS'>
'TEAMS_PENDING', | IAsyncAction<ITeam, 'TEAM_CREATE'>
'TEAMS_RESOLVED', | IAsyncAction<ITeam, 'TEAM_UPDATE'>
'TEAMS_REJECTED'> | IAsyncAction<{id: number}, 'TEAM_REMOVE'>
| IAsyncAction<ITeam, | IAsyncAction<IUserInTeam, 'TEAM_USER_ADD'>
'TEAM_CREATE_PENDING', | IAsyncAction<{userId: number, teamId: number}, 'TEAM_USER_REMOVE'>
'TEAM_CREATE_RESOLVED', | IAsyncAction<{teamId: number, usersInTeam: IUserInTeam[]}, 'TEAM_USERS'>
'TEAM_CREATE_REJECTED'> | IAsyncAction<IUser | undefined, 'TEAM_USER_FIND'>
| IAsyncAction<ITeam,
'TEAM_UPDATE_PENDING',
'TEAM_UPDATE_RESOLVED',
'TEAM_UPDATE_REJECTED'>
| IAsyncAction<{id: number},
'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'>
type Action<T extends string> = GetAction<TeamActionType, T> type Action<T extends string> = GetPendingAction<TeamActionType, T>
export class TeamActions { export class TeamActions {
constructor(protected readonly http: IHTTPClient<IAPIDef>) {} constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
fetchMyTeams = (): Action<'TEAMS_PENDING'> => { fetchMyTeams = (): Action<'TEAMS'> => {
return { return new PendingAction(this.http.get('/my/teams'), 'TEAMS')
payload: this.http.get('/my/teams'),
type: 'TEAMS_PENDING',
}
} }
createTeam = (team: {name: string}): Action<'TEAM_CREATE_PENDING'> => { createTeam = (team: {name: string}): Action<'TEAM_CREATE'> => {
return { return new PendingAction(this.http.post('/teams', team), 'TEAM_CREATE')
payload: this.http.post('/teams', team),
type: 'TEAM_CREATE_PENDING',
}
} }
updateTeam = ({id, name}: {id: number, name: string}) updateTeam = ({id, name}: {id: number, name: string})
: Action<'TEAM_UPDATE_PENDING'> => { : Action<'TEAM_UPDATE'> => {
return { return new PendingAction(
payload: this.http.put('/teams/:id', {name}, {id}), this.http.put('/teams/:id', {name}, {id}),
type: 'TEAM_UPDATE_PENDING', 'TEAM_UPDATE',
} )
} }
removeTeam = ({id}: {id: number}): Action<'TEAM_REMOVE_PENDING'> => { removeTeam = ({id}: {id: number}): Action<'TEAM_REMOVE'> => {
return { return new PendingAction(
payload: this.http.delete('/teams/:id', {}, {id}), this.http.delete('/teams/:id', {}, {id}),
type: 'TEAM_REMOVE_PENDING', 'TEAM_REMOVE',
} )
} }
addUser( addUser(
@ -77,14 +47,14 @@ export class TeamActions {
teamId: number, teamId: number,
roleId: number, roleId: number,
}) })
: Action<'TEAM_USER_ADD_PENDING'> { : Action<'TEAM_USER_ADD'> {
return { return new PendingAction(
payload: this.http.post('/teams/:teamId/users/:userId', {}, { this.http.post('/teams/:teamId/users/:userId', {}, {
userId, userId,
teamId, teamId,
}), }),
type: 'TEAM_USER_ADD_PENDING', 'TEAM_USER_ADD',
} )
} }
removeUser = ( removeUser = (
@ -92,33 +62,31 @@ export class TeamActions {
userId: number, userId: number,
teamId: number, teamId: number,
}) })
: Action<'TEAM_USER_REMOVE_PENDING'> => { : Action<'TEAM_USER_REMOVE'> => {
return { return new PendingAction(
payload: this.http.delete('/teams/:teamId/users/:userId', {}, { this.http.delete('/teams/:teamId/users/:userId', {}, {
userId, userId,
teamId, teamId,
}), }),
type: 'TEAM_USER_REMOVE_PENDING', 'TEAM_USER_REMOVE',
} )
} }
fetchUsersInTeam = ({teamId}: {teamId: number}) fetchUsersInTeam = ({teamId}: {teamId: number})
: Action<'TEAM_USERS_PENDING'> => { : Action<'TEAM_USERS'> => {
return { return new PendingAction(
payload: this.http.get('/teams/:teamId/users', {}, { this.http.get('/teams/:teamId/users', {}, {
teamId, teamId,
}) })
.then(usersInTeam => ({teamId, usersInTeam})), .then(usersInTeam => ({teamId, usersInTeam})),
type: 'TEAM_USERS_PENDING', 'TEAM_USERS',
} )
} }
findUserByEmail = (email: string): Action<'TEAM_USER_FIND_PENDING'> => { findUserByEmail = (email: string): Action<'TEAM_USER_FIND'> => {
return { return new PendingAction(
payload: this.http.get('/users/emails/:email', {}, { this.http.get('/users/emails/:email', {}, {email}),
email, 'TEAM_USER_FIND',
}), )
type: 'TEAM_USER_FIND_PENDING',
}
} }
} }

View File

@ -2,7 +2,7 @@ import {
ITeam, IUserInTeam, ReadonlyRecord, indexBy, without, ITeam, IUserInTeam, ReadonlyRecord, indexBy, without,
} from '@rondo/common' } from '@rondo/common'
import {TeamActionType} from './TeamActions' import {TeamActionType} from './TeamActions'
import {GetAction} from '../actions' import {GetResolvedAction} from '../actions'
export interface ITeamState { export interface ITeamState {
readonly error: string readonly error: string
@ -26,7 +26,7 @@ const defaultState: ITeamState = {
function removeUser( function removeUser(
state: ITeamState, state: ITeamState,
action: GetAction<TeamActionType, 'TEAM_USER_REMOVE_RESOLVED'>, action: GetResolvedAction<TeamActionType, 'TEAM_USER_REMOVE'>,
) { ) {
const {payload} = action const {payload} = action
@ -53,15 +53,24 @@ function getUserKey(userInTeam: {userId: number, teamId: number}) {
} }
export function Team(state = defaultState, action: TeamActionType): ITeamState { export function Team(state = defaultState, action: TeamActionType): ITeamState {
switch (action.status) {
case 'pending':
return state
case 'rejected':
return {
...state,
error: action.payload.message,
}
case 'resolved':
switch (action.type) { switch (action.type) {
case 'TEAMS_RESOLVED': case 'TEAMS':
return { return {
...state, ...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_RESOLVED': case 'TEAM_CREATE':
case 'TEAM_UPDATE_RESOLVED': case 'TEAM_UPDATE':
return { return {
...state, ...state,
teamIds: state.teamIds.indexOf(action.payload.id) >= 0 teamIds: state.teamIds.indexOf(action.payload.id) >= 0
@ -72,7 +81,7 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
[action.payload.id]: action.payload, [action.payload.id]: action.payload,
}, },
} }
case 'TEAM_USER_ADD_RESOLVED': case 'TEAM_USER_ADD':
return { return {
...state, ...state,
userKeysByTeamId: { userKeysByTeamId: {
@ -87,9 +96,9 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
[getUserKey(action.payload)]: action.payload, [getUserKey(action.payload)]: action.payload,
}, },
} }
case 'TEAM_USER_REMOVE_RESOLVED': case 'TEAM_USER_REMOVE':
return removeUser(state, action) return removeUser(state, action)
case 'TEAM_USERS_RESOLVED': 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
@ -108,20 +117,14 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
...usersByKey, ...usersByKey,
}, },
} }
case 'TEAM_REMOVE_RESOLVED': case 'TEAM_REMOVE':
return { return {
...state, ...state,
teamIds: state.teamIds.filter(id => id !== action.payload.id), teamIds: state.teamIds.filter(id => id !== action.payload.id),
teamsById: without(state.teamsById, action.payload.id), teamsById: without(state.teamsById, action.payload.id),
} }
case 'TEAM_CREATE_REJECTED': default:
case 'TEAM_UPDATE_REJECTED': return state
case 'TEAM_USER_ADD_REJECTED':
case 'TEAM_USER_REMOVE_REJECTED':
case 'TEAM_USERS_REJECTED':
return {
...state,
error: action.error.message,
} }
default: default:
return state return state