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:
parent
61e7e8db4f
commit
8b6f90235e
@ -1,6 +1,6 @@
|
||||
import {IAction} from './IAction'
|
||||
|
||||
export type GetAction<MyTypes, T extends string> =
|
||||
MyTypes extends IAction<T>
|
||||
MyTypes extends IAction<infer U, T>
|
||||
? MyTypes
|
||||
: never
|
||||
|
||||
6
packages/client/src/actions/GetPendingAction.ts
Normal file
6
packages/client/src/actions/GetPendingAction.ts
Normal 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
|
||||
6
packages/client/src/actions/GetResolvedAction.ts
Normal file
6
packages/client/src/actions/GetResolvedAction.ts
Normal 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
|
||||
@ -1,3 +1,4 @@
|
||||
export interface IAction<ActionType extends string> {
|
||||
export interface IAction<T, ActionType extends string> {
|
||||
payload: T
|
||||
type: ActionType
|
||||
}
|
||||
|
||||
@ -2,11 +2,9 @@ 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>
|
||||
export type IAsyncStatus = 'pending' | 'resolved' | 'rejected'
|
||||
|
||||
export type IAsyncAction<T, ActionType extends string> =
|
||||
IPendingAction<T, ActionType>
|
||||
| IResolvedAction<T, ActionType>
|
||||
| IRejectedAction<ActionType>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {IAction} from './IAction'
|
||||
|
||||
export interface IPendingAction<T, ActionType extends string> extends
|
||||
IAction<ActionType> {
|
||||
payload: Promise<T>
|
||||
IAction<Promise<T>, ActionType> {
|
||||
status: 'pending'
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {IAction} from './IAction'
|
||||
|
||||
export interface IRejectedAction<ActionType extends string> extends
|
||||
IAction<ActionType> {
|
||||
error: Error
|
||||
IAction<Error, ActionType> {
|
||||
status: 'rejected'
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {IAction} from './IAction'
|
||||
|
||||
export interface IResolvedAction<T, ActionType extends string> extends
|
||||
IAction<ActionType> {
|
||||
IAction<T, ActionType> {
|
||||
payload: T
|
||||
status: 'resolved'
|
||||
}
|
||||
|
||||
9
packages/client/src/actions/PendingAction.ts
Normal file
9
packages/client/src/actions/PendingAction.ts
Normal 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,
|
||||
) {}
|
||||
}
|
||||
@ -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'
|
||||
|
||||
@ -48,37 +48,31 @@ export class CRUDReducer<T extends ICRUDIdable> {
|
||||
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,
|
||||
|
||||
@ -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<ICrumbs, 'BREADCRUMBS_SET'>
|
||||
IAction<ICrumbs, 'BREADCRUMBS_SET'>
|
||||
|
||||
type Action<T extends string> = GetAction<CrumbsActionType, T>
|
||||
|
||||
|
||||
@ -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<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'>
|
||||
IAsyncAction<IUser, 'LOGIN'>
|
||||
| IAsyncAction<unknown, 'LOGIN_LOGOUT'>
|
||||
| IAsyncAction<IUser, 'LOGIN_REGISTER'>
|
||||
| IAction<{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): 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',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}])
|
||||
})
|
||||
|
||||
|
||||
@ -20,45 +20,34 @@ 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 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<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<{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'>
|
||||
IAsyncAction<ITeam[], 'TEAMS'>
|
||||
| IAsyncAction<ITeam, 'TEAM_CREATE'>
|
||||
| IAsyncAction<ITeam, 'TEAM_UPDATE'>
|
||||
| IAsyncAction<{id: number}, 'TEAM_REMOVE'>
|
||||
| IAsyncAction<IUserInTeam, 'TEAM_USER_ADD'>
|
||||
| IAsyncAction<{userId: number, teamId: number}, 'TEAM_USER_REMOVE'>
|
||||
| IAsyncAction<{teamId: number, usersInTeam: IUserInTeam[]}, 'TEAM_USERS'>
|
||||
| IAsyncAction<IUser | undefined, 'TEAM_USER_FIND'>
|
||||
|
||||
type Action<T extends string> = GetAction<TeamActionType, T>
|
||||
type Action<T extends string> = GetPendingAction<TeamActionType, T>
|
||||
|
||||
export class TeamActions {
|
||||
constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
|
||||
|
||||
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',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<TeamActionType, 'TEAM_USER_REMOVE_RESOLVED'>,
|
||||
action: GetResolvedAction<TeamActionType, 'TEAM_USER_REMOVE'>,
|
||||
) {
|
||||
|
||||
const {payload} = action
|
||||
@ -53,15 +53,24 @@ function getUserKey(userInTeam: {userId: number, teamId: number}) {
|
||||
}
|
||||
|
||||
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) {
|
||||
case 'TEAMS_RESOLVED':
|
||||
case 'TEAMS':
|
||||
return {
|
||||
...state,
|
||||
teamIds: action.payload.map(team => team.id),
|
||||
teamsById: indexBy(action.payload, 'id'),
|
||||
}
|
||||
case 'TEAM_CREATE_RESOLVED':
|
||||
case 'TEAM_UPDATE_RESOLVED':
|
||||
case 'TEAM_CREATE':
|
||||
case 'TEAM_UPDATE':
|
||||
return {
|
||||
...state,
|
||||
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,
|
||||
},
|
||||
}
|
||||
case 'TEAM_USER_ADD_RESOLVED':
|
||||
case 'TEAM_USER_ADD':
|
||||
return {
|
||||
...state,
|
||||
userKeysByTeamId: {
|
||||
@ -87,9 +96,9 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
|
||||
[getUserKey(action.payload)]: action.payload,
|
||||
},
|
||||
}
|
||||
case 'TEAM_USER_REMOVE_RESOLVED':
|
||||
case 'TEAM_USER_REMOVE':
|
||||
return removeUser(state, action)
|
||||
case 'TEAM_USERS_RESOLVED':
|
||||
case 'TEAM_USERS':
|
||||
const usersByKey = action.payload.usersInTeam
|
||||
.reduce((obj, userInTeam) => {
|
||||
obj[getUserKey(userInTeam)] = userInTeam
|
||||
@ -108,20 +117,14 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
|
||||
...usersByKey,
|
||||
},
|
||||
}
|
||||
case 'TEAM_REMOVE_RESOLVED':
|
||||
case 'TEAM_REMOVE':
|
||||
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,
|
||||
default:
|
||||
return state
|
||||
}
|
||||
default:
|
||||
return state
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user