projects/node: Add custom redirectTo after successful login
This commit is contained in:
parent
8733dc2c30
commit
003bccc9e8
@ -3,58 +3,68 @@ import {IAPIDef, ICredentials, IUser} from '@rondo/common'
|
|||||||
import {IHTTPClient} from '../http/IHTTPClient'
|
import {IHTTPClient} from '../http/IHTTPClient'
|
||||||
|
|
||||||
export enum LoginActionKeys {
|
export enum LoginActionKeys {
|
||||||
USER_LOG_IN = 'USER_LOG_IN',
|
LOGIN = 'LOGIN',
|
||||||
USER_LOG_IN_PENDING = 'USER_LOG_IN_PENDING',
|
LOGIN_PENDING = 'LOGIN_PENDING',
|
||||||
USER_LOG_IN_REJECTED = 'USER_LOG_IN_REJECTED',
|
LOGIN_REJECTED = 'LOGIN_REJECTED',
|
||||||
|
|
||||||
USER_LOG_OUT = 'USER_LOG_OUT',
|
LOGIN_LOG_OUT = 'LOGIN_LOG_OUT',
|
||||||
USER_LOG_OUT_PENDING = 'USER_LOG_OUT_PENDING',
|
LOGIN_LOG_OUT_PENDING = 'LOGIN_LOG_OUT_PENDING',
|
||||||
USER_LOG_OUT_REJECTED = 'USER_LOG_OUT_REJECTED',
|
LOGIN_LOG_OUT_REJECTED = 'LOGIN_LOG_OUT_REJECTED',
|
||||||
|
|
||||||
REGISTER_USER = 'REGISTER_USER',
|
LOGIN_REGISTER = 'LOGIN_REGISTER',
|
||||||
REGISTER_USER_PENDING = 'REGISTER_USER_PENDING',
|
LOGIN_REGISTER_PENDING = 'LOGIN_REGISTER_PENDING',
|
||||||
REGISTER_USER_REJECTED = 'REGISTER_USER_REJECTED',
|
LOGIN_REGISTER_REJECTED = 'LOGIN_REGISTER_REJECTED',
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_SET = 'LOGIN_REDIRECT_SET',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LoginActions {
|
export class LoginActions {
|
||||||
constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
|
constructor(protected readonly http: IHTTPClient<IAPIDef>) {}
|
||||||
|
|
||||||
logIn = (credentials: ICredentials)
|
logIn = (credentials: ICredentials)
|
||||||
: IAction<IUser, LoginActionKeys.USER_LOG_IN> => {
|
: IAction<IUser, LoginActionKeys.LOGIN> => {
|
||||||
return {
|
return {
|
||||||
payload: this.http.post('/auth/login', credentials),
|
payload: this.http.post('/auth/login', credentials),
|
||||||
type: LoginActionKeys.USER_LOG_IN,
|
type: LoginActionKeys.LOGIN,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logInError = (error: Error)
|
logInError = (error: Error)
|
||||||
: IErrorAction<LoginActionKeys.USER_LOG_IN_REJECTED> => {
|
: IErrorAction<LoginActionKeys.LOGIN_REJECTED> => {
|
||||||
return {
|
return {
|
||||||
error,
|
error,
|
||||||
type: LoginActionKeys.USER_LOG_IN_REJECTED,
|
type: LoginActionKeys.LOGIN_REJECTED,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logOut = (): IAction<unknown, LoginActionKeys.USER_LOG_OUT> => {
|
logOut = (): IAction<unknown, LoginActionKeys.LOGIN_LOG_OUT> => {
|
||||||
return {
|
return {
|
||||||
payload: this.http.get('/auth/logout'),
|
payload: this.http.get('/auth/logout'),
|
||||||
type: LoginActionKeys.USER_LOG_OUT,
|
type: LoginActionKeys.LOGIN_LOG_OUT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
register = (profile: ICredentials):
|
register = (profile: ICredentials):
|
||||||
IAction<IUser, LoginActionKeys.REGISTER_USER> => {
|
IAction<IUser, LoginActionKeys.LOGIN_REGISTER> => {
|
||||||
return {
|
return {
|
||||||
payload: this.http.post('/auth/register', profile),
|
payload: this.http.post('/auth/register', profile),
|
||||||
type: LoginActionKeys.REGISTER_USER,
|
type: LoginActionKeys.LOGIN_REGISTER,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerError = (error: Error)
|
registerError = (error: Error)
|
||||||
: IErrorAction<LoginActionKeys.REGISTER_USER_REJECTED> => {
|
: IErrorAction<LoginActionKeys.LOGIN_REGISTER_REJECTED> => {
|
||||||
return {
|
return {
|
||||||
error,
|
error,
|
||||||
type: LoginActionKeys.REGISTER_USER_REJECTED,
|
type: LoginActionKeys.LOGIN_REGISTER_REJECTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRedirectTo = (redirectTo: string)
|
||||||
|
: IAction<{redirectTo: string}, LoginActionKeys.LOGIN_REDIRECT_SET> => {
|
||||||
|
return {
|
||||||
|
payload: {redirectTo},
|
||||||
|
type: LoginActionKeys.LOGIN_REDIRECT_SET,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const defaultCredentials: ICredentials = {
|
|||||||
password: '',
|
password: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LoginConnector extends Connector {
|
export class LoginConnector extends Connector<ILoginState> {
|
||||||
|
|
||||||
constructor(protected readonly loginActions: LoginActions) {
|
constructor(protected readonly loginActions: LoginActions) {
|
||||||
super()
|
super()
|
||||||
@ -24,6 +24,7 @@ export class LoginConnector extends Connector {
|
|||||||
state => ({
|
state => ({
|
||||||
error: state.error,
|
error: state.error,
|
||||||
user: state.user,
|
user: state.user,
|
||||||
|
redirectTo: state.redirectTo,
|
||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
onSubmit: bindActionCreators(this.loginActions.logIn, dispatch),
|
onSubmit: bindActionCreators(this.loginActions.logIn, dispatch),
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import * as Feature from './'
|
import * as Feature from './'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import T from 'react-dom/test-utils'
|
||||||
import {HTTPClientMock, TestUtils, getError} from '../test-utils'
|
import {HTTPClientMock, TestUtils, getError} from '../test-utils'
|
||||||
import {IAPIDef} from '@rondo/common'
|
import {IAPIDef} from '@rondo/common'
|
||||||
import T from 'react-dom/test-utils'
|
|
||||||
|
|
||||||
const test = new TestUtils()
|
const test = new TestUtils()
|
||||||
|
|
||||||
@ -10,15 +11,18 @@ describe('LoginForm', () => {
|
|||||||
const http = new HTTPClientMock<IAPIDef>()
|
const http = new HTTPClientMock<IAPIDef>()
|
||||||
const loginActions = new Feature.LoginActions(http)
|
const loginActions = new Feature.LoginActions(http)
|
||||||
|
|
||||||
const t = test.withProvider({
|
const createTestProvider = () => test.withProvider({
|
||||||
reducers: {Login: Feature.Login},
|
reducers: {Login: Feature.Login},
|
||||||
state: {Login: {user: {id: 1}}},
|
|
||||||
connector: new Feature.LoginConnector(loginActions),
|
connector: new Feature.LoginConnector(loginActions),
|
||||||
select: state => state.Login,
|
select: state => state.Login,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
(window as any).__MOCK_SERVER_SIDE__ = true
|
||||||
|
})
|
||||||
|
|
||||||
it('should render', () => {
|
it('should render', () => {
|
||||||
t.render()
|
createTestProvider().render()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('submit', () => {
|
describe('submit', () => {
|
||||||
@ -26,6 +30,7 @@ describe('LoginForm', () => {
|
|||||||
const data = {username: 'user', password: 'pass'}
|
const data = {username: 'user', password: 'pass'}
|
||||||
const onSuccess = jest.fn()
|
const onSuccess = jest.fn()
|
||||||
let node: Element
|
let node: Element
|
||||||
|
let component: React.Component
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
http.mockAdd({
|
http.mockAdd({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@ -33,8 +38,10 @@ describe('LoginForm', () => {
|
|||||||
data,
|
data,
|
||||||
}, {id: 123})
|
}, {id: 123})
|
||||||
|
|
||||||
|
const t = createTestProvider()
|
||||||
const r = t.render({onSuccess})
|
const r = t.render({onSuccess})
|
||||||
node = r.node
|
node = r.node
|
||||||
|
component = r.component
|
||||||
T.Simulate.change(
|
T.Simulate.change(
|
||||||
node.querySelector('input[name="username"]')!,
|
node.querySelector('input[name="username"]')!,
|
||||||
{target: {value: 'user'}} as any,
|
{target: {value: 'user'}} as any,
|
||||||
@ -45,7 +52,7 @@ describe('LoginForm', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should submit a form and clear it', async () => {
|
it('should submit a form, clear it and redirect it', async () => {
|
||||||
T.Simulate.submit(node)
|
T.Simulate.submit(node)
|
||||||
const {req} = await http.wait()
|
const {req} = await http.wait()
|
||||||
expect(req).toEqual({
|
expect(req).toEqual({
|
||||||
@ -64,6 +71,8 @@ describe('LoginForm', () => {
|
|||||||
.value,
|
.value,
|
||||||
)
|
)
|
||||||
.toEqual('')
|
.toEqual('')
|
||||||
|
node = ReactDOM.findDOMNode(component) as Element
|
||||||
|
expect(node.innerHTML).toMatch(/<a href="\/">/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets the error message on failure', async () => {
|
it('sets the error message on failure', async () => {
|
||||||
|
|||||||
@ -1,18 +1,24 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ICredentials} from '@rondo/common'
|
import {ICredentials, IUser} from '@rondo/common'
|
||||||
import {Input} from '../components/Input'
|
import {Input} from '../components/Input'
|
||||||
|
import {Redirect} from '../components/Redirect'
|
||||||
|
|
||||||
export interface ILoginFormProps {
|
export interface ILoginFormProps {
|
||||||
error?: string
|
error?: string
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
onChange: (name: string, value: string) => void
|
onChange: (name: string, value: string) => void
|
||||||
data: ICredentials
|
data: ICredentials
|
||||||
|
user?: IUser
|
||||||
|
redirectTo: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO maybe replace this with Formik, which is recommended in React docs
|
// TODO maybe replace this with Formik, which is recommended in React docs
|
||||||
// https://jaredpalmer.com/formik/docs/overview
|
// https://jaredpalmer.com/formik/docs/overview
|
||||||
export class LoginForm extends React.PureComponent<ILoginFormProps> {
|
export class LoginForm extends React.PureComponent<ILoginFormProps> {
|
||||||
render() {
|
render() {
|
||||||
|
if (this.props.user) {
|
||||||
|
return <Redirect to={this.props.redirectTo} />
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.props.onSubmit}>
|
<form onSubmit={this.props.onSubmit}>
|
||||||
<p className='error'>{this.props.error}</p>
|
<p className='error'>{this.props.error}</p>
|
||||||
|
|||||||
@ -2,13 +2,15 @@ import {IUser} from '@rondo/common'
|
|||||||
import {LoginActionKeys, LoginActionType} from './LoginActions'
|
import {LoginActionKeys, LoginActionType} from './LoginActions'
|
||||||
|
|
||||||
export interface ILoginState {
|
export interface ILoginState {
|
||||||
readonly error?: string,
|
readonly error?: string
|
||||||
readonly user?: IUser
|
readonly user?: IUser
|
||||||
|
readonly redirectTo: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultState: ILoginState = {
|
const defaultState: ILoginState = {
|
||||||
error: undefined,
|
error: undefined,
|
||||||
user: undefined,
|
user: undefined,
|
||||||
|
redirectTo: '/',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Login(
|
export function Login(
|
||||||
@ -16,16 +18,18 @@ export function Login(
|
|||||||
action: LoginActionType,
|
action: LoginActionType,
|
||||||
): ILoginState {
|
): ILoginState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case LoginActionKeys.USER_LOG_IN:
|
case LoginActionKeys.LOGIN:
|
||||||
return {...state, user: action.payload, error: ''}
|
return {...state, user: action.payload, error: ''}
|
||||||
case LoginActionKeys.USER_LOG_OUT:
|
case LoginActionKeys.LOGIN_LOG_OUT:
|
||||||
return {...state, user: undefined}
|
return {...state, user: undefined}
|
||||||
case LoginActionKeys.USER_LOG_IN_REJECTED:
|
case LoginActionKeys.LOGIN_REJECTED:
|
||||||
return {...state, error: action.error.message}
|
return {...state, error: action.error.message}
|
||||||
case LoginActionKeys.REGISTER_USER:
|
case LoginActionKeys.LOGIN_REGISTER:
|
||||||
return {...state, user: action.payload, error: ''}
|
return {...state, user: action.payload, error: ''}
|
||||||
case LoginActionKeys.REGISTER_USER_REJECTED:
|
case LoginActionKeys.LOGIN_REGISTER_REJECTED:
|
||||||
return {...state, error: action.error.message}
|
return {...state, error: action.error.message}
|
||||||
|
case LoginActionKeys.LOGIN_REDIRECT_SET:
|
||||||
|
return {...state, redirectTo: action.payload.redirectTo}
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const defaultCredentials: ICredentials = {
|
|||||||
password: '',
|
password: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RegisterConnector extends Connector {
|
export class RegisterConnector extends Connector<ILoginState> {
|
||||||
|
|
||||||
constructor(protected readonly loginActions: LoginActions) {
|
constructor(protected readonly loginActions: LoginActions) {
|
||||||
super()
|
super()
|
||||||
@ -23,6 +23,8 @@ export class RegisterConnector extends Connector {
|
|||||||
getLocalState,
|
getLocalState,
|
||||||
state => ({
|
state => ({
|
||||||
error: state.error,
|
error: state.error,
|
||||||
|
user: state.user,
|
||||||
|
redirectTo: state.redirectTo,
|
||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
onSubmit: bindActionCreators(this.loginActions.register, dispatch),
|
onSubmit: bindActionCreators(this.loginActions.register, dispatch),
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import * as Feature from './'
|
import * as Feature from './'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import T from 'react-dom/test-utils'
|
||||||
import {HTTPClientMock, TestUtils, getError} from '../test-utils'
|
import {HTTPClientMock, TestUtils, getError} from '../test-utils'
|
||||||
import {IAPIDef} from '@rondo/common'
|
import {IAPIDef} from '@rondo/common'
|
||||||
import T from 'react-dom/test-utils'
|
|
||||||
|
|
||||||
const test = new TestUtils()
|
const test = new TestUtils()
|
||||||
|
|
||||||
@ -10,15 +11,18 @@ describe('RegisterForm', () => {
|
|||||||
const http = new HTTPClientMock<IAPIDef>()
|
const http = new HTTPClientMock<IAPIDef>()
|
||||||
const loginActions = new Feature.LoginActions(http)
|
const loginActions = new Feature.LoginActions(http)
|
||||||
|
|
||||||
const t = test.withProvider({
|
const createTestProvider = () => test.withProvider({
|
||||||
reducers: {Login: Feature.Login},
|
reducers: {Login: Feature.Login},
|
||||||
state: {Login: {user: {id: 1}}},
|
|
||||||
connector: new Feature.RegisterConnector(loginActions),
|
connector: new Feature.RegisterConnector(loginActions),
|
||||||
select: state => state.Login,
|
select: state => state.Login,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
(window as any).__MOCK_SERVER_SIDE__ = true
|
||||||
|
})
|
||||||
|
|
||||||
it('should render', () => {
|
it('should render', () => {
|
||||||
t.render()
|
createTestProvider().render()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('submit', () => {
|
describe('submit', () => {
|
||||||
@ -26,6 +30,7 @@ describe('RegisterForm', () => {
|
|||||||
const data = {username: 'user', password: 'pass'}
|
const data = {username: 'user', password: 'pass'}
|
||||||
const onSuccess = jest.fn()
|
const onSuccess = jest.fn()
|
||||||
let node: Element
|
let node: Element
|
||||||
|
let component: React.Component
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
http.mockAdd({
|
http.mockAdd({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@ -33,7 +38,9 @@ describe('RegisterForm', () => {
|
|||||||
data,
|
data,
|
||||||
}, {id: 123})
|
}, {id: 123})
|
||||||
|
|
||||||
node = t.render({onSuccess}).node
|
const r = createTestProvider().render({onSuccess})
|
||||||
|
node = r.node
|
||||||
|
component = r.component
|
||||||
T.Simulate.change(
|
T.Simulate.change(
|
||||||
node.querySelector('input[name="username"]')!,
|
node.querySelector('input[name="username"]')!,
|
||||||
{target: {value: 'user'}} as any,
|
{target: {value: 'user'}} as any,
|
||||||
@ -44,7 +51,7 @@ describe('RegisterForm', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should submit a form', async () => {
|
it('should submit a form, clear it, and redirect', async () => {
|
||||||
T.Simulate.submit(node)
|
T.Simulate.submit(node)
|
||||||
const {req} = await http.wait()
|
const {req} = await http.wait()
|
||||||
expect(req).toEqual({
|
expect(req).toEqual({
|
||||||
@ -53,6 +60,18 @@ describe('RegisterForm', () => {
|
|||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
expect(onSuccess.mock.calls.length).toBe(1)
|
expect(onSuccess.mock.calls.length).toBe(1)
|
||||||
|
expect(
|
||||||
|
(node.querySelector('input[name="username"]') as HTMLInputElement)
|
||||||
|
.value,
|
||||||
|
)
|
||||||
|
.toEqual('')
|
||||||
|
expect(
|
||||||
|
(node.querySelector('input[name="password"]') as HTMLInputElement)
|
||||||
|
.value,
|
||||||
|
)
|
||||||
|
.toEqual('')
|
||||||
|
node = ReactDOM.findDOMNode(component) as Element
|
||||||
|
expect(node.innerHTML).toMatch(/<a href="\/">/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets the error message on failure', async () => {
|
it('sets the error message on failure', async () => {
|
||||||
|
|||||||
@ -1,16 +1,22 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ICredentials} from '@rondo/common'
|
import {ICredentials, IUser} from '@rondo/common'
|
||||||
import {Input} from '../components/Input'
|
import {Input} from '../components/Input'
|
||||||
|
import {Redirect} from '../components/Redirect'
|
||||||
|
|
||||||
export interface IRegisterFormProps {
|
export interface IRegisterFormProps {
|
||||||
error?: string
|
error?: string
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
onChange: (name: string, value: string) => void
|
onChange: (name: string, value: string) => void
|
||||||
data: ICredentials
|
data: ICredentials
|
||||||
|
user?: IUser
|
||||||
|
redirectTo: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RegisterForm extends React.PureComponent<IRegisterFormProps> {
|
export class RegisterForm extends React.PureComponent<IRegisterFormProps> {
|
||||||
render() {
|
render() {
|
||||||
|
if (this.props.user) {
|
||||||
|
return <Redirect to={this.props.redirectTo} />
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.props.onSubmit}>
|
<form onSubmit={this.props.onSubmit}>
|
||||||
<p className='error'>{this.props.error}</p>
|
<p className='error'>{this.props.error}</p>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import {ComponentType} from 'react'
|
|||||||
* easy to mock it during tests, or swap out different dependencies for
|
* easy to mock it during tests, or swap out different dependencies for
|
||||||
* different applications.
|
* different applications.
|
||||||
*/
|
*/
|
||||||
export abstract class Connector {
|
export abstract class Connector<LocalState> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects a component using redux. The `selectState` method is used to
|
* Connects a component using redux. The `selectState` method is used to
|
||||||
@ -29,13 +29,12 @@ export abstract class Connector {
|
|||||||
*
|
*
|
||||||
* https://stackoverflow.com/questions/54277411
|
* https://stackoverflow.com/questions/54277411
|
||||||
*/
|
*/
|
||||||
abstract connect<State, LocalState>(
|
abstract connect<State>(
|
||||||
selectState: IStateSelector<State, LocalState>,
|
selectState: IStateSelector<State, LocalState>,
|
||||||
): ComponentType<any>
|
): ComponentType<any>
|
||||||
|
|
||||||
protected wrap<
|
protected wrap<
|
||||||
State,
|
State,
|
||||||
LocalState,
|
|
||||||
StateProps,
|
StateProps,
|
||||||
DispatchProps,
|
DispatchProps,
|
||||||
Props
|
Props
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export function isClientSide() {
|
export function isClientSide() {
|
||||||
return typeof window !== 'undefined' &&
|
return typeof window !== 'undefined' &&
|
||||||
typeof window.document !== 'undefined' &&
|
typeof window.document !== 'undefined' &&
|
||||||
typeof window.document.createElement === 'function'
|
typeof window.document.createElement === 'function' &&
|
||||||
|
typeof (window as any).__MOCK_SERVER_SIDE__ === 'undefined'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
interface IRenderParams<State> {
|
interface IRenderParams<State> {
|
||||||
reducers: ReducersMapObject<State, any>
|
reducers: ReducersMapObject<State, any>
|
||||||
state?: DeepPartial<State>
|
state?: DeepPartial<State>
|
||||||
connector: Connector
|
connector: Connector<any>
|
||||||
select: IStateSelector<State, any>
|
select: IStateSelector<State, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user