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'
|
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
|
||||||
|
|||||||
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
|
type: ActionType
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
|
|||||||
@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
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 './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'
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user