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:
|
||||
// https://github.com/Microsoft/TypeScript/pull/29478
|
||||
|
||||
export interface IAction<T = any, ActionType extends string = string> {
|
||||
payload: Promise<T> | T,
|
||||
export interface IAction<ActionType extends string> {
|
||||
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 './IErrorAction'
|
||||
export * from './UnionType'
|
||||
export * from './IAsyncAction'
|
||||
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 {IHTTPClient} from '../http/IHTTPClient'
|
||||
|
||||
@ -18,56 +18,51 @@ export enum LoginActionKeys {
|
||||
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 {
|
||||
constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
|
||||
|
||||
logIn = (credentials: ICredentials)
|
||||
: IAction<IUser, LoginActionKeys.LOGIN> => {
|
||||
logIn = (credentials: ICredentials): Action<'LOGIN_PENDING'> => {
|
||||
return {
|
||||
payload: this.http.post('/auth/login', credentials),
|
||||
type: LoginActionKeys.LOGIN,
|
||||
type: 'LOGIN_PENDING',
|
||||
}
|
||||
}
|
||||
|
||||
logInError = (error: Error)
|
||||
: IErrorAction<LoginActionKeys.LOGIN_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: LoginActionKeys.LOGIN_REJECTED,
|
||||
}
|
||||
}
|
||||
|
||||
logOut = (): IAction<unknown, LoginActionKeys.LOGIN_LOG_OUT> => {
|
||||
logOut = (): Action<'LOGIN_LOGOUT_PENDING'> => {
|
||||
return {
|
||||
payload: this.http.get('/auth/logout'),
|
||||
type: LoginActionKeys.LOGIN_LOG_OUT,
|
||||
type: 'LOGIN_LOGOUT_PENDING',
|
||||
}
|
||||
}
|
||||
|
||||
register = (profile: INewUser):
|
||||
IAction<IUser, LoginActionKeys.LOGIN_REGISTER> => {
|
||||
register = (profile: INewUser): Action<'LOGIN_REGISTER_PENDING'> => {
|
||||
return {
|
||||
payload: this.http.post('/auth/register', profile),
|
||||
type: LoginActionKeys.LOGIN_REGISTER,
|
||||
type: 'LOGIN_REGISTER_PENDING',
|
||||
}
|
||||
}
|
||||
|
||||
registerError = (error: Error)
|
||||
: IErrorAction<LoginActionKeys.LOGIN_REGISTER_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: LoginActionKeys.LOGIN_REGISTER_REJECTED,
|
||||
}
|
||||
}
|
||||
|
||||
setRedirectTo = (redirectTo: string)
|
||||
: IAction<{redirectTo: string}, LoginActionKeys.LOGIN_REDIRECT_SET> => {
|
||||
setRedirectTo = (redirectTo: string): Action<'LOGIN_REDIRECT_SET'> => {
|
||||
return {
|
||||
payload: {redirectTo},
|
||||
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 {LoginActionKeys, LoginActionType} from './LoginActions'
|
||||
import {LoginActionType} from './LoginActions'
|
||||
|
||||
export interface ILoginState {
|
||||
readonly error?: string
|
||||
@ -18,17 +18,17 @@ export function Login(
|
||||
action: LoginActionType,
|
||||
): ILoginState {
|
||||
switch (action.type) {
|
||||
case LoginActionKeys.LOGIN:
|
||||
case 'LOGIN_RESOLVED':
|
||||
return {...state, user: action.payload, error: ''}
|
||||
case LoginActionKeys.LOGIN_LOG_OUT:
|
||||
case 'LOGIN_LOGOUT_RESOLVED':
|
||||
return {...state, user: undefined}
|
||||
case LoginActionKeys.LOGIN_REJECTED:
|
||||
case 'LOGIN_REJECTED':
|
||||
return {...state, error: action.error.message}
|
||||
case LoginActionKeys.LOGIN_REGISTER:
|
||||
case 'LOGIN_REGISTER_RESOLVED':
|
||||
return {...state, user: action.payload, error: ''}
|
||||
case LoginActionKeys.LOGIN_REGISTER_REJECTED:
|
||||
case 'LOGIN_REGISTER_REJECTED':
|
||||
return {...state, error: action.error.message}
|
||||
case LoginActionKeys.LOGIN_REDIRECT_SET:
|
||||
case 'LOGIN_REDIRECT_SET':
|
||||
return {...state, redirectTo: action.payload.redirectTo}
|
||||
default:
|
||||
return state
|
||||
|
||||
@ -10,7 +10,7 @@ export interface IComponentProps<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
|
||||
// would have the same required property as the HOC, like onSuccess?
|
||||
onSuccess?: () => void
|
||||
|
||||
@ -26,21 +26,21 @@ describe('PromiseMiddleware', () => {
|
||||
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 type = 'TEST'
|
||||
const action = {
|
||||
payload: Promise.resolve(value),
|
||||
type,
|
||||
type: `${type}_PENDING`,
|
||||
}
|
||||
const result = store.dispatch(action)
|
||||
expect(result).toBe(action)
|
||||
await result.payload
|
||||
expect(store.getState().slice(1)).toEqual([{
|
||||
type: `${type}_PENDING`,
|
||||
...action,
|
||||
}, {
|
||||
payload: value,
|
||||
type,
|
||||
type: type + '_RESOLVED',
|
||||
}])
|
||||
})
|
||||
|
||||
@ -49,14 +49,14 @@ describe('PromiseMiddleware', () => {
|
||||
const type = 'TEST'
|
||||
const action = {
|
||||
payload: Promise.reject(error),
|
||||
type,
|
||||
type: `${type}_PENDING`,
|
||||
}
|
||||
const result = store.dispatch(action)
|
||||
expect(result).toBe(action)
|
||||
const err = await getError(result.payload)
|
||||
expect(err).toBe(error)
|
||||
expect(store.getState().slice(1)).toEqual([{
|
||||
type: `${type}_PENDING`,
|
||||
...action,
|
||||
}, {
|
||||
error,
|
||||
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
|
||||
* 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
|
||||
* both Action creators and Reducers.
|
||||
*
|
||||
@ -20,36 +20,42 @@ 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 fulfilledExtension = '',
|
||||
readonly resolvedExtension = '_RESOLVED',
|
||||
readonly rejectedExtension = '_REJECTED',
|
||||
) {
|
||||
assert(
|
||||
this.pendingExtension !== this.fulfilledExtension &&
|
||||
this.fulfilledExtension !== this.rejectedExtension &&
|
||||
this.pendingExtension !== this.resolvedExtension &&
|
||||
this.resolvedExtension !== 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) => {
|
||||
const {payload, type} = action
|
||||
// Propagate this action. Only attach listeners to the promise.
|
||||
next(action)
|
||||
if (!isPromise(payload)) {
|
||||
next(action)
|
||||
return
|
||||
}
|
||||
store.dispatch({type: type + this.pendingExtension})
|
||||
|
||||
const strippedType = type.replace(this.regexp, '')
|
||||
|
||||
payload
|
||||
.then(result => {
|
||||
store.dispatch({
|
||||
payload: result,
|
||||
type,
|
||||
type: strippedType + this.resolvedExtension,
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
store.dispatch({
|
||||
error: err,
|
||||
type: type + this.rejectedExtension,
|
||||
type: strippedType + this.rejectedExtension,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,105 +1,73 @@
|
||||
import {IAPIDef} from '@rondo/common'
|
||||
import {IAction, IErrorAction, ActionTypes} from '../actions'
|
||||
import {GetAction, IAsyncAction} from '../actions'
|
||||
import {IHTTPClient} from '../http/IHTTPClient'
|
||||
import {ITeam, IUser, IUserInTeam} from '@rondo/common'
|
||||
|
||||
export enum TeamActionKeys {
|
||||
TEAMS = 'TEAMS',
|
||||
TEAMS_PENDING = 'TEAMS_PENDING',
|
||||
TEAMS_REJECTED = 'TEAMS_REJECTED',
|
||||
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<{},
|
||||
'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',
|
||||
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',
|
||||
}
|
||||
type Action<T extends string> = GetAction<TeamActionType, T>
|
||||
|
||||
export class TeamActions {
|
||||
constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
|
||||
|
||||
fetchMyTeams = (): IAction<ITeam[], TeamActionKeys.TEAMS> => {
|
||||
fetchMyTeams = (): Action<'TEAMS_PENDING'> => {
|
||||
return {
|
||||
payload: this.http.get('/my/teams'),
|
||||
type: TeamActionKeys.TEAMS,
|
||||
type: 'TEAMS_PENDING',
|
||||
}
|
||||
}
|
||||
|
||||
fetchMyTeamsError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAMS_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAMS_REJECTED,
|
||||
}
|
||||
}
|
||||
|
||||
createTeam = (team: {name: string})
|
||||
: IAction<ITeam, TeamActionKeys.TEAM_CREATE> => {
|
||||
createTeam = (team: {name: string}): Action<'TEAM_CREATE_PENDING'> => {
|
||||
return {
|
||||
payload: this.http.post('/teams', team),
|
||||
type: TeamActionKeys.TEAM_CREATE,
|
||||
}
|
||||
}
|
||||
|
||||
createTeamError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_CREATE_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_CREATE_REJECTED,
|
||||
type: 'TEAM_CREATE_PENDING',
|
||||
}
|
||||
}
|
||||
|
||||
updateTeam = ({id, name}: {id: number, name: string})
|
||||
: IAction<ITeam, TeamActionKeys.TEAM_UPDATE> => {
|
||||
: Action<'TEAM_UPDATE_PENDING'> => {
|
||||
return {
|
||||
payload: this.http.put('/teams/:id', {name}, {id}),
|
||||
type: TeamActionKeys.TEAM_UPDATE,
|
||||
type: 'TEAM_UPDATE_PENDING',
|
||||
}
|
||||
}
|
||||
|
||||
updateTeamError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_UPDATE_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_UPDATE_REJECTED,
|
||||
}
|
||||
}
|
||||
|
||||
removeTeam = ({id}: {id: number})
|
||||
: IAction<{}, TeamActionKeys.TEAM_REMOVE> => {
|
||||
removeTeam = ({id}: {id: number}): Action<'TEAM_REMOVE_PENDING'> => {
|
||||
return {
|
||||
payload: this.http.delete('/teams/:id', {}, {id}),
|
||||
type: TeamActionKeys.TEAM_REMOVE,
|
||||
}
|
||||
}
|
||||
|
||||
removeTeamError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_REMOVE_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_REMOVE_REJECTED,
|
||||
type: 'TEAM_REMOVE_PENDING',
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,21 +77,13 @@ export class TeamActions {
|
||||
teamId: number,
|
||||
roleId: number,
|
||||
})
|
||||
: IAction<IUserInTeam, TeamActionKeys.TEAM_USER_ADD> {
|
||||
: Action<'TEAM_USER_ADD_PENDING'> {
|
||||
return {
|
||||
payload: this.http.post('/teams/:teamId/users/:userId', {}, {
|
||||
userId,
|
||||
teamId,
|
||||
}),
|
||||
type: TeamActionKeys.TEAM_USER_ADD,
|
||||
}
|
||||
}
|
||||
|
||||
addUserError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_USER_ADD_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_USER_ADD_REJECTED,
|
||||
type: 'TEAM_USER_ADD_PENDING',
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,66 +92,33 @@ export class TeamActions {
|
||||
userId: number,
|
||||
teamId: number,
|
||||
})
|
||||
: IAction<{
|
||||
userId: number,
|
||||
teamId: number,
|
||||
}, TeamActionKeys.TEAM_USER_REMOVE> => {
|
||||
: Action<'TEAM_USER_REMOVE_PENDING'> => {
|
||||
return {
|
||||
payload: this.http.delete('/teams/:teamId/users/:userId', {}, {
|
||||
userId,
|
||||
teamId,
|
||||
}),
|
||||
type: TeamActionKeys.TEAM_USER_REMOVE,
|
||||
}
|
||||
}
|
||||
|
||||
removeUserError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_USER_REMOVE_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_USER_REMOVE_REJECTED,
|
||||
type: 'TEAM_USER_REMOVE_PENDING',
|
||||
}
|
||||
}
|
||||
|
||||
fetchUsersInTeam = ({teamId}: {teamId: number})
|
||||
: IAction<{
|
||||
teamId: number,
|
||||
usersInTeam: IUserInTeam[]
|
||||
}, TeamActionKeys.TEAM_USERS> => {
|
||||
: Action<'TEAM_USERS_PENDING'> => {
|
||||
return {
|
||||
payload: this.http.get('/teams/:teamId/users', {
|
||||
teamId,
|
||||
})
|
||||
.then(usersInTeam => ({teamId, usersInTeam})),
|
||||
type: TeamActionKeys.TEAM_USERS,
|
||||
type: 'TEAM_USERS_PENDING',
|
||||
}
|
||||
}
|
||||
|
||||
fetchUsersInTeamError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_USERS_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_USERS_REJECTED,
|
||||
}
|
||||
}
|
||||
|
||||
findUserByEmail = (email: string)
|
||||
: IAction<IUser | undefined, TeamActionKeys.TEAM_USER_FIND> => {
|
||||
findUserByEmail = (email: string): Action<'TEAM_USER_FIND_PENDING'> => {
|
||||
return {
|
||||
payload: this.http.get('/users/emails/:email', {
|
||||
email,
|
||||
}),
|
||||
type: TeamActionKeys.TEAM_USER_FIND,
|
||||
}
|
||||
}
|
||||
|
||||
findUserByEmailError = (error: Error)
|
||||
: IErrorAction<TeamActionKeys.TEAM_USER_FIND_REJECTED> => {
|
||||
return {
|
||||
error,
|
||||
type: TeamActionKeys.TEAM_USER_FIND_REJECTED,
|
||||
type: 'TEAM_USER_FIND_PENDING',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type TeamActionType = ActionTypes<TeamActions>
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
import React from 'react'
|
||||
import {IAction} from '../actions'
|
||||
import {ITeam, ReadonlyRecord} from '@rondo/common'
|
||||
import {TeamActions} from './TeamActions'
|
||||
|
||||
export interface ITeamListProps {
|
||||
teamsById: ReadonlyRecord<number, ITeam>,
|
||||
teamIds: ReadonlyArray<number>,
|
||||
onAddTeam: (params: {name: string}) => IAction
|
||||
onRemoveTeam: (params: {id: number}) => IAction
|
||||
onUpdateTeam: (params: {id: number, name: string}) => IAction
|
||||
onAddTeam: TeamActions['createTeam']
|
||||
onRemoveTeam: TeamActions['removeTeam']
|
||||
onUpdateTeam: TeamActions['updateTeam']
|
||||
editTeamId: number
|
||||
}
|
||||
|
||||
export interface ITeamProps {
|
||||
team: ITeam
|
||||
editTeamId: number // TODO handle edits via react-router params
|
||||
onRemoveTeam: (params: {id: number}) => IAction
|
||||
onUpdateTeam: (params: {id: number, name: string}) => IAction
|
||||
onRemoveTeam: TeamActions['removeTeam']
|
||||
onUpdateTeam: TeamActions['updateTeam']
|
||||
}
|
||||
|
||||
export interface IAddTeamProps {
|
||||
onAddTeam: (params: {name: string}) => IAction
|
||||
onUpdateTeam: (params: {id: number, name: string}) => IAction
|
||||
onAddTeam: TeamActions['createTeam']
|
||||
onUpdateTeam: TeamActions['updateTeam']
|
||||
team?: ITeam
|
||||
}
|
||||
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import React from 'react'
|
||||
import {IAction} from '../actions'
|
||||
import {ITeam, IUser, IUserInTeam, ReadonlyRecord} from '@rondo/common'
|
||||
import {ITeam, IUserInTeam, ReadonlyRecord} from '@rondo/common'
|
||||
import {TeamList} from './TeamList'
|
||||
import {TeamUserList} from './TeamUserList'
|
||||
import {TeamActions} from './TeamActions'
|
||||
|
||||
export interface ITeamManagerProps {
|
||||
createTeam: (params: {name: string}) => IAction
|
||||
updateTeam: (params: {id: number, name: string}) => IAction
|
||||
removeTeam: (params: {id: number}) => IAction
|
||||
createTeam: TeamActions['createTeam']
|
||||
updateTeam: TeamActions['updateTeam']
|
||||
removeTeam: TeamActions['removeTeam']
|
||||
|
||||
addUser: (params: {userId: number, teamId: number, roleId: number}) => IAction
|
||||
removeUser: (params: {userId: number, teamId: number}) => IAction
|
||||
fetchMyTeams: () => IAction
|
||||
fetchUsersInTeam: (params: {teamId: number}) => IAction
|
||||
findUserByEmail: (email: string) => IAction<IUser | undefined>
|
||||
addUser: TeamActions['addUser']
|
||||
removeUser: TeamActions['removeUser']
|
||||
fetchMyTeams: TeamActions['fetchMyTeams']
|
||||
fetchUsersInTeam: TeamActions['fetchUsersInTeam']
|
||||
findUserByEmail: TeamActions['findUserByEmail']
|
||||
|
||||
teamsById: ReadonlyRecord<number, ITeam>
|
||||
teamIds: ReadonlyArray<number>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {ITeam, IUserInTeam, ReadonlyRecord, indexBy} from '@rondo/common'
|
||||
import {TeamActionKeys, TeamActionType} from './TeamActions'
|
||||
import {TeamActionType} from './TeamActions'
|
||||
import {GetAction} from '../actions'
|
||||
|
||||
export interface ITeamState {
|
||||
readonly error: string
|
||||
@ -23,10 +24,7 @@ const defaultState: ITeamState = {
|
||||
|
||||
function removeUser(
|
||||
state: ITeamState,
|
||||
action: {
|
||||
payload: {userId: number, teamId: number},
|
||||
type: TeamActionKeys.TEAM_USER_REMOVE,
|
||||
},
|
||||
action: GetAction<TeamActionType, 'TEAM_USER_REMOVE_RESOLVED'>,
|
||||
) {
|
||||
|
||||
const {payload} = action
|
||||
@ -54,14 +52,14 @@ function getUserKey(userInTeam: {userId: number, teamId: number}) {
|
||||
|
||||
export function Team(state = defaultState, action: TeamActionType): ITeamState {
|
||||
switch (action.type) {
|
||||
case TeamActionKeys.TEAMS:
|
||||
case 'TEAMS_RESOLVED':
|
||||
return {
|
||||
...state,
|
||||
teamIds: action.payload.map(team => team.id),
|
||||
teamsById: indexBy(action.payload, 'id'),
|
||||
}
|
||||
case TeamActionKeys.TEAM_CREATE:
|
||||
case TeamActionKeys.TEAM_UPDATE:
|
||||
case 'TEAM_CREATE_RESOLVED':
|
||||
case 'TEAM_UPDATE_RESOLVED':
|
||||
return {
|
||||
...state,
|
||||
teamIds: state.teamIds.indexOf(action.payload.id) >= 0
|
||||
@ -73,7 +71,7 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
|
||||
},
|
||||
}
|
||||
return state
|
||||
case TeamActionKeys.TEAM_USER_ADD:
|
||||
case 'TEAM_USER_ADD_RESOLVED':
|
||||
return {
|
||||
...state,
|
||||
userKeysByTeamId: {
|
||||
@ -88,9 +86,9 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
|
||||
[getUserKey(action.payload)]: action.payload,
|
||||
},
|
||||
}
|
||||
case TeamActionKeys.TEAM_USER_REMOVE:
|
||||
case 'TEAM_USER_REMOVE_RESOLVED':
|
||||
return removeUser(state, action)
|
||||
case TeamActionKeys.TEAM_USERS:
|
||||
case 'TEAM_USERS_RESOLVED':
|
||||
const usersByKey = action.payload.usersInTeam
|
||||
.reduce((obj, userInTeam) => {
|
||||
obj[getUserKey(userInTeam)] = userInTeam
|
||||
@ -109,11 +107,11 @@ export function Team(state = defaultState, action: TeamActionType): ITeamState {
|
||||
...usersByKey,
|
||||
},
|
||||
}
|
||||
case TeamActionKeys.TEAM_CREATE_REJECTED:
|
||||
case TeamActionKeys.TEAM_UPDATE_REJECTED:
|
||||
case TeamActionKeys.TEAM_USER_ADD_REJECTED:
|
||||
case TeamActionKeys.TEAM_USER_REMOVE_REJECTED:
|
||||
case TeamActionKeys.TEAM_USERS_REJECTED:
|
||||
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,
|
||||
|
||||
@ -1,17 +1,16 @@
|
||||
import React from 'react'
|
||||
import {IUser, IUserInTeam, ReadonlyRecord} from '@rondo/common'
|
||||
import {IAction} from '../actions'
|
||||
import {TeamActions} from './TeamActions'
|
||||
|
||||
const EMPTY_ARRAY: ReadonlyArray<string> = []
|
||||
|
||||
export interface ITeamUsersProps {
|
||||
// fetchMyTeams: () => void,
|
||||
fetchUsersInTeam: (params: {teamId: number}) => IAction
|
||||
findUserByEmail: (email: string) => IAction
|
||||
fetchUsersInTeam: TeamActions['fetchUsersInTeam']
|
||||
findUserByEmail: TeamActions['findUserByEmail']
|
||||
|
||||
onAddUser: (params: {userId: number, teamId: number, roleId: number})
|
||||
=> IAction<IUserInTeam>
|
||||
onRemoveUser: (params: {userId: number, teamId: number}) => IAction
|
||||
onAddUser: TeamActions['addUser']
|
||||
onRemoveUser: TeamActions['removeUser']
|
||||
|
||||
teamId: number
|
||||
userKeysByTeamId: ReadonlyRecord<number, ReadonlyArray<string>>
|
||||
@ -24,12 +23,8 @@ export interface ITeamUserProps {
|
||||
}
|
||||
|
||||
export interface IAddUserProps {
|
||||
onAddUser: (params: {
|
||||
userId: number,
|
||||
teamId: number,
|
||||
roleId: number,
|
||||
}) => IAction<IUserInTeam>
|
||||
onSearchUser: (email: string) => IAction<IUser>
|
||||
onAddUser: TeamActions['addUser']
|
||||
onSearchUser: TeamActions['findUserByEmail']
|
||||
teamId: number
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user