From 8b6f90235e4435648cf0478f891c0bea18c69f51 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Mon, 1 Apr 2019 15:20:41 +0800 Subject: [PATCH] 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 | ... And now we can write: type MyActionTypes = IAsyncAction | ... --- packages/client/src/actions/GetAction.ts | 2 +- .../client/src/actions/GetPendingAction.ts | 6 + .../client/src/actions/GetResolvedAction.ts | 6 + packages/client/src/actions/IAction.ts | 3 +- packages/client/src/actions/IAsyncAction.ts | 14 +- packages/client/src/actions/IPendingAction.ts | 4 +- .../client/src/actions/IRejectedAction.ts | 4 +- .../client/src/actions/IResolvedAction.ts | 3 +- packages/client/src/actions/PendingAction.ts | 9 ++ packages/client/src/actions/index.ts | 3 + packages/client/src/crud/CRUDReducer.ts | 34 ++--- packages/client/src/crumbs/CrumbsActions.ts | 4 +- packages/client/src/login/LoginActions.ts | 67 +++------ packages/client/src/login/LoginReducer.ts | 40 +++-- .../src/middleware/PromiseMiddleware.test.ts | 39 +++-- .../src/middleware/PromiseMiddleware.ts | 35 ++--- packages/client/src/team/TeamActions.ts | 120 ++++++--------- packages/client/src/team/TeamReducer.ts | 137 +++++++++--------- 18 files changed, 253 insertions(+), 277 deletions(-) create mode 100644 packages/client/src/actions/GetPendingAction.ts create mode 100644 packages/client/src/actions/GetResolvedAction.ts create mode 100644 packages/client/src/actions/PendingAction.ts diff --git a/packages/client/src/actions/GetAction.ts b/packages/client/src/actions/GetAction.ts index df704c0..03d6d42 100644 --- a/packages/client/src/actions/GetAction.ts +++ b/packages/client/src/actions/GetAction.ts @@ -1,6 +1,6 @@ import {IAction} from './IAction' export type GetAction = - MyTypes extends IAction + MyTypes extends IAction ? MyTypes : never diff --git a/packages/client/src/actions/GetPendingAction.ts b/packages/client/src/actions/GetPendingAction.ts new file mode 100644 index 0000000..6334b02 --- /dev/null +++ b/packages/client/src/actions/GetPendingAction.ts @@ -0,0 +1,6 @@ +import {IAsyncAction} from './IAsyncAction' + +export type GetPendingAction = + MyTypes extends IAsyncAction & {status: 'pending'} + ? MyTypes + : never diff --git a/packages/client/src/actions/GetResolvedAction.ts b/packages/client/src/actions/GetResolvedAction.ts new file mode 100644 index 0000000..8e5b5ea --- /dev/null +++ b/packages/client/src/actions/GetResolvedAction.ts @@ -0,0 +1,6 @@ +import {IAsyncAction} from './IAsyncAction' + +export type GetResolvedAction = + MyTypes extends IAsyncAction & {status: 'resolved'} + ? MyTypes + : never diff --git a/packages/client/src/actions/IAction.ts b/packages/client/src/actions/IAction.ts index 1c2ce5f..5a0b3d2 100644 --- a/packages/client/src/actions/IAction.ts +++ b/packages/client/src/actions/IAction.ts @@ -1,3 +1,4 @@ -export interface IAction { +export interface IAction { + payload: T type: ActionType } diff --git a/packages/client/src/actions/IAsyncAction.ts b/packages/client/src/actions/IAsyncAction.ts index d64b03a..4fece8b 100644 --- a/packages/client/src/actions/IAsyncAction.ts +++ b/packages/client/src/actions/IAsyncAction.ts @@ -2,11 +2,9 @@ import {IPendingAction} from './IPendingAction' import {IResolvedAction} from './IResolvedAction' import {IRejectedAction} from './IRejectedAction' -export type IAsyncAction = - IPendingAction - | IResolvedAction - | IRejectedAction +export type IAsyncStatus = 'pending' | 'resolved' | 'rejected' + +export type IAsyncAction = + IPendingAction + | IResolvedAction + | IRejectedAction diff --git a/packages/client/src/actions/IPendingAction.ts b/packages/client/src/actions/IPendingAction.ts index 1712788..0bb5ade 100644 --- a/packages/client/src/actions/IPendingAction.ts +++ b/packages/client/src/actions/IPendingAction.ts @@ -1,6 +1,6 @@ import {IAction} from './IAction' export interface IPendingAction extends - IAction { - payload: Promise + IAction, ActionType> { + status: 'pending' } diff --git a/packages/client/src/actions/IRejectedAction.ts b/packages/client/src/actions/IRejectedAction.ts index 4088cdc..199e129 100644 --- a/packages/client/src/actions/IRejectedAction.ts +++ b/packages/client/src/actions/IRejectedAction.ts @@ -1,6 +1,6 @@ import {IAction} from './IAction' export interface IRejectedAction extends - IAction { - error: Error + IAction { + status: 'rejected' } diff --git a/packages/client/src/actions/IResolvedAction.ts b/packages/client/src/actions/IResolvedAction.ts index afb045e..d92c90f 100644 --- a/packages/client/src/actions/IResolvedAction.ts +++ b/packages/client/src/actions/IResolvedAction.ts @@ -1,6 +1,7 @@ import {IAction} from './IAction' export interface IResolvedAction extends - IAction { + IAction { payload: T + status: 'resolved' } diff --git a/packages/client/src/actions/PendingAction.ts b/packages/client/src/actions/PendingAction.ts new file mode 100644 index 0000000..34ff048 --- /dev/null +++ b/packages/client/src/actions/PendingAction.ts @@ -0,0 +1,9 @@ +import {IPendingAction} from './IPendingAction' + +export class PendingAction { + readonly status = 'pending' + constructor( + readonly payload: T, + readonly type: ActionType, + ) {} +} diff --git a/packages/client/src/actions/index.ts b/packages/client/src/actions/index.ts index e50eafd..9e3b68b 100644 --- a/packages/client/src/actions/index.ts +++ b/packages/client/src/actions/index.ts @@ -1,6 +1,9 @@ export * from './GetAction' +export * from './GetResolvedAction' +export * from './GetPendingAction' export * from './IAction' export * from './IAsyncAction' export * from './IPendingAction' export * from './IRejectedAction' export * from './IResolvedAction' +export * from './PendingAction' diff --git a/packages/client/src/crud/CRUDReducer.ts b/packages/client/src/crud/CRUDReducer.ts index a07a967..a378773 100644 --- a/packages/client/src/crud/CRUDReducer.ts +++ b/packages/client/src/crud/CRUDReducer.ts @@ -48,37 +48,31 @@ export class CRUDReducer { readonly resolvedExtension = '_RESOLVED', readonly rejectedExtension = '_REJECTED', ) { + + const defaultMethodStatus = this.getDefaultMethodStatus() this.defaultState = { ids: [], byId: {}, status: { - post: { - error: '', - isLoading: false, - }, - put: { - error: '', - isLoading: false, - }, - delete: { - error: '', - isLoading: false, - }, - get: { - error: '', - isLoading: false, - }, - getMany: { - error: '', - isLoading: false, - }, + post: defaultMethodStatus, + put: defaultMethodStatus, + delete: defaultMethodStatus, + get: defaultMethodStatus, + getMany: defaultMethodStatus, }, } this.actionTypes = this.getActionTypes() } + getDefaultMethodStatus(): ICRUDMethodStatus { + return { + error: '', + isLoading: false, + } + } + protected getPromiseActionNames(type: string) { return { pending: type + this.pendingExtension, diff --git a/packages/client/src/crumbs/CrumbsActions.ts b/packages/client/src/crumbs/CrumbsActions.ts index 5dd2e2d..798d520 100644 --- a/packages/client/src/crumbs/CrumbsActions.ts +++ b/packages/client/src/crumbs/CrumbsActions.ts @@ -1,4 +1,4 @@ -import {GetAction, IResolvedAction} from '../actions' +import {GetAction, IAction} from '../actions' export interface ICrumbLink { name: string @@ -11,7 +11,7 @@ export interface ICrumbs { } export type CrumbsActionType = - IResolvedAction + IAction type Action = GetAction diff --git a/packages/client/src/login/LoginActions.ts b/packages/client/src/login/LoginActions.ts index c1db71c..9e4e755 100644 --- a/packages/client/src/login/LoginActions.ts +++ b/packages/client/src/login/LoginActions.ts @@ -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 {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 = - IAsyncAction - | IAsyncAction - | IAsyncAction - | IResolvedAction<{redirectTo: string}, 'LOGIN_REDIRECT_SET'> + IAsyncAction + | IAsyncAction + | IAsyncAction + | IAction<{redirectTo: string}, 'LOGIN_REDIRECT_SET'> type Action = GetAction export class LoginActions { constructor(protected readonly http: IHTTPClient) {} - logIn = (credentials: ICredentials): Action<'LOGIN_PENDING'> => { - return { - payload: this.http.post('/auth/login', credentials), - type: 'LOGIN_PENDING', - } + logIn = (credentials: ICredentials) => { + return new PendingAction( + this.http.post('/auth/login', credentials), + 'LOGIN', + ) } - logOut = (): Action<'LOGIN_LOGOUT_PENDING'> => { - return { - payload: this.http.get('/auth/logout'), - type: 'LOGIN_LOGOUT_PENDING', - } + logOut = () => { + return new PendingAction( + this.http.get('/auth/logout'), + 'LOGIN_LOGOUT', + ) } - register = (profile: INewUser): Action<'LOGIN_REGISTER_PENDING'> => { - return { - payload: this.http.post('/auth/register', profile), - type: 'LOGIN_REGISTER_PENDING', - } + register = (profile: INewUser) => { + return new PendingAction( + this.http.post('/auth/register', profile), + 'LOGIN_REGISTER', + ) } setRedirectTo = (redirectTo: string): Action<'LOGIN_REDIRECT_SET'> => { return { payload: {redirectTo}, - type: LoginActionKeys.LOGIN_REDIRECT_SET, + type: 'LOGIN_REDIRECT_SET', } } } diff --git a/packages/client/src/login/LoginReducer.ts b/packages/client/src/login/LoginReducer.ts index 8711402..2518cd8 100644 --- a/packages/client/src/login/LoginReducer.ts +++ b/packages/client/src/login/LoginReducer.ts @@ -2,13 +2,15 @@ import {IUser} from '@rondo/common' import {LoginActionType} from './LoginActions' export interface ILoginState { - readonly error?: string + readonly error: string + readonly isLoading: boolean readonly user?: IUser readonly redirectTo: string } const defaultState: ILoginState = { - error: undefined, + error: '', + isLoading: false, user: undefined, redirectTo: '/', } @@ -18,19 +20,33 @@ export function Login( action: LoginActionType, ): ILoginState { switch (action.type) { - case 'LOGIN_RESOLVED': - 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} + // sync actions case 'LOGIN_REDIRECT_SET': return {...state, redirectTo: action.payload.redirectTo} 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 } } diff --git a/packages/client/src/middleware/PromiseMiddleware.test.ts b/packages/client/src/middleware/PromiseMiddleware.test.ts index 51ee5dd..97f94fd 100644 --- a/packages/client/src/middleware/PromiseMiddleware.test.ts +++ b/packages/client/src/middleware/PromiseMiddleware.test.ts @@ -4,13 +4,6 @@ import {getError} from '../test-utils' 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 beforeEach(() => { const middleware = new PromiseMiddleware() @@ -22,8 +15,11 @@ describe('PromiseMiddleware', () => { it('does nothing when payload is not a promise', () => { const action = {type: 'test'} - store.dispatch(action) - expect(store.getState().slice(1)).toEqual([action]) + const result = store.dispatch(action) + expect(result).toBe(action) + expect(store.getState().slice(1)).toEqual([{ + type: action.type, + }]) }) it('dispatches pending and resolved action', async () => { @@ -31,16 +27,21 @@ describe('PromiseMiddleware', () => { const type = 'TEST' const action = { payload: Promise.resolve(value), - type: `${type}_PENDING`, + type, } const result = store.dispatch(action) - expect(result).toBe(action) + expect(result).toEqual({ + ...action, + status: 'pending', + }) await result.payload expect(store.getState().slice(1)).toEqual([{ ...action, + status: 'pending', }, { payload: value, - type: type + '_RESOLVED', + status: 'resolved', + type, }]) }) @@ -49,17 +50,23 @@ describe('PromiseMiddleware', () => { const type = 'TEST' const action = { payload: Promise.reject(error), - type: `${type}_PENDING`, + type, } const result = store.dispatch(action) - expect(result).toBe(action) + expect(result).toEqual({ + ...action, + status: 'pending', + }) const err = await getError(result.payload) expect(err).toBe(error) expect(store.getState().slice(1)).toEqual([{ - ...action, + payload: action.payload, + status: 'pending', + type, }, { error, - type: `${type}_REJECTED`, + status: 'rejected', + type, }]) }) diff --git a/packages/client/src/middleware/PromiseMiddleware.ts b/packages/client/src/middleware/PromiseMiddleware.ts index 1c3e80e..7a49404 100644 --- a/packages/client/src/middleware/PromiseMiddleware.ts +++ b/packages/client/src/middleware/PromiseMiddleware.ts @@ -20,45 +20,34 @@ function isPromise(value: any): value is Promise { * const middleware = applyMiddleware(new PromiseMiddleware().handle) */ 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) => { const {payload, type} = action - // Propagate this action. Only attach listeners to the promise. - next(action) if (!isPromise(payload)) { - return + return next(action) } - - const strippedType = type.replace(this.regexp, '') + const pendingAction = { + ...action, + status: 'pending', + } + // Propagate this action. Only attach listeners to the promise. + next(pendingAction) payload .then(result => { store.dispatch({ payload: result, - type: strippedType + this.resolvedExtension, + status: 'resolved', + type, }) }) .catch(err => { store.dispatch({ error: err, - type: strippedType + this.rejectedExtension, + status: 'rejected', + type, }) }) - return action + return pendingAction } } diff --git a/packages/client/src/team/TeamActions.ts b/packages/client/src/team/TeamActions.ts index 6ebbd71..77d8eb4 100644 --- a/packages/client/src/team/TeamActions.ts +++ b/packages/client/src/team/TeamActions.ts @@ -1,74 +1,44 @@ import {IAPIDef} from '@rondo/common' -import {GetAction, IAsyncAction} from '../actions' +import {GetPendingAction, IAsyncAction, PendingAction} from '../actions' import {IHTTPClient} from '../http/IHTTPClient' import {ITeam, IUser, IUserInTeam} from '@rondo/common' export type TeamActionType = - IAsyncAction - | IAsyncAction - | IAsyncAction - | IAsyncAction<{id: number}, - 'TEAM_REMOVE_PENDING', - 'TEAM_REMOVE_RESOLVED', - 'TEAM_REMOVE_REJECTED'> - | IAsyncAction - | 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 + IAsyncAction + | IAsyncAction + | IAsyncAction + | IAsyncAction<{id: number}, 'TEAM_REMOVE'> + | IAsyncAction + | IAsyncAction<{userId: number, teamId: number}, 'TEAM_USER_REMOVE'> + | IAsyncAction<{teamId: number, usersInTeam: IUserInTeam[]}, 'TEAM_USERS'> + | IAsyncAction -type Action = GetAction +type Action = GetPendingAction export class TeamActions { constructor(protected readonly http: IHTTPClient) {} - fetchMyTeams = (): Action<'TEAMS_PENDING'> => { - return { - payload: this.http.get('/my/teams'), - type: 'TEAMS_PENDING', - } + fetchMyTeams = (): Action<'TEAMS'> => { + return new PendingAction(this.http.get('/my/teams'), 'TEAMS') } - createTeam = (team: {name: string}): Action<'TEAM_CREATE_PENDING'> => { - return { - payload: this.http.post('/teams', team), - type: 'TEAM_CREATE_PENDING', - } + 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_PENDING'> => { - return { - payload: this.http.put('/teams/:id', {name}, {id}), - type: 'TEAM_UPDATE_PENDING', - } + : Action<'TEAM_UPDATE'> => { + return new PendingAction( + this.http.put('/teams/:id', {name}, {id}), + 'TEAM_UPDATE', + ) } - removeTeam = ({id}: {id: number}): Action<'TEAM_REMOVE_PENDING'> => { - return { - payload: this.http.delete('/teams/:id', {}, {id}), - type: 'TEAM_REMOVE_PENDING', - } + removeTeam = ({id}: {id: number}): Action<'TEAM_REMOVE'> => { + return new PendingAction( + this.http.delete('/teams/:id', {}, {id}), + 'TEAM_REMOVE', + ) } addUser( @@ -77,14 +47,14 @@ export class TeamActions { teamId: number, roleId: number, }) - : Action<'TEAM_USER_ADD_PENDING'> { - return { - payload: this.http.post('/teams/:teamId/users/:userId', {}, { + : Action<'TEAM_USER_ADD'> { + return new PendingAction( + this.http.post('/teams/:teamId/users/:userId', {}, { userId, teamId, }), - type: 'TEAM_USER_ADD_PENDING', - } + 'TEAM_USER_ADD', + ) } removeUser = ( @@ -92,33 +62,31 @@ export class TeamActions { userId: number, teamId: number, }) - : Action<'TEAM_USER_REMOVE_PENDING'> => { - return { - payload: this.http.delete('/teams/:teamId/users/:userId', {}, { + : Action<'TEAM_USER_REMOVE'> => { + return new PendingAction( + this.http.delete('/teams/:teamId/users/:userId', {}, { userId, teamId, }), - type: 'TEAM_USER_REMOVE_PENDING', - } + 'TEAM_USER_REMOVE', + ) } fetchUsersInTeam = ({teamId}: {teamId: number}) - : Action<'TEAM_USERS_PENDING'> => { - return { - payload: this.http.get('/teams/:teamId/users', {}, { + : Action<'TEAM_USERS'> => { + return new PendingAction( + this.http.get('/teams/:teamId/users', {}, { teamId, }) .then(usersInTeam => ({teamId, usersInTeam})), - type: 'TEAM_USERS_PENDING', - } + 'TEAM_USERS', + ) } - findUserByEmail = (email: string): Action<'TEAM_USER_FIND_PENDING'> => { - return { - payload: this.http.get('/users/emails/:email', {}, { - email, - }), - type: 'TEAM_USER_FIND_PENDING', - } + findUserByEmail = (email: string): Action<'TEAM_USER_FIND'> => { + return new PendingAction( + this.http.get('/users/emails/:email', {}, {email}), + 'TEAM_USER_FIND', + ) } } diff --git a/packages/client/src/team/TeamReducer.ts b/packages/client/src/team/TeamReducer.ts index 8e840a4..15e1b95 100644 --- a/packages/client/src/team/TeamReducer.ts +++ b/packages/client/src/team/TeamReducer.ts @@ -2,7 +2,7 @@ import { ITeam, IUserInTeam, ReadonlyRecord, indexBy, without, } from '@rondo/common' import {TeamActionType} from './TeamActions' -import {GetAction} from '../actions' +import {GetResolvedAction} from '../actions' export interface ITeamState { readonly error: string @@ -26,7 +26,7 @@ const defaultState: ITeamState = { function removeUser( state: ITeamState, - action: GetAction, + action: GetResolvedAction, ) { const {payload} = action @@ -53,75 +53,78 @@ function getUserKey(userInTeam: {userId: number, teamId: number}) { } export function Team(state = defaultState, action: TeamActionType): ITeamState { - switch (action.type) { - case 'TEAMS_RESOLVED': + switch (action.status) { + case 'pending': + return state + case 'rejected': return { ...state, - teamIds: action.payload.map(team => team.id), - teamsById: indexBy(action.payload, 'id'), + error: action.payload.message, } - case 'TEAM_CREATE_RESOLVED': - case 'TEAM_UPDATE_RESOLVED': - 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_RESOLVED': - return { - ...state, - userKeysByTeamId: { - ...state.userKeysByTeamId, - [action.payload.teamId]: [ - ...state.userKeysByTeamId[action.payload.teamId], - getUserKey(action.payload), - ], - }, - usersByKey: { - ...state.usersByKey, - [getUserKey(action.payload)]: action.payload, - }, - } - case 'TEAM_USER_REMOVE_RESOLVED': - return removeUser(state, action) - case 'TEAM_USERS_RESOLVED': - const usersByKey = action.payload.usersInTeam - .reduce((obj, userInTeam) => { - obj[getUserKey(userInTeam)] = userInTeam - return obj - }, {} as Record) + 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), + ], + }, + usersByKey: { + ...state.usersByKey, + [getUserKey(action.payload)]: action.payload, + }, + } + case 'TEAM_USER_REMOVE': + return removeUser(state, action) + case 'TEAM_USERS': + const usersByKey = action.payload.usersInTeam + .reduce((obj, userInTeam) => { + obj[getUserKey(userInTeam)] = userInTeam + return obj + }, {} as Record) - return { - ...state, - userKeysByTeamId: { - ...state.userKeysByTeamId, - [action.payload.teamId]: action.payload.usersInTeam - .map(ut => getUserKey(ut)), - }, - usersByKey: { - ...state.usersByKey, - ...usersByKey, - }, - } - case 'TEAM_REMOVE_RESOLVED': - return { - ...state, - teamIds: state.teamIds.filter(id => id !== action.payload.id), - teamsById: without(state.teamsById, action.payload.id), - } - 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, + return { + ...state, + userKeysByTeamId: { + ...state.userKeysByTeamId, + [action.payload.teamId]: action.payload.usersInTeam + .map(ut => getUserKey(ut)), + }, + usersByKey: { + ...state.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