Refactor action definitions to type less
This commit is contained in:
parent
f2e44f477c
commit
8f8c3b6c9c
@ -1,28 +0,0 @@
|
|||||||
// Get the type of promise
|
|
||||||
// https://www.typescriptlang.org/docs/handbook/advanced-types.html
|
|
||||||
// section: Type inference in conditional types
|
|
||||||
type Unpacked<T> = T extends Promise<infer U> ? U : T
|
|
||||||
import {IAction} from './IAction'
|
|
||||||
import {IErrorAction} from './IErrorAction'
|
|
||||||
|
|
||||||
// Also from TypeScript handbook:
|
|
||||||
// https://www.typescriptlang.org/docs/handbook/advanced-types.html
|
|
||||||
type FunctionProperties<T> = {
|
|
||||||
[K in keyof T]:
|
|
||||||
T[K] extends (...args: any[]) => IAction<any, string>
|
|
||||||
? {
|
|
||||||
payload: Unpacked<ReturnType<T[K]>['payload']>,
|
|
||||||
type: Unpacked<ReturnType<T[K]>['type']>,
|
|
||||||
}
|
|
||||||
: T[K] extends (...args: any[]) => IErrorAction<string>
|
|
||||||
? {
|
|
||||||
error: Error,
|
|
||||||
type: Unpacked<ReturnType<T[K]>['type']>,
|
|
||||||
}
|
|
||||||
: never
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/48305190/
|
|
||||||
// Is there an automatic way to create a discriminated union for all interfaces
|
|
||||||
// in a namespace?
|
|
||||||
export type ActionTypes<T> = FunctionProperties<T>[keyof FunctionProperties<T>]
|
|
||||||
6
packages/client/src/actions/GetAction.ts
Normal file
6
packages/client/src/actions/GetAction.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import {IAction} from './IAction'
|
||||||
|
|
||||||
|
export type GetAction<MyTypes, T extends string> =
|
||||||
|
MyTypes extends IAction<T>
|
||||||
|
? MyTypes
|
||||||
|
: never
|
||||||
@ -1,7 +1,3 @@
|
|||||||
// Maybe this won't be necessary after this is merged:
|
export interface IAction<ActionType extends string> {
|
||||||
// https://github.com/Microsoft/TypeScript/pull/29478
|
|
||||||
|
|
||||||
export interface IAction<T = any, ActionType extends string = string> {
|
|
||||||
payload: Promise<T> | T,
|
|
||||||
type: ActionType
|
type: ActionType
|
||||||
}
|
}
|
||||||
|
|||||||
12
packages/client/src/actions/IAsyncAction.ts
Normal file
12
packages/client/src/actions/IAsyncAction.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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>
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export interface IErrorAction<ActionType extends string> {
|
|
||||||
error: Error,
|
|
||||||
type: ActionType
|
|
||||||
}
|
|
||||||
6
packages/client/src/actions/IPendingAction.ts
Normal file
6
packages/client/src/actions/IPendingAction.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import {IAction} from './IAction'
|
||||||
|
|
||||||
|
export interface IPendingAction<T, ActionType extends string> extends
|
||||||
|
IAction<ActionType> {
|
||||||
|
payload: Promise<T>
|
||||||
|
}
|
||||||
6
packages/client/src/actions/IRejectedAction.ts
Normal file
6
packages/client/src/actions/IRejectedAction.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import {IAction} from './IAction'
|
||||||
|
|
||||||
|
export interface IRejectedAction<ActionType extends string> extends
|
||||||
|
IAction<ActionType> {
|
||||||
|
error: Error
|
||||||
|
}
|
||||||
6
packages/client/src/actions/IResolvedAction.ts
Normal file
6
packages/client/src/actions/IResolvedAction.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import {IAction} from './IAction'
|
||||||
|
|
||||||
|
export interface IResolvedAction<T, ActionType extends string> extends
|
||||||
|
IAction<ActionType> {
|
||||||
|
payload: T
|
||||||
|
}
|
||||||
@ -1,13 +0,0 @@
|
|||||||
// Get the type of promise
|
|
||||||
// https://www.typescriptlang.org/docs/handbook/advanced-types.html
|
|
||||||
// section: Type inference in conditional types
|
|
||||||
type Unpacked<T> = T extends Promise<infer U> ? U : T
|
|
||||||
|
|
||||||
type FunctionProperties<T> = {
|
|
||||||
[K in keyof T]: T[K] extends (...args: any[]) => any ? {
|
|
||||||
payload: Unpacked<ReturnType<T[K]>['payload']>,
|
|
||||||
type: Unpacked<ReturnType<T[K]>['type']>,
|
|
||||||
}: never
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UnionType<T> = FunctionProperties<T>[keyof FunctionProperties<T>]
|
|
||||||
@ -1,4 +1,6 @@
|
|||||||
export * from './ActionTypes'
|
export * from './GetAction'
|
||||||
export * from './IAction'
|
export * from './IAction'
|
||||||
export * from './IErrorAction'
|
export * from './IAsyncAction'
|
||||||
export * from './UnionType'
|
export * from './IPendingAction'
|
||||||
|
export * from './IRejectedAction'
|
||||||
|
export * from './IResolvedAction'
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {IAction, IErrorAction, ActionTypes} from '../actions'
|
import {GetAction, IAsyncAction, IResolvedAction} 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'
|
||||||
|
|
||||||
@ -18,56 +18,51 @@ export enum LoginActionKeys {
|
|||||||
LOGIN_REDIRECT_SET = 'LOGIN_REDIRECT_SET',
|
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'>
|
||||||
|
|
||||||
|
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)
|
logIn = (credentials: ICredentials): Action<'LOGIN_PENDING'> => {
|
||||||
: IAction<IUser, LoginActionKeys.LOGIN> => {
|
|
||||||
return {
|
return {
|
||||||
payload: this.http.post('/auth/login', credentials),
|
payload: this.http.post('/auth/login', credentials),
|
||||||
type: LoginActionKeys.LOGIN,
|
type: 'LOGIN_PENDING',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logInError = (error: Error)
|
logOut = (): Action<'LOGIN_LOGOUT_PENDING'> => {
|
||||||
: IErrorAction<LoginActionKeys.LOGIN_REJECTED> => {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
type: LoginActionKeys.LOGIN_REJECTED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logOut = (): IAction<unknown, LoginActionKeys.LOGIN_LOG_OUT> => {
|
|
||||||
return {
|
return {
|
||||||
payload: this.http.get('/auth/logout'),
|
payload: this.http.get('/auth/logout'),
|
||||||
type: LoginActionKeys.LOGIN_LOG_OUT,
|
type: 'LOGIN_LOGOUT_PENDING',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
register = (profile: INewUser):
|
register = (profile: INewUser): Action<'LOGIN_REGISTER_PENDING'> => {
|
||||||
IAction<IUser, LoginActionKeys.LOGIN_REGISTER> => {
|
|
||||||
return {
|
return {
|
||||||
payload: this.http.post('/auth/register', profile),
|
payload: this.http.post('/auth/register', profile),
|
||||||
type: LoginActionKeys.LOGIN_REGISTER,
|
type: 'LOGIN_REGISTER_PENDING',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerError = (error: Error)
|
setRedirectTo = (redirectTo: string): Action<'LOGIN_REDIRECT_SET'> => {
|
||||||
: IErrorAction<LoginActionKeys.LOGIN_REGISTER_REJECTED> => {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
type: LoginActionKeys.LOGIN_REGISTER_REJECTED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setRedirectTo = (redirectTo: string)
|
|
||||||
: IAction<{redirectTo: string}, LoginActionKeys.LOGIN_REDIRECT_SET> => {
|
|
||||||
return {
|
return {
|
||||||
payload: {redirectTo},
|
payload: {redirectTo},
|
||||||
type: LoginActionKeys.LOGIN_REDIRECT_SET,
|
type: LoginActionKeys.LOGIN_REDIRECT_SET,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This makes it very easy to write reducer code.
|
|
||||||
export type LoginActionType = ActionTypes<LoginActions>
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {IUser} from '@rondo/common'
|
import {IUser} from '@rondo/common'
|
||||||
import {LoginActionKeys, LoginActionType} from './LoginActions'
|
import {LoginActionType} from './LoginActions'
|
||||||
|
|
||||||
export interface ILoginState {
|
export interface ILoginState {
|
||||||
readonly error?: string
|
readonly error?: string
|
||||||
@ -18,17 +18,17 @@ export function Login(
|
|||||||
action: LoginActionType,
|
action: LoginActionType,
|
||||||
): ILoginState {
|
): ILoginState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case LoginActionKeys.LOGIN:
|
case 'LOGIN_RESOLVED':
|
||||||
return {...state, user: action.payload, error: ''}
|
return {...state, user: action.payload, error: ''}
|
||||||
case LoginActionKeys.LOGIN_LOG_OUT:
|
case 'LOGIN_LOGOUT_RESOLVED':
|
||||||
return {...state, user: undefined}
|
return {...state, user: undefined}
|
||||||
case LoginActionKeys.LOGIN_REJECTED:
|
case 'LOGIN_REJECTED':
|
||||||
return {...state, error: action.error.message}
|
return {...state, error: action.error.message}
|
||||||
case LoginActionKeys.LOGIN_REGISTER:
|
case 'LOGIN_REGISTER_RESOLVED':
|
||||||
return {...state, user: action.payload, error: ''}
|
return {...state, user: action.payload, error: ''}
|
||||||
case LoginActionKeys.LOGIN_REGISTER_REJECTED:
|
case 'LOGIN_REGISTER_REJECTED':
|
||||||
return {...state, error: action.error.message}
|
return {...state, error: action.error.message}
|
||||||
case LoginActionKeys.LOGIN_REDIRECT_SET:
|
case 'LOGIN_REDIRECT_SET':
|
||||||
return {...state, redirectTo: action.payload.redirectTo}
|
return {...state, redirectTo: action.payload.redirectTo}
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export interface IComponentProps<Data> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IFormHOCProps<Data> {
|
export interface IFormHOCProps<Data> {
|
||||||
onSubmit: (props: Data) => IAction<any, any>
|
onSubmit: (props: Data) => IAction<any>
|
||||||
// TODO figure out what would happen if the underlying child component
|
// TODO figure out what would happen if the underlying child component
|
||||||
// would have the same required property as the HOC, like onSuccess?
|
// would have the same required property as the HOC, like onSuccess?
|
||||||
onSuccess?: () => void
|
onSuccess?: () => void
|
||||||
|
|||||||
@ -26,21 +26,21 @@ describe('PromiseMiddleware', () => {
|
|||||||
expect(store.getState().slice(1)).toEqual([action])
|
expect(store.getState().slice(1)).toEqual([action])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('dispatches pending and fulfilled action', async () => {
|
it('dispatches pending and resolved action', async () => {
|
||||||
const value = 123
|
const value = 123
|
||||||
const type = 'TEST'
|
const type = 'TEST'
|
||||||
const action = {
|
const action = {
|
||||||
payload: Promise.resolve(value),
|
payload: Promise.resolve(value),
|
||||||
type,
|
type: `${type}_PENDING`,
|
||||||
}
|
}
|
||||||
const result = store.dispatch(action)
|
const result = store.dispatch(action)
|
||||||
expect(result).toBe(action)
|
expect(result).toBe(action)
|
||||||
await result.payload
|
await result.payload
|
||||||
expect(store.getState().slice(1)).toEqual([{
|
expect(store.getState().slice(1)).toEqual([{
|
||||||
type: `${type}_PENDING`,
|
...action,
|
||||||
}, {
|
}, {
|
||||||
payload: value,
|
payload: value,
|
||||||
type,
|
type: type + '_RESOLVED',
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -49,14 +49,14 @@ describe('PromiseMiddleware', () => {
|
|||||||
const type = 'TEST'
|
const type = 'TEST'
|
||||||
const action = {
|
const action = {
|
||||||
payload: Promise.reject(error),
|
payload: Promise.reject(error),
|
||||||
type,
|
type: `${type}_PENDING`,
|
||||||
}
|
}
|
||||||
const result = store.dispatch(action)
|
const result = store.dispatch(action)
|
||||||
expect(result).toBe(action)
|
expect(result).toBe(action)
|
||||||
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([{
|
||||||
type: `${type}_PENDING`,
|
...action,
|
||||||
}, {
|
}, {
|
||||||
error,
|
error,
|
||||||
type: `${type}_REJECTED`,
|
type: `${type}_REJECTED`,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ function isPromise(value: any): value is Promise<any> {
|
|||||||
*
|
*
|
||||||
* If `action.payload` is a `Promise`, it will be handled by this class. It
|
* If `action.payload` is a `Promise`, it will be handled by this class. It
|
||||||
* differs from other promise middlewares for redux because by default it does
|
* differs from other promise middlewares for redux because by default it does
|
||||||
* not add an extension to action dispatched after a promise is fulfilled. This
|
* not add an extension to action dispatched after a promise is resolved. This
|
||||||
* makes it easier to infer types from the API endpoints so they can be used in
|
* makes it easier to infer types from the API endpoints so they can be used in
|
||||||
* both Action creators and Reducers.
|
* both Action creators and Reducers.
|
||||||
*
|
*
|
||||||
@ -20,36 +20,42 @@ 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(
|
constructor(
|
||||||
readonly pendingExtension = '_PENDING',
|
readonly pendingExtension = '_PENDING',
|
||||||
readonly fulfilledExtension = '',
|
readonly resolvedExtension = '_RESOLVED',
|
||||||
readonly rejectedExtension = '_REJECTED',
|
readonly rejectedExtension = '_REJECTED',
|
||||||
) {
|
) {
|
||||||
assert(
|
assert(
|
||||||
this.pendingExtension !== this.fulfilledExtension &&
|
this.pendingExtension !== this.resolvedExtension &&
|
||||||
this.fulfilledExtension !== this.rejectedExtension &&
|
this.resolvedExtension !== this.rejectedExtension &&
|
||||||
this.pendingExtension !== this.rejectedExtension,
|
this.pendingExtension !== this.rejectedExtension,
|
||||||
'Pending, fulfilled and rejected extensions must be unique')
|
'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
|
||||||
if (!isPromise(payload)) {
|
// Propagate this action. Only attach listeners to the promise.
|
||||||
next(action)
|
next(action)
|
||||||
|
if (!isPromise(payload)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
store.dispatch({type: type + this.pendingExtension})
|
|
||||||
|
const strippedType = type.replace(this.regexp, '')
|
||||||
|
|
||||||
payload
|
payload
|
||||||
.then(result => {
|
.then(result => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
payload: result,
|
payload: result,
|
||||||
type,
|
type: strippedType + this.resolvedExtension,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
error: err,
|
error: err,
|
||||||
type: type + this.rejectedExtension,
|
type: strippedType + this.rejectedExtension,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,105 +1,73 @@
|
|||||||
import {IAPIDef} from '@rondo/common'
|
import {IAPIDef} from '@rondo/common'
|
||||||
import {IAction, IErrorAction, ActionTypes} from '../actions'
|
import {GetAction, IAsyncAction} 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 enum TeamActionKeys {
|
export type TeamActionType =
|
||||||
TEAMS = 'TEAMS',
|
IAsyncAction<ITeam[],
|
||||||
TEAMS_PENDING = 'TEAMS_PENDING',
|
'TEAMS_PENDING',
|
||||||
TEAMS_REJECTED = 'TEAMS_REJECTED',
|
'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<{},
|
||||||
|
'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'>
|
||||||
|
|
||||||
TEAM_CREATE = 'TEAM_CREATE',
|
type Action<T extends string> = GetAction<TeamActionType, T>
|
||||||
TEAM_CREATE_PENDING = 'TEAM_CREATE_PENDING',
|
|
||||||
TEAM_CREATE_REJECTED = 'TEAM_CREATE_REJECTED',
|
|
||||||
|
|
||||||
TEAM_UPDATE = 'TEAM_UPDATE',
|
|
||||||
TEAM_UPDATE_PENDING = 'TEAM_UPDATE_PENDING',
|
|
||||||
TEAM_UPDATE_REJECTED = 'TEAM_UPDATE_REJECTED',
|
|
||||||
|
|
||||||
TEAM_REMOVE = 'TEAM_REMOVE',
|
|
||||||
TEAM_REMOVE_PENDING = 'TEAM_REMOVE_PENDING',
|
|
||||||
TEAM_REMOVE_REJECTED = 'TEAM_REMOVE_REJECTED',
|
|
||||||
|
|
||||||
TEAM_USER_ADD = 'TEAM_USER_ADD',
|
|
||||||
TEAM_USER_ADD_PENDING = 'TEAM_USER_ADD_PENDING',
|
|
||||||
TEAM_USER_ADD_REJECTED = 'TEAM_USER_ADD_REJECTED',
|
|
||||||
|
|
||||||
TEAM_USER_REMOVE = 'TEAM_USER_REMOVE',
|
|
||||||
TEAM_USER_REMOVE_PENDING = 'TEAM_USER_REMOVE_PENDING',
|
|
||||||
TEAM_USER_REMOVE_REJECTED = 'TEAM_USER_REMOVE_REJECTED',
|
|
||||||
|
|
||||||
TEAM_USERS = 'TEAM_USERS',
|
|
||||||
TEAM_USERS_PENDING = 'TEAM_USERS_PENDING',
|
|
||||||
TEAM_USERS_REJECTED = 'TEAM_USERS_REJECTED',
|
|
||||||
|
|
||||||
TEAM_USER_FIND = 'TEAM_USER_FIND',
|
|
||||||
TEAM_USER_FIND_PENDING = 'TEAM_USER_FIND_PENDING',
|
|
||||||
TEAM_USER_FIND_REJECTED = 'TEAM_USER_FIND_REJECTED',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TeamActions {
|
export class TeamActions {
|
||||||
constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
|
constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
|
||||||
|
|
||||||
fetchMyTeams = (): IAction<ITeam[], TeamActionKeys.TEAMS> => {
|
fetchMyTeams = (): Action<'TEAMS_PENDING'> => {
|
||||||
return {
|
return {
|
||||||
payload: this.http.get('/my/teams'),
|
payload: this.http.get('/my/teams'),
|
||||||
type: TeamActionKeys.TEAMS,
|
type: 'TEAMS_PENDING',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchMyTeamsError = (error: Error)
|
createTeam = (team: {name: string}): Action<'TEAM_CREATE_PENDING'> => {
|
||||||
: IErrorAction<TeamActionKeys.TEAMS_REJECTED> => {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
type: TeamActionKeys.TEAMS_REJECTED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createTeam = (team: {name: string})
|
|
||||||
: IAction<ITeam, TeamActionKeys.TEAM_CREATE> => {
|
|
||||||
return {
|
return {
|
||||||
payload: this.http.post('/teams', team),
|
payload: this.http.post('/teams', team),
|
||||||
type: TeamActionKeys.TEAM_CREATE,
|
type: 'TEAM_CREATE_PENDING',
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createTeamError = (error: Error)
|
|
||||||
: IErrorAction<TeamActionKeys.TEAM_CREATE_REJECTED> => {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
type: TeamActionKeys.TEAM_CREATE_REJECTED,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTeam = ({id, name}: {id: number, name: string})
|
updateTeam = ({id, name}: {id: number, name: string})
|
||||||
: IAction<ITeam, TeamActionKeys.TEAM_UPDATE> => {
|
: Action<'TEAM_UPDATE_PENDING'> => {
|
||||||
return {
|
return {
|
||||||
payload: this.http.put('/teams/:id', {name}, {id}),
|
payload: this.http.put('/teams/:id', {name}, {id}),
|
||||||
type: TeamActionKeys.TEAM_UPDATE,
|
type: 'TEAM_UPDATE_PENDING',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTeamError = (error: Error)
|
removeTeam = ({id}: {id: number}): Action<'TEAM_REMOVE_PENDING'> => {
|
||||||
: IErrorAction<TeamActionKeys.TEAM_UPDATE_REJECTED> => {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
type: TeamActionKeys.TEAM_UPDATE_REJECTED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTeam = ({id}: {id: number})
|
|
||||||
: IAction<{}, TeamActionKeys.TEAM_REMOVE> => {
|
|
||||||
return {
|
return {
|
||||||
payload: this.http.delete('/teams/:id', {}, {id}),
|
payload: this.http.delete('/teams/:id', {}, {id}),
|
||||||
type: TeamActionKeys.TEAM_REMOVE,
|
type: 'TEAM_REMOVE_PENDING',
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTeamError = (error: Error)
|
|
||||||
: IErrorAction<TeamActionKeys.TEAM_REMOVE_REJECTED> => {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
type: TeamActionKeys.TEAM_REMOVE_REJECTED,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,21 +77,13 @@ export class TeamActions {
|
|||||||
teamId: number,
|
teamId: number,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
})
|
})
|
||||||
: IAction<IUserInTeam, TeamActionKeys.TEAM_USER_ADD> {
|
: Action<'TEAM_USER_ADD_PENDING'> {
|
||||||
return {
|
return {
|
||||||
payload: this.http.post('/teams/:teamId/users/:userId', {}, {
|
payload: this.http.post('/teams/:teamId/users/:userId', {}, {
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
}),
|
}),
|
||||||
type: TeamActionKeys.TEAM_USER_ADD,
|
type: 'TEAM_USER_ADD_PENDING',
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addUserError = (error: Error)
|
|
||||||
: IErrorAction<TeamActionKeys.TEAM_USER_ADD_REJECTED> => {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
type: TeamActionKeys.TEAM_USER_ADD_REJECTED,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,66 +92,33 @@ export class TeamActions {
|
|||||||
userId: number,
|
userId: number,
|
||||||
teamId: number,
|
teamId: number,
|
||||||
})
|
})
|
||||||
: IAction<{
|
: Action<'TEAM_USER_REMOVE_PENDING'> => {
|
||||||
userId: number,
|
|
||||||
teamId: number,
|
|
||||||
}, TeamActionKeys.TEAM_USER_REMOVE> => {
|
|
||||||
return {
|
return {
|
||||||
payload: this.http.delete('/teams/:teamId/users/:userId', {}, {
|
payload: this.http.delete('/teams/:teamId/users/:userId', {}, {
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
}),
|
}),
|
||||||
type: TeamActionKeys.TEAM_USER_REMOVE,
|
type: 'TEAM_USER_REMOVE_PENDING',
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeUserError = (error: Error)
|
|
||||||
: IErrorAction<TeamActionKeys.TEAM_USER_REMOVE_REJECTED> => {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
type: TeamActionKeys.TEAM_USER_REMOVE_REJECTED,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchUsersInTeam = ({teamId}: {teamId: number})
|
fetchUsersInTeam = ({teamId}: {teamId: number})
|
||||||
: IAction<{
|
: Action<'TEAM_USERS_PENDING'> => {
|
||||||
teamId: number,
|
|
||||||
usersInTeam: IUserInTeam[]
|
|
||||||
}, TeamActionKeys.TEAM_USERS> => {
|
|
||||||
return {
|
return {
|
||||||
payload: this.http.get('/teams/:teamId/users', {
|
payload: this.http.get('/teams/:teamId/users', {
|
||||||
teamId,
|
teamId,
|
||||||
})
|
})
|
||||||
.then(usersInTeam => ({teamId, usersInTeam})),
|
.then(usersInTeam => ({teamId, usersInTeam})),
|
||||||
type: TeamActionKeys.TEAM_USERS,
|
type: 'TEAM_USERS_PENDING',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchUsersInTeamError = (error: Error)
|
findUserByEmail = (email: string): Action<'TEAM_USER_FIND_PENDING'> => {
|
||||||
: IErrorAction<TeamActionKeys.TEAM_USERS_REJECTED> => {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
type: TeamActionKeys.TEAM_USERS_REJECTED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
findUserByEmail = (email: string)
|
|
||||||
: IAction<IUser | undefined, TeamActionKeys.TEAM_USER_FIND> => {
|
|
||||||
return {
|
return {
|
||||||
payload: this.http.get('/users/emails/:email', {
|
payload: this.http.get('/users/emails/:email', {
|
||||||
email,
|
email,
|
||||||
}),
|
}),
|
||||||
type: TeamActionKeys.TEAM_USER_FIND,
|
type: 'TEAM_USER_FIND_PENDING',
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
findUserByEmailError = (error: Error)
|
|
||||||
: IErrorAction<TeamActionKeys.TEAM_USER_FIND_REJECTED> => {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
type: TeamActionKeys.TEAM_USER_FIND_REJECTED,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TeamActionType = ActionTypes<TeamActions>
|
|
||||||
|
|||||||
@ -1,26 +1,26 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {IAction} from '../actions'
|
|
||||||
import {ITeam, ReadonlyRecord} from '@rondo/common'
|
import {ITeam, ReadonlyRecord} from '@rondo/common'
|
||||||
|
import {TeamActions} from './TeamActions'
|
||||||
|
|
||||||
export interface ITeamListProps {
|
export interface ITeamListProps {
|
||||||
teamsById: ReadonlyRecord<number, ITeam>,
|
teamsById: ReadonlyRecord<number, ITeam>,
|
||||||
teamIds: ReadonlyArray<number>,
|
teamIds: ReadonlyArray<number>,
|
||||||
onAddTeam: (params: {name: string}) => IAction
|
onAddTeam: TeamActions['createTeam']
|
||||||
onRemoveTeam: (params: {id: number}) => IAction
|
onRemoveTeam: TeamActions['removeTeam']
|
||||||
onUpdateTeam: (params: {id: number, name: string}) => IAction
|
onUpdateTeam: TeamActions['updateTeam']
|
||||||
editTeamId: number
|
editTeamId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITeamProps {
|
export interface ITeamProps {
|
||||||
team: ITeam
|
team: ITeam
|
||||||
editTeamId: number // TODO handle edits via react-router params
|
editTeamId: number // TODO handle edits via react-router params
|
||||||
onRemoveTeam: (params: {id: number}) => IAction
|
onRemoveTeam: TeamActions['removeTeam']
|
||||||
onUpdateTeam: (params: {id: number, name: string}) => IAction
|
onUpdateTeam: TeamActions['updateTeam']
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAddTeamProps {
|
export interface IAddTeamProps {
|
||||||
onAddTeam: (params: {name: string}) => IAction
|
onAddTeam: TeamActions['createTeam']
|
||||||
onUpdateTeam: (params: {id: number, name: string}) => IAction
|
onUpdateTeam: TeamActions['updateTeam']
|
||||||
team?: ITeam
|
team?: ITeam
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {IAction} from '../actions'
|
import {ITeam, IUserInTeam, ReadonlyRecord} from '@rondo/common'
|
||||||
import {ITeam, IUser, IUserInTeam, ReadonlyRecord} from '@rondo/common'
|
|
||||||
import {TeamList} from './TeamList'
|
import {TeamList} from './TeamList'
|
||||||
import {TeamUserList} from './TeamUserList'
|
import {TeamUserList} from './TeamUserList'
|
||||||
|
import {TeamActions} from './TeamActions'
|
||||||
|
|
||||||
export interface ITeamManagerProps {
|
export interface ITeamManagerProps {
|
||||||
createTeam: (params: {name: string}) => IAction
|
createTeam: TeamActions['createTeam']
|
||||||
updateTeam: (params: {id: number, name: string}) => IAction
|
updateTeam: TeamActions['updateTeam']
|
||||||
removeTeam: (params: {id: number}) => IAction
|
removeTeam: TeamActions['removeTeam']
|
||||||
|
|
||||||
addUser: (params: {userId: number, teamId: number, roleId: number}) => IAction
|
addUser: TeamActions['addUser']
|
||||||
removeUser: (params: {userId: number, teamId: number}) => IAction
|
removeUser: TeamActions['removeUser']
|
||||||
fetchMyTeams: () => IAction
|
fetchMyTeams: TeamActions['fetchMyTeams']
|
||||||
fetchUsersInTeam: (params: {teamId: number}) => IAction
|
fetchUsersInTeam: TeamActions['fetchUsersInTeam']
|
||||||
findUserByEmail: (email: string) => IAction<IUser | undefined>
|
findUserByEmail: TeamActions['findUserByEmail']
|
||||||
|
|
||||||
teamsById: ReadonlyRecord<number, ITeam>
|
teamsById: ReadonlyRecord<number, ITeam>
|
||||||
teamIds: ReadonlyArray<number>
|
teamIds: ReadonlyArray<number>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {ITeam, IUserInTeam, ReadonlyRecord, indexBy} from '@rondo/common'
|
import {ITeam, IUserInTeam, ReadonlyRecord, indexBy} from '@rondo/common'
|
||||||
import {TeamActionKeys, TeamActionType} from './TeamActions'
|
import {TeamActionType} from './TeamActions'
|
||||||
|
import {GetAction} from '../actions'
|
||||||
|
|
||||||
export interface ITeamState {
|
export interface ITeamState {
|
||||||
readonly error: string
|
readonly error: string
|
||||||
@ -23,10 +24,7 @@ const defaultState: ITeamState = {
|
|||||||
|
|
||||||
function removeUser(
|
function removeUser(
|
||||||
state: ITeamState,
|
state: ITeamState,
|
||||||
action: {
|
action: GetAction<TeamActionType, 'TEAM_USER_REMOVE_RESOLVED'>,
|
||||||
payload: {userId: number, teamId: number},
|
|
||||||
type: TeamActionKeys.TEAM_USER_REMOVE,
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const {payload} = action
|
const {payload} = action
|
||||||
@ -54,14 +52,14 @@ 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.type) {
|
switch (action.type) {
|
||||||
case TeamActionKeys.TEAMS:
|
case 'TEAMS_RESOLVED':
|
||||||
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 TeamActionKeys.TEAM_CREATE:
|
case 'TEAM_CREATE_RESOLVED':
|
||||||
case TeamActionKeys.TEAM_UPDATE:
|
case 'TEAM_UPDATE_RESOLVED':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
teamIds: state.teamIds.indexOf(action.payload.id) >= 0
|
teamIds: state.teamIds.indexOf(action.payload.id) >= 0
|
||||||
@ -73,7 +71,7 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
case TeamActionKeys.TEAM_USER_ADD:
|
case 'TEAM_USER_ADD_RESOLVED':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
userKeysByTeamId: {
|
userKeysByTeamId: {
|
||||||
@ -88,9 +86,9 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
|
|||||||
[getUserKey(action.payload)]: action.payload,
|
[getUserKey(action.payload)]: action.payload,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case TeamActionKeys.TEAM_USER_REMOVE:
|
case 'TEAM_USER_REMOVE_RESOLVED':
|
||||||
return removeUser(state, action)
|
return removeUser(state, action)
|
||||||
case TeamActionKeys.TEAM_USERS:
|
case 'TEAM_USERS_RESOLVED':
|
||||||
const usersByKey = action.payload.usersInTeam
|
const usersByKey = action.payload.usersInTeam
|
||||||
.reduce((obj, userInTeam) => {
|
.reduce((obj, userInTeam) => {
|
||||||
obj[getUserKey(userInTeam)] = userInTeam
|
obj[getUserKey(userInTeam)] = userInTeam
|
||||||
@ -109,11 +107,11 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
|
|||||||
...usersByKey,
|
...usersByKey,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case TeamActionKeys.TEAM_CREATE_REJECTED:
|
case 'TEAM_CREATE_REJECTED':
|
||||||
case TeamActionKeys.TEAM_UPDATE_REJECTED:
|
case 'TEAM_UPDATE_REJECTED':
|
||||||
case TeamActionKeys.TEAM_USER_ADD_REJECTED:
|
case 'TEAM_USER_ADD_REJECTED':
|
||||||
case TeamActionKeys.TEAM_USER_REMOVE_REJECTED:
|
case 'TEAM_USER_REMOVE_REJECTED':
|
||||||
case TeamActionKeys.TEAM_USERS_REJECTED:
|
case 'TEAM_USERS_REJECTED':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
error: action.error.message,
|
error: action.error.message,
|
||||||
|
|||||||
@ -1,17 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {IUser, IUserInTeam, ReadonlyRecord} from '@rondo/common'
|
import {IUser, IUserInTeam, ReadonlyRecord} from '@rondo/common'
|
||||||
import {IAction} from '../actions'
|
import {TeamActions} from './TeamActions'
|
||||||
|
|
||||||
const EMPTY_ARRAY: ReadonlyArray<string> = []
|
const EMPTY_ARRAY: ReadonlyArray<string> = []
|
||||||
|
|
||||||
export interface ITeamUsersProps {
|
export interface ITeamUsersProps {
|
||||||
// fetchMyTeams: () => void,
|
// fetchMyTeams: () => void,
|
||||||
fetchUsersInTeam: (params: {teamId: number}) => IAction
|
fetchUsersInTeam: TeamActions['fetchUsersInTeam']
|
||||||
findUserByEmail: (email: string) => IAction
|
findUserByEmail: TeamActions['findUserByEmail']
|
||||||
|
|
||||||
onAddUser: (params: {userId: number, teamId: number, roleId: number})
|
onAddUser: TeamActions['addUser']
|
||||||
=> IAction<IUserInTeam>
|
onRemoveUser: TeamActions['removeUser']
|
||||||
onRemoveUser: (params: {userId: number, teamId: number}) => IAction
|
|
||||||
|
|
||||||
teamId: number
|
teamId: number
|
||||||
userKeysByTeamId: ReadonlyRecord<number, ReadonlyArray<string>>
|
userKeysByTeamId: ReadonlyRecord<number, ReadonlyArray<string>>
|
||||||
@ -24,12 +23,8 @@ export interface ITeamUserProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IAddUserProps {
|
export interface IAddUserProps {
|
||||||
onAddUser: (params: {
|
onAddUser: TeamActions['addUser']
|
||||||
userId: number,
|
onSearchUser: TeamActions['findUserByEmail']
|
||||||
teamId: number,
|
|
||||||
roleId: number,
|
|
||||||
}) => IAction<IUserInTeam>
|
|
||||||
onSearchUser: (email: string) => IAction<IUser>
|
|
||||||
teamId: number
|
teamId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user