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