Fix packages/client

This commit is contained in:
Jerko Steiner 2019-09-16 00:44:21 +07:00
parent 30d1f1fcd4
commit 92912af839
33 changed files with 320 additions and 309 deletions

View File

@ -37,6 +37,10 @@ rules:
'@typescript-eslint/explicit-function-return-type': off '@typescript-eslint/explicit-function-return-type': off
'@typescript-eslint/no-non-null-assertion': off '@typescript-eslint/no-non-null-assertion': off
'@typescript-eslint/no-use-before-define': off '@typescript-eslint/no-use-before-define': off
'@typescript-eslint/no-empty-interface': off
'@typescript-eslint/no-explicit-any':
- warn
- ignoreRestArgs: true
overrides: overrides:
- files: - files:
- '*.test.ts' - '*.test.ts'

View File

@ -2,7 +2,7 @@ import React from 'react'
import {Control, Field, Input as I, Heading} from 'bloomer' import {Control, Field, Input as I, Heading} from 'bloomer'
import {IconType} from 'react-icons' import {IconType} from 'react-icons'
export interface IInputProps { export interface InputProps {
name: string name: string
type: 'text' | 'password' | 'hidden' | 'submit' | 'email' type: 'text' | 'password' | 'hidden' | 'submit' | 'email'
value?: string value?: string
@ -14,7 +14,7 @@ export interface IInputProps {
required?: boolean required?: boolean
} }
export class Input extends React.PureComponent<IInputProps> { export class Input extends React.PureComponent<InputProps> {
handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(this.props.name, e.target.value) this.props.onChange(this.props.name, e.target.value)

View File

@ -1,24 +1,21 @@
import { URLFormatter } from '@rondo.dev/http-client'
import React from 'react' import React from 'react'
import {History, Location} from 'history' import { withRouter } from 'react-router'
import {IWithRouterProps} from './IWithRouterProps' import { Link as RouterLink } from 'react-router-dom'
import {Link as RouterLink, LinkProps} from 'react-router-dom' import { WithRouterProps } from './WithRouterProps'
import {URLFormatter} from '@rondo.dev/http-client'
import {withRouter} from 'react-router'
export interface ILinkProps export interface LinkProps
extends IWithRouterProps<Record<string, string>> { extends WithRouterProps<Record<string, string>> {
readonly className?: string readonly className?: string
readonly to: string readonly to: string
} }
class ContextLink extends React.PureComponent<ILinkProps> { class ContextLink extends React.PureComponent<LinkProps> {
protected readonly urlFormatter = new URLFormatter() protected readonly urlFormatter = new URLFormatter()
render() { render() {
const { const {
className, className,
history,
location,
match, match,
to, to,
children, children,

View File

@ -15,7 +15,7 @@ export class Redirect extends React.PureComponent<RedirectProps> {
return ( return (
<span> <span>
You are being redirected. You are being redirected.
Click <a href={href}>here</a> to 'continue' Click <a href={href}>here</a> to {'continue'}
</span> </span>
) )
} }

View File

@ -1,22 +1,18 @@
import React, {useEffect} from 'react' import {useEffect} from 'react'
import {Dispatch, bindActionCreators} from 'redux' import {Dispatch, bindActionCreators} from 'redux'
import {IWithRouterProps} from './IWithRouterProps' import {WithRouterProps} from './WithRouterProps'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {setRedirectTo} from '../login/LoginActions' import {setRedirectTo} from '../login/LoginActions'
import {withRouter} from 'react-router' import {withRouter} from 'react-router'
export interface IReturnToProps extends IWithRouterProps { export interface ReturnToProps extends WithRouterProps {
setRedirectTo: typeof setRedirectTo setRedirectTo: typeof setRedirectTo
} }
function FReturnHere(props: IReturnToProps) { function FReturnHere(props: ReturnToProps) {
const { const {
// tslint:disable-next-line
setRedirectTo, setRedirectTo,
history,
location,
match, match,
...otherProps
} = props } = props
useEffect(() => { useEffect(() => {

View File

@ -1,12 +1,12 @@
import {format} from 'timeago.js' import {format} from 'timeago.js'
import React from 'react' import React from 'react'
export interface ITimeAgoProps { export interface TimeAgoProps {
className?: string className?: string
date: Date | string date: Date | string
} }
export class TimeAgo extends React.PureComponent<ITimeAgoProps> { export class TimeAgo extends React.PureComponent<TimeAgoProps> {
render() { render() {
return ( return (
<time className={this.props.className}> <time className={this.props.className}>

View File

@ -1,7 +1,7 @@
import {match as Match} from 'react-router' import {match as Match} from 'react-router'
import {History, Location} from 'history' import {History, Location} from 'history'
export interface IWithRouterProps<MatchProps = unknown> { export interface WithRouterProps<MatchProps = unknown> {
history: History history: History
location: Location location: Location
match: Match<MatchProps> match: Match<MatchProps>

View File

@ -2,7 +2,7 @@ import {History, Location} from 'history'
import {withRouter, match as Match} from 'react-router' import {withRouter, match as Match} from 'react-router'
import React from 'react' import React from 'react'
export interface IWithHistoryProps { export interface WithHistoryProps {
history: History history: History
location: Location location: Location
match: Match match: Match
@ -11,9 +11,9 @@ export interface IWithHistoryProps {
export function withHistory<T extends {history: History}>( export function withHistory<T extends {history: History}>(
Component: React.ComponentType<T>, Component: React.ComponentType<T>,
) { ) {
class HistoryProvider extends React.PureComponent<IWithHistoryProps & T> { class HistoryProvider extends React.PureComponent<WithHistoryProps & T> {
render() { render() {
const {history, location, match, children, ...props} = this.props const {history, children} = this.props
return ( return (
<Component history={history} {...this.props}> <Component history={history} {...this.props}>
{children} {children}

View File

@ -1,69 +1,69 @@
import { HTTPClientMock } from '@rondo.dev/http-client' import { HTTPClientMock } from '@rondo.dev/http-client'
import { TMethod } from '@rondo.dev/http-types' import { Method } from '@rondo.dev/http-types'
import { IPendingAction } from '@rondo.dev/redux' import { PendingAction } from '@rondo.dev/redux'
import { getError } from '@rondo.dev/test-utils' import { getError } from '@rondo.dev/test-utils'
import { AnyAction } from 'redux' import { AnyAction } from 'redux'
import { TestUtils } from '../test-utils' import { TestUtils } from '../test-utils'
import { CRUDReducer, TCRUDAsyncMethod } from './' import { CRUDReducer, CRUDAsyncMethod } from './'
import { createCRUDActions } from './CRUDActions' import { createCRUDActions } from './CRUDActions'
describe('CRUD', () => { describe('CRUD', () => {
interface ITwo { interface Two {
id: number id: number
name: string name: string
} }
interface ITwoCreateBody { interface TwoCreateBody {
name: string name: string
} }
interface ITwoListParams { interface TwoListParams {
oneId: number oneId: number
} }
interface ITwoSpecificParams { interface TwoSpecificParams {
oneId: number oneId: number
twoId: number twoId: number
} }
interface ITestAPI { interface TestAPI {
'/one/:oneId/two/:twoId': { '/one/:oneId/two/:twoId': {
get: { get: {
params: ITwoSpecificParams params: TwoSpecificParams
response: ITwo response: Two
} }
put: { put: {
params: ITwoSpecificParams params: TwoSpecificParams
body: ITwoCreateBody body: TwoCreateBody
response: ITwo response: Two
} }
delete: { delete: {
params: ITwoSpecificParams params: TwoSpecificParams
response: {id: number} // TODO return ITwoSpecificParams response: {id: number} // TODO return ITwoSpecificParams
} }
} }
'/one/:oneId/two': { '/one/:oneId/two': {
get: { get: {
params: ITwoListParams params: TwoListParams
response: ITwo[] response: Two[]
} }
post: { post: {
params: ITwoListParams params: TwoListParams
body: ITwoCreateBody body: TwoCreateBody
response: ITwo response: Two
} }
} }
} }
const http = new HTTPClientMock<ITestAPI>() const http = new HTTPClientMock<TestAPI>()
const actions = createCRUDActions( const actions = createCRUDActions(
http, http,
'/one/:oneId/two/:twoId', '/one/:oneId/two/:twoId',
'/one/:oneId/two', '/one/:oneId/two',
'TEST', 'TEST',
) )
const crudReducer = new CRUDReducer<ITwo, 'TEST'>('TEST', {name: ''}) const crudReducer = new CRUDReducer<Two, 'TEST'>('TEST', {name: ''})
const Crud = crudReducer.reduce const Crud = crudReducer.reduce
const test = new TestUtils() const test = new TestUtils()
@ -101,8 +101,8 @@ describe('CRUD', () => {
}) })
function dispatch( function dispatch(
method: TCRUDAsyncMethod, method: CRUDAsyncMethod,
action: IPendingAction<unknown, string>, action: PendingAction<unknown, string>,
) { ) {
store.dispatch(action) store.dispatch(action)
expect(store.getState().Crud.status[method].isLoading).toBe(true) expect(store.getState().Crud.status[method].isLoading).toBe(true)
@ -110,13 +110,13 @@ describe('CRUD', () => {
return action return action
} }
function getUrl(method: TCRUDAsyncMethod) { function getUrl(method: CRUDAsyncMethod) {
return method === 'save' || method === 'findMany' return method === 'save' || method === 'findMany'
? '/one/1/two' ? '/one/1/two'
: '/one/1/two/2' : '/one/1/two/2'
} }
function getHTTPMethod(method: TCRUDAsyncMethod): TMethod { function getHTTPMethod(method: CRUDAsyncMethod): Method {
switch (method) { switch (method) {
case 'save': case 'save':
return 'post' return 'post'
@ -132,7 +132,7 @@ describe('CRUD', () => {
describe('Promise rejections', () => { describe('Promise rejections', () => {
const testCases: Array<{ const testCases: Array<{
method: TCRUDAsyncMethod method: CRUDAsyncMethod
params: any params: any
}> = [{ }> = [{
method: 'findOne', method: 'findOne',
@ -204,7 +204,7 @@ describe('CRUD', () => {
const entity = {id: 100, name: 'test'} const entity = {id: 100, name: 'test'}
const testCases: Array<{ const testCases: Array<{
method: TCRUDAsyncMethod, method: CRUDAsyncMethod
params: any params: any
body?: any body?: any
response: any response: any

View File

@ -0,0 +1,41 @@
import {Action, AsyncAction} from '@rondo.dev/redux'
import {CRUDMethod} from './CRUDMethod'
// Async actions
export type CRUDSaveAction<T, ActionType extends string> =
AsyncAction<T, ActionType> & {method: Extract<CRUDMethod, 'save'>}
export type CRUDUpdateAction<T, ActionType extends string> =
AsyncAction<T, ActionType> & {method: Extract<CRUDMethod, 'update'>}
export type CRUDRemoveAction<T, ActionType extends string> =
AsyncAction<T, ActionType> & {method: Extract<CRUDMethod, 'remove'>}
export type CRUDFindOneAction<T, ActionType extends string> =
AsyncAction<T, ActionType> & {method: Extract<CRUDMethod, 'findOne'>}
export type CRUDFindManyAction<T, ActionType extends string> =
AsyncAction<T[], ActionType> & {method: Extract<CRUDMethod, 'findMany'>}
// Synchronous actions
export type CRUDCreateAction<T, ActionType extends string> =
Action<Partial<T>, ActionType> & {method: Extract<CRUDMethod, 'create'>}
export type CRUDEditAction<ActionType extends string> =
Action<{id: number}, ActionType> & {method: Extract<CRUDMethod, 'edit'>}
export type CRUDChangeAction<T, ActionType extends string> =
Action<{id?: number, key: keyof T, value: string}, ActionType>
& {method: Extract<CRUDMethod, 'change'>}
export type CRUDAction<T, ActionType extends string> =
CRUDSaveAction<T, ActionType>
| CRUDUpdateAction<T, ActionType>
| CRUDRemoveAction<T, ActionType>
| CRUDFindOneAction<T, ActionType>
| CRUDFindManyAction<T, ActionType>
| CRUDCreateAction<T, ActionType>
| CRUDEditAction<ActionType>
| CRUDChangeAction<T, ActionType>

View File

@ -1,35 +1,45 @@
import { TFilter, TOnlyDefined } from '@rondo.dev/common' import { Filter, OnlyDefined } from '@rondo.dev/common'
import { IHTTPClient } from '@rondo.dev/http-client' import { HTTPClient } from '@rondo.dev/http-client'
import { IRoutes } from '@rondo.dev/http-types' import { Routes } from '@rondo.dev/http-types'
import { TCRUDAction, TCRUDChangeAction, TCRUDCreateAction, TCRUDEditAction } from './TCRUDAction' import { CRUDAction, CRUDChangeAction, CRUDCreateAction, CRUDEditAction } from './CRUDAction'
import { TCRUDMethod } from './TCRUDMethod' import { CRUDMethod } from './CRUDMethod'
type TAction <T, ActionType extends string, Method extends TCRUDMethod> = type Action <T, ActionType extends string, Method extends CRUDMethod> =
TFilter<TCRUDAction<T, ActionType> , {method: Method, status: 'pending'}> Filter<CRUDAction<T, ActionType> , {method: Method, status: 'pending'}>
export interface ICRUDChangeParams<T> { interface PostParams<Body = unknown, Params = unknown> {
body: Body
params: Params
}
interface GetParams<Query = unknown, Params = unknown> {
query: Query
params: Params
}
export interface CRUDChangeParams<T> {
id?: number id?: number
key: keyof T & string key: keyof T & string
value: string value: string
} }
export class SaveActionCreator< export class SaveActionCreator<
T extends IRoutes, T extends Routes,
Route extends keyof T & string, Route extends keyof T & string,
ActionType extends string, ActionType extends string,
> { > {
constructor( constructor(
readonly http: IHTTPClient<T>, readonly http: HTTPClient<T>,
readonly route: Route, readonly route: Route,
readonly type: ActionType, readonly type: ActionType,
) {} ) {}
save = (params: TOnlyDefined<{ save = (params: OnlyDefined<{
body: T[Route]['post']['body'], body: T[Route]['post']['body']
params: T[Route]['post']['params'], params: T[Route]['post']['params']
}>): TAction<T[Route]['post']['response'], ActionType, 'save'> => { }>): Action<T[Route]['post']['response'], ActionType, 'save'> => {
const p = params as any const p = params as PostParams
return { return {
payload: this.http.post(this.route, p.body, p.params), payload: this.http.post(this.route, p.body, p.params),
type: this.type, type: this.type,
@ -40,22 +50,22 @@ export class SaveActionCreator<
} }
export class FindOneActionCreator< export class FindOneActionCreator<
T extends IRoutes, T extends Routes,
Route extends keyof T & string, Route extends keyof T & string,
ActionType extends string, ActionType extends string,
> { > {
constructor( constructor(
readonly http: IHTTPClient<T>, readonly http: HTTPClient<T>,
readonly route: Route, readonly route: Route,
readonly type: ActionType, readonly type: ActionType,
) {} ) {}
findOne = (params: TOnlyDefined<{ findOne = (params: OnlyDefined<{
query: T[Route]['get']['query'], query: T[Route]['get']['query']
params: T[Route]['get']['params'], params: T[Route]['get']['params']
}>): TAction<T[Route]['get']['response'], ActionType, 'findOne'> => { }>): Action<T[Route]['get']['response'], ActionType, 'findOne'> => {
const p = params as any const p = params as {query: unknown, params: unknown}
return { return {
payload: this.http.get(this.route, p.query, p.params), payload: this.http.get(this.route, p.query, p.params),
type: this.type, type: this.type,
@ -67,22 +77,22 @@ export class FindOneActionCreator<
} }
export class UpdateActionCreator< export class UpdateActionCreator<
T extends IRoutes, T extends Routes,
Route extends keyof T & string, Route extends keyof T & string,
ActionType extends string ActionType extends string
> { > {
constructor( constructor(
readonly http: IHTTPClient<T>, readonly http: HTTPClient<T>,
readonly route: Route, readonly route: Route,
readonly type: ActionType, readonly type: ActionType,
) {} ) {}
update = (params: TOnlyDefined<{ update = (params: OnlyDefined<{
body: T[Route]['put']['body'], body: T[Route]['put']['body']
params: T[Route]['put']['params'], params: T[Route]['put']['params']
}>): TAction<T[Route]['put']['response'], ActionType, 'update'> => { }>): Action<T[Route]['put']['response'], ActionType, 'update'> => {
const p = params as any const p = params as PostParams
return { return {
payload: this.http.put(this.route, p.body, p.params), payload: this.http.put(this.route, p.body, p.params),
type: this.type, type: this.type,
@ -94,22 +104,22 @@ export class UpdateActionCreator<
} }
export class RemoveActionCreator< export class RemoveActionCreator<
T extends IRoutes, T extends Routes,
Route extends keyof T & string, Route extends keyof T & string,
ActionType extends string, ActionType extends string,
> { > {
constructor( constructor(
readonly http: IHTTPClient<T>, readonly http: HTTPClient<T>,
readonly route: Route, readonly route: Route,
readonly type: ActionType, readonly type: ActionType,
) {} ) {}
remove = (params: TOnlyDefined<{ remove = (params: OnlyDefined<{
body: T[Route]['delete']['body'], body: T[Route]['delete']['body']
params: T[Route]['delete']['params'], params: T[Route]['delete']['params']
}>): TAction<T[Route]['delete']['response'], ActionType, 'remove'> => { }>): Action<T[Route]['delete']['response'], ActionType, 'remove'> => {
const p = params as any const p = params as PostParams
return { return {
payload: this.http.delete(this.route, p.body, p.params), payload: this.http.delete(this.route, p.body, p.params),
type: this.type, type: this.type,
@ -120,27 +130,27 @@ export class RemoveActionCreator<
} }
export class FindManyActionCreator< export class FindManyActionCreator<
T extends IRoutes, T extends Routes,
Route extends keyof T & string, Route extends keyof T & string,
ActionType extends string, ActionType extends string,
> { > {
constructor( constructor(
readonly http: IHTTPClient<T>, readonly http: HTTPClient<T>,
readonly route: Route, readonly route: Route,
readonly type: ActionType, readonly type: ActionType,
) {} ) {}
findMany = (params: TOnlyDefined<{ findMany = (params: OnlyDefined<{
query: T[Route]['get']['query'], query: T[Route]['get']['query']
params: T[Route]['get']['params'], params: T[Route]['get']['params']
}>): { }>): {
payload: Promise<T[Route]['get']['response']> payload: Promise<T[Route]['get']['response']>
type: ActionType type: ActionType
status: 'pending', status: 'pending'
method: 'findMany', method: 'findMany'
} => { } => {
const p = params as any const p = params as GetParams
return { return {
payload: this.http.get(this.route, p.query, p.params), payload: this.http.get(this.route, p.query, p.params),
type: this.type, type: this.type,
@ -154,7 +164,7 @@ export class FindManyActionCreator<
export class FormActionCreator<T, ActionType extends string> { export class FormActionCreator<T, ActionType extends string> {
constructor(readonly actionType: ActionType) {} constructor(readonly actionType: ActionType) {}
create = (item: Partial<T>): TCRUDCreateAction<T, ActionType> => { create = (item: Partial<T>): CRUDCreateAction<T, ActionType> => {
return { return {
payload: item, payload: item,
type: this.actionType, type: this.actionType,
@ -162,7 +172,7 @@ export class FormActionCreator<T, ActionType extends string> {
} }
} }
edit = (params: {id: number}): TCRUDEditAction<ActionType> => { edit = (params: {id: number}): CRUDEditAction<ActionType> => {
return { return {
payload: {id: params.id}, payload: {id: params.id},
type: this.actionType, type: this.actionType,
@ -170,8 +180,9 @@ export class FormActionCreator<T, ActionType extends string> {
} }
} }
change = (params: ICRUDChangeParams<T>) change = (
: TCRUDChangeAction<T, ActionType> => { params: CRUDChangeParams<T>,
): CRUDChangeAction<T, ActionType> => {
return { return {
payload: params, payload: params,
type: this.actionType, type: this.actionType,
@ -181,12 +192,12 @@ export class FormActionCreator<T, ActionType extends string> {
} }
export function createCRUDActions< export function createCRUDActions<
T extends IRoutes, T extends Routes,
EntityRoute extends keyof T & string, EntityRoute extends keyof T & string,
ListRoute extends keyof T & string, ListRoute extends keyof T & string,
ActionType extends string, ActionType extends string,
>( >(
http: IHTTPClient<T>, http: HTTPClient<T>,
entityRoute: EntityRoute, entityRoute: EntityRoute,
listRoute: ListRoute, listRoute: ListRoute,
actionType: ActionType, actionType: ActionType,

View File

@ -1,12 +1,12 @@
import React from 'react' import React from 'react'
import {Control, Field, Heading, Icon, Input} from 'bloomer' import {Control, Field, Heading, Icon, Input} from 'bloomer'
import {ICRUDChangeParams} from './CRUDActions' import {CRUDChangeParams} from './CRUDActions'
export type TCRUDFieldType = 'text' | 'password' | 'number' | 'email' | 'tel' export type TCRUDFieldType = 'text' | 'password' | 'number' | 'email' | 'tel'
export interface ICRUDFieldProps<T> { export interface CRUDFieldProps<T> {
id?: number id?: number
onChange<K extends keyof T>(params: ICRUDChangeParams<T>): void onChange<K extends keyof T>(params: CRUDChangeParams<T>): void
Icon?: React.ComponentType Icon?: React.ComponentType
error?: string error?: string
label: string label: string
@ -18,7 +18,7 @@ export interface ICRUDFieldProps<T> {
export type TCRUDErrors<T> = Partial<Record<keyof T & string, string>> export type TCRUDErrors<T> = Partial<Record<keyof T & string, string>>
export interface ICRUDField<T> { export interface CRUDField<T> {
Icon?: React.ComponentType Icon?: React.ComponentType
label: string label: string
placeholder?: string placeholder?: string
@ -26,19 +26,19 @@ export interface ICRUDField<T> {
type: TCRUDFieldType type: TCRUDFieldType
} }
export interface ICRUDFormProps<T> { export interface CRUDFormProps<T> {
errors: TCRUDErrors<T> errors: TCRUDErrors<T>
id?: number id?: number
item?: T item?: T
error: string error: string
submitText: string submitText: string
fields: Array<ICRUDField<T>> fields: Array<CRUDField<T>>
onSubmit: (t: T) => void onSubmit: (t: T) => void
onChange(params: ICRUDChangeParams<T>): void onChange(params: CRUDChangeParams<T>): void
} }
export class CRUDField<T> extends React.PureComponent<ICRUDFieldProps<T>> { export class CRUDField<T> extends React.PureComponent<CRUDFieldProps<T>> {
handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const {onChange} = this.props const {onChange} = this.props
const {value} = e.target const {value} = e.target
@ -71,7 +71,7 @@ export class CRUDField<T> extends React.PureComponent<ICRUDFieldProps<T>> {
} }
} }
export class CRUDForm<T> extends React.PureComponent<ICRUDFormProps<T>> { export class CRUDForm<T> extends React.PureComponent<CRUDFormProps<T>> {
static defaultProps = { static defaultProps = {
errors: {}, errors: {},
} }

View File

@ -3,7 +3,7 @@ import {Button, Panel, PanelHeading, PanelBlock} from 'bloomer'
import {FaPlus, FaEdit, FaTimes} from 'react-icons/fa' import {FaPlus, FaEdit, FaTimes} from 'react-icons/fa'
import {Link} from '../components' import {Link} from '../components'
export interface ICRUDListProps<T> { export interface CRUDListProps<T> {
nameKey: keyof T nameKey: keyof T
editLink?: (item: T) => string editLink?: (item: T) => string
itemIds: ReadonlyArray<number> itemIds: ReadonlyArray<number>
@ -11,37 +11,37 @@ export interface ICRUDListProps<T> {
newLink?: string newLink?: string
onRemove?: (t: T) => void onRemove?: (t: T) => void
title: string title: string
Info?: React.ComponentType<ICRUDItemInfoProps<T>> Info?: React.ComponentType<CRUDItemInfoProps<T>>
RowButtons?: React.ComponentType<ICRUDRowButtons<T>> RowButtons?: React.ComponentType<CRUDRowButtons<T>>
} }
export interface ICRUDRowButtons<T> { export interface CRUDRowButtons<T> {
item: T item: T
} }
export interface ICRUDItemRowProps<T> { export interface CRUDItemRowProps<T> {
Info?: React.ComponentType<ICRUDItemInfoProps<T>> Info?: React.ComponentType<CRUDItemInfoProps<T>>
RowButtons?: React.ComponentType<ICRUDRowButtons<T>> RowButtons?: React.ComponentType<CRUDRowButtons<T>>
nameKey: keyof T nameKey: keyof T
editLink?: string editLink?: string
item: T item: T
onRemove?: (t: T) => void onRemove?: (t: T) => void
} }
export interface ICRUDItemInfoProps<T> { export interface CRUDItemInfoProps<T> {
item: T item: T
nameKey: keyof T nameKey: keyof T
} }
export class CRUDItemInfo<T> export class CRUDItemInfo<T>
extends React.PureComponent<ICRUDItemInfoProps<T>> { extends React.PureComponent<CRUDItemInfoProps<T>> {
render() { render() {
const {item, nameKey} = this.props const {item, nameKey} = this.props
return <span>{item[nameKey]}</span> return <span>{item[nameKey]}</span>
} }
} }
export class CRUDItemRow<T> extends React.PureComponent<ICRUDItemRowProps<T>> { export class CRUDItemRow<T> extends React.PureComponent<CRUDItemRowProps<T>> {
handleRemove = () => { handleRemove = () => {
const {onRemove, item} = this.props const {onRemove, item} = this.props
if (onRemove) { if (onRemove) {
@ -88,7 +88,7 @@ export class CRUDItemRow<T> extends React.PureComponent<ICRUDItemRowProps<T>> {
} }
} }
export class CRUDList<T> extends React.PureComponent<ICRUDListProps<T>> { export class CRUDList<T> extends React.PureComponent<CRUDListProps<T>> {
render() { render() {
const {nameKey, editLink, itemIds, itemsById, newLink, title} = this.props const {nameKey, editLink, itemIds, itemsById, newLink, title} = this.props

View File

@ -0,0 +1,15 @@
export type CRUDAsyncMethod =
'save'
| 'update'
| 'findOne'
| 'findMany'
| 'remove'
export type CRUDSyncMethod =
| 'create'
| 'edit'
| 'change'
export type CRUDMethod =
CRUDAsyncMethod
| CRUDSyncMethod

View File

@ -1,45 +1,44 @@
import {IAction, IResolvedAction} from '@rondo.dev/redux' import { indexBy, without } from '@rondo.dev/common'
import {TCRUDAction} from './TCRUDAction' import { CRUDAction } from './CRUDAction'
import {TCRUDMethod} from './TCRUDMethod' import { CRUDMethod } from './CRUDMethod'
import {indexBy, without, TFilter} from '@rondo.dev/common'
export interface ICRUDEntity { export interface CRUDEntity {
readonly id: number readonly id: number
} }
export interface ICRUDMethodStatus { export interface CRUDMethodStatus {
readonly isLoading: boolean readonly isLoading: boolean
readonly error: string readonly error: string
} }
export interface ICRUDState<T extends ICRUDEntity> { export interface CRUDState<T extends CRUDEntity> {
readonly ids: ReadonlyArray<number> readonly ids: ReadonlyArray<number>
readonly byId: Record<number, T> readonly byId: Record<number, T>
readonly status: ICRUDStatus readonly status: CRUDStatus
readonly form: ICRUDForm<T> readonly form: CRUDForm<T>
} }
export interface ICRUDForm<T extends ICRUDEntity> { export interface CRUDForm<T extends CRUDEntity> {
readonly createItem: Pick<T, Exclude<keyof T, 'id'>>, readonly createItem: Pick<T, Exclude<keyof T, 'id'>>
readonly createErrors: Partial<Record<keyof T, string>> readonly createErrors: Partial<Record<keyof T, string>>
readonly itemsById: Record<number, T> readonly itemsById: Record<number, T>
readonly errorsById: Record<number, Partial<Record<keyof T, string>>> readonly errorsById: Record<number, Partial<Record<keyof T, string>>>
} }
export interface ICRUDStatus { export interface CRUDStatus {
readonly save: ICRUDMethodStatus readonly save: CRUDMethodStatus
readonly update: ICRUDMethodStatus readonly update: CRUDMethodStatus
readonly remove: ICRUDMethodStatus readonly remove: CRUDMethodStatus
readonly findOne: ICRUDMethodStatus readonly findOne: CRUDMethodStatus
readonly findMany: ICRUDMethodStatus readonly findMany: CRUDMethodStatus
} }
export class CRUDReducer< export class CRUDReducer<
T extends ICRUDEntity, T extends CRUDEntity,
ActionType extends string, ActionType extends string,
> { > {
readonly defaultState: ICRUDState<T> readonly defaultState: CRUDState<T>
constructor( constructor(
readonly actionName: ActionType, readonly actionName: ActionType,
@ -67,14 +66,14 @@ export class CRUDReducer<
} }
} }
getDefaultMethodStatus(): ICRUDMethodStatus { getDefaultMethodStatus(): CRUDMethodStatus {
return { return {
error: '', error: '',
isLoading: false, isLoading: false,
} }
} }
protected getSuccessStatus(): ICRUDMethodStatus { protected getSuccessStatus(): CRUDMethodStatus {
return { return {
isLoading: false, isLoading: false,
error: '', error: '',
@ -82,10 +81,10 @@ export class CRUDReducer<
} }
handleRejected = ( handleRejected = (
state: ICRUDState<T>, state: CRUDState<T>,
method: TCRUDMethod, method: CRUDMethod,
error: Error, error: Error,
): ICRUDState<T> => { ): CRUDState<T> => {
return { return {
...state, ...state,
status: { status: {
@ -99,9 +98,9 @@ export class CRUDReducer<
} }
handleLoading = ( handleLoading = (
state: ICRUDState<T>, state: CRUDState<T>,
method: TCRUDMethod, method: CRUDMethod,
): ICRUDState<T> => { ): CRUDState<T> => {
return { return {
...state, ...state,
status: { status: {
@ -114,7 +113,7 @@ export class CRUDReducer<
} }
} }
handleFindOne = (state: ICRUDState<T>, payload: T): ICRUDState<T> => { handleFindOne = (state: CRUDState<T>, payload: T): CRUDState<T> => {
const ids = !state.byId[payload.id] const ids = !state.byId[payload.id]
? [...state.ids, payload.id] ? [...state.ids, payload.id]
: state.ids : state.ids
@ -132,7 +131,7 @@ export class CRUDReducer<
} }
} }
handleSave = (state: ICRUDState<T>, payload: T): ICRUDState<T> => { handleSave = (state: CRUDState<T>, payload: T): CRUDState<T> => {
return { return {
...state, ...state,
ids: [...state.ids, payload.id], ids: [...state.ids, payload.id],
@ -147,7 +146,7 @@ export class CRUDReducer<
} }
} }
handleUpdate = (state: ICRUDState<T>, payload: T): ICRUDState<T> => { handleUpdate = (state: CRUDState<T>, payload: T): CRUDState<T> => {
return { return {
...state, ...state,
byId: { byId: {
@ -160,7 +159,7 @@ export class CRUDReducer<
} }
} }
handleRemove = (state: ICRUDState<T>, payload: T): ICRUDState<T> => { handleRemove = (state: CRUDState<T>, payload: T): CRUDState<T> => {
// FIXME site does not get removed because payload looks different! // FIXME site does not get removed because payload looks different!
return { return {
...state, ...state,
@ -173,11 +172,11 @@ export class CRUDReducer<
} }
} }
handleFindMany = (state: ICRUDState<T>, payload: T[]): ICRUDState<T> => { handleFindMany = (state: CRUDState<T>, payload: T[]): CRUDState<T> => {
return { return {
...state, ...state,
ids: payload.map(item => item.id), ids: payload.map(item => item.id),
byId: indexBy(payload, 'id' as any), byId: indexBy(payload, 'id' as any), // eslint-disable-line
status: { status: {
...state.status, ...state.status,
findMany: this.getSuccessStatus(), findMany: this.getSuccessStatus(),
@ -185,7 +184,7 @@ export class CRUDReducer<
} }
} }
handleCreate = (state: ICRUDState<T>, payload: Partial<T>): ICRUDState<T> => { handleCreate = (state: CRUDState<T>, payload: Partial<T>): CRUDState<T> => {
return { return {
...state, ...state,
form: { form: {
@ -199,7 +198,7 @@ export class CRUDReducer<
} }
} }
handleEdit = (state: ICRUDState<T>, id: number): ICRUDState<T> => { handleEdit = (state: CRUDState<T>, id: number): CRUDState<T> => {
return { return {
...state, ...state,
form: { form: {
@ -216,11 +215,11 @@ export class CRUDReducer<
} }
} }
handleChange = (state: ICRUDState<T>, payload: { handleChange = (state: CRUDState<T>, payload: {
id?: number, id?: number
key: keyof T, key: keyof T
value: string, value: string
}): ICRUDState<T> => { }): CRUDState<T> => {
const {id, key, value} = payload const {id, key, value} = payload
if (!id) { if (!id) {
@ -252,9 +251,9 @@ export class CRUDReducer<
} }
reduce = ( reduce = (
state: ICRUDState<T> | undefined, state: CRUDState<T> | undefined,
action: TCRUDAction<T, ActionType>, action: CRUDAction<T, ActionType>,
): ICRUDState<T> => { ): CRUDState<T> => {
const {defaultState} = this const {defaultState} = this
state = state || defaultState state = state || defaultState
@ -293,6 +292,7 @@ export class CRUDReducer<
case 'findMany': case 'findMany':
return this.handleFindMany(state, action.payload) return this.handleFindMany(state, action.payload)
} }
return state
default: default:
return state return state
} }

View File

@ -1,41 +0,0 @@
import {IAction, TAsyncAction} from '@rondo.dev/redux'
import {TCRUDMethod} from './TCRUDMethod'
// Async actions
export type TCRUDSaveAction<T, ActionType extends string> =
TAsyncAction<T, ActionType> & {method: Extract<TCRUDMethod, 'save'>}
export type TCRUDUpdateAction<T, ActionType extends string> =
TAsyncAction<T, ActionType> & {method: Extract<TCRUDMethod, 'update'>}
export type TCRUDRemoveAction<T, ActionType extends string> =
TAsyncAction<T, ActionType> & {method: Extract<TCRUDMethod, 'remove'>}
export type TCRUDFindOneAction<T, ActionType extends string> =
TAsyncAction<T, ActionType> & {method: Extract<TCRUDMethod, 'findOne'>}
export type TCRUDFindManyAction<T, ActionType extends string> =
TAsyncAction<T[], ActionType> & {method: Extract<TCRUDMethod, 'findMany'>}
// Synchronous actions
export type TCRUDCreateAction<T, ActionType extends string> =
IAction<Partial<T>, ActionType> & {method: Extract<TCRUDMethod, 'create'>}
export type TCRUDEditAction<ActionType extends string> =
IAction<{id: number}, ActionType> & {method: Extract<TCRUDMethod, 'edit'>}
export type TCRUDChangeAction<T, ActionType extends string> =
IAction<{id?: number, key: keyof T, value: string}, ActionType>
& {method: Extract<TCRUDMethod, 'change'>}
export type TCRUDAction<T, ActionType extends string> =
TCRUDSaveAction<T, ActionType>
| TCRUDUpdateAction<T, ActionType>
| TCRUDRemoveAction<T, ActionType>
| TCRUDFindOneAction<T, ActionType>
| TCRUDFindManyAction<T, ActionType>
| TCRUDCreateAction<T, ActionType>
| TCRUDEditAction<ActionType>
| TCRUDChangeAction<T, ActionType>

View File

@ -1,15 +0,0 @@
export type TCRUDAsyncMethod =
'save'
| 'update'
| 'findOne'
| 'findMany'
| 'remove'
export type TCRUDSyncMethod =
| 'create'
| 'edit'
| 'change'
export type TCRUDMethod =
TCRUDAsyncMethod
| TCRUDSyncMethod

View File

@ -2,5 +2,5 @@ export * from './CRUDActions'
export * from './CRUDForm' export * from './CRUDForm'
export * from './CRUDList' export * from './CRUDList'
export * from './CRUDReducer' export * from './CRUDReducer'
export * from './TCRUDAction' export * from './CRUDAction'
export * from './TCRUDMethod' export * from './CRUDMethod'

View File

@ -1,14 +1,14 @@
import React from 'react' import React from 'react'
import {Breadcrumb, BreadcrumbItem} from 'bloomer' import {Breadcrumb, BreadcrumbItem} from 'bloomer'
import {Link} from 'react-router-dom' import {Link} from 'react-router-dom'
import {ICrumbLink} from './ICrumbLink' import {CrumbLink} from './CrumbLink'
export interface ICrumbProps { export interface CrumbProps {
links: ICrumbLink[] links: CrumbLink[]
current: string current: string
} }
export class Crumb extends React.PureComponent<ICrumbProps> { export class Crumb extends React.PureComponent<CrumbProps> {
render() { render() {
return ( return (
<Breadcrumb> <Breadcrumb>

View File

@ -1,4 +1,4 @@
export interface ICrumbLink { export interface CrumbLink {
name: string name: string
to: string to: string
} }

View File

@ -1,18 +1,15 @@
import {TGetAction, IAction} from '@rondo.dev/redux' import {GetAllActions, Action} from '@rondo.dev/redux'
import {ICrumbLink} from './ICrumbLink' import {CrumbLink} from './CrumbLink'
export interface ICrumbs { export interface Crumbs {
links: ICrumbLink[] links: CrumbLink[]
current: string current: string
} }
export type TCrumbsAction = export type CrumbsAction = GetAllActions<CrumbsActions>
IAction<ICrumbs, 'BREADCRUMBS_SET'>
type Action<T extends string> = TGetAction<TCrumbsAction, T>
export class CrumbsActions { export class CrumbsActions {
setCrumbs(breadcrumbs: ICrumbs): Action<'BREADCRUMBS_SET'> { setCrumbs(breadcrumbs: Crumbs): Action<Crumbs, 'BREADCRUMBS_SET'> {
return { return {
payload: breadcrumbs, payload: breadcrumbs,
type: 'BREADCRUMBS_SET', type: 'BREADCRUMBS_SET',

View File

@ -1,15 +1,17 @@
import {ICrumbs, TCrumbsAction} from './CrumbsActions' import {Crumbs, CrumbsAction} from './CrumbsActions'
export interface ICrumbsState extends ICrumbs { export interface CrumbsState extends Crumbs {
} }
const defaultState: ICrumbsState = { const defaultState: CrumbsState = {
links: [], links: [],
current: 'Home', current: 'Home',
} }
export function Crumbs(state = defaultState, action: TCrumbsAction) export function Crumbs(
: ICrumbsState { state = defaultState,
action: CrumbsAction,
): CrumbsState {
switch (action.type) { switch (action.type) {
case 'BREADCRUMBS_SET': case 'BREADCRUMBS_SET':
return { return {

View File

@ -1,32 +1,32 @@
import { History } from 'history'
import React from 'react' import React from 'react'
import {Crumb} from './Crumb' import { match as Match, matchPath } from 'react-router'
import {History, Location} from 'history' import { withHistory } from '../components'
import {ICrumbLink} from './ICrumbLink' import { Crumb } from './Crumb'
import {match as Match, matchPath, withRouter} from 'react-router' import { CrumbLink } from './CrumbLink'
import {withHistory} from '../components'
export interface ICrumbsRoute { export interface CrumbsRoute {
exact?: boolean exact?: boolean
links: ICrumbLink[] links: CrumbLink[]
current: string current: string
} }
export interface IHistoryCrumbsProps { export interface HistoryCrumbsProps {
history: History history: History
routes: Record<string, ICrumbsRoute> routes: Record<string, CrumbsRoute>
} }
export interface IHistoryCrumbsState { export interface HistoryCrumbsState {
links: ICrumbLink[] links: CrumbLink[]
current: string current: string
} }
export const HistoryCrumbs = withHistory( export const HistoryCrumbs = withHistory(
class InnerHistoryCrumbs class InnerHistoryCrumbs
extends React.PureComponent<IHistoryCrumbsProps, IHistoryCrumbsState> { extends React.PureComponent<HistoryCrumbsProps, HistoryCrumbsState> {
unlisten!: () => void unlisten!: () => void
constructor(props: IHistoryCrumbsProps) { constructor(props: HistoryCrumbsProps) {
super(props) super(props)
this.state = { this.state = {
links: [], links: [],
@ -48,7 +48,7 @@ export const HistoryCrumbs = withHistory(
handleChange(path: string) { handleChange(path: string) {
const {routes} = this.props const {routes} = this.props
let found: null | {match: Match<{}>, route: ICrumbsRoute} = null let found: null | {match: Match<{}>, route: CrumbsRoute} = null
Object.keys(routes).some(route => { Object.keys(routes).some(route => {
const match = matchPath(path, { const match = matchPath(path, {

View File

@ -3,4 +3,4 @@ export * from './Crumb'
export * from './CrumbsActions' export * from './CrumbsActions'
export * from './CrumbsReducer' export * from './CrumbsReducer'
export * from './HistoryCrumbs' export * from './HistoryCrumbs'
export * from './ICrumbLink' export * from './CrumbLink'

View File

@ -1,17 +1,18 @@
import {TGetAction, TAsyncAction, IAction, PendingAction} from '@rondo.dev/redux' import { APIDef, Credentials, NewUser, UserProfile } from '@rondo.dev/common'
import {IAPIDef, ICredentials, INewUser, IUser} from '@rondo.dev/common' import { HTTPClient } from '@rondo.dev/http-client'
import {IHTTPClient} from '@rondo.dev/http-client' import { Action, AsyncAction, createPendingAction, TGetAction } from '@rondo.dev/redux'
export type TLoginAction = export type TLoginAction =
TAsyncAction<IUser, 'LOGIN'> AsyncAction<UserProfile, 'LOGIN'>
| TAsyncAction<unknown, 'LOGIN_LOGOUT'> | AsyncAction<unknown, 'LOGIN_LOGOUT'>
| TAsyncAction<IUser, 'LOGIN_REGISTER'> | AsyncAction<UserProfile, 'LOGIN_REGISTER'>
| IAction<{redirectTo: string}, 'LOGIN_REDIRECT_SET'> | Action<{redirectTo: string}, 'LOGIN_REDIRECT_SET'>
type TAction<T extends string> = TGetAction<TLoginAction, T> type TAction<T extends string> = TGetAction<TLoginAction, T>
export const setRedirectTo = (redirectTo: string) export const setRedirectTo = (
: TAction<'LOGIN_REDIRECT_SET'> => { redirectTo: string,
): Action<{redirectTo: string}, 'LOGIN_REDIRECT_SET'> => {
return { return {
payload: {redirectTo}, payload: {redirectTo},
type: 'LOGIN_REDIRECT_SET', type: 'LOGIN_REDIRECT_SET',
@ -19,24 +20,24 @@ export const setRedirectTo = (redirectTo: string)
} }
export class LoginActions { export class LoginActions {
constructor(protected readonly http: IHTTPClient<IAPIDef>) {} constructor(protected readonly http: HTTPClient<APIDef>) {}
logIn = (credentials: ICredentials) => { logIn = (credentials: Credentials) => {
return new PendingAction( return createPendingAction(
this.http.post('/auth/login', credentials), this.http.post('/auth/login', credentials),
'LOGIN', 'LOGIN',
) )
} }
logOut = () => { logOut = () => {
return new PendingAction( return createPendingAction(
this.http.get('/auth/logout'), this.http.get('/auth/logout'),
'LOGIN_LOGOUT', 'LOGIN_LOGOUT',
) )
} }
register = (profile: INewUser) => { register = (profile: NewUser) => {
return new PendingAction( return createPendingAction(
this.http.post('/auth/register', profile), this.http.post('/auth/register', profile),
'LOGIN_REGISTER', 'LOGIN_REGISTER',
) )

View File

@ -1,22 +1,22 @@
import React from 'react' import React from 'react'
import {FaUser, FaLock} from 'react-icons/fa' import {FaUser, FaLock} from 'react-icons/fa'
import {ICredentials, IUser} from '@rondo.dev/common' import {Credentials, UserProfile} from '@rondo.dev/common'
import {Input} from '../components/Input' import {Input} from '../components/Input'
import {Link} from 'react-router-dom' import {Link} from 'react-router-dom'
import {Redirect} from '../components/Redirect' import {Redirect} from '../components/Redirect'
export interface ILoginFormProps { export interface LoginFormProps {
error?: string error?: string
onSubmit: () => void onSubmit: () => void
onChange: (name: string, value: string) => void onChange: (name: string, value: string) => void
data: ICredentials data: Credentials
user?: IUser user?: UserProfile
redirectTo: string 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<LoginFormProps> {
render() { render() {
if (this.props.user) { if (this.props.user) {
return <Redirect to={this.props.redirectTo} /> return <Redirect to={this.props.redirectTo} />

View File

@ -1,14 +1,14 @@
import {IUser} from '@rondo.dev/common' import {UserProfile} from '@rondo.dev/common'
import {TLoginAction} from './LoginActions' import {TLoginAction} from './LoginActions'
export interface ILoginState { export interface LoginState {
readonly error: string readonly error: string
readonly isLoading: boolean readonly isLoading: boolean
readonly user?: IUser readonly user?: UserProfile
readonly redirectTo: string readonly redirectTo: string
} }
const defaultState: ILoginState = { const defaultState: LoginState = {
error: '', error: '',
isLoading: false, isLoading: false,
user: undefined, user: undefined,
@ -18,7 +18,7 @@ const defaultState: ILoginState = {
export function Login( export function Login(
state = defaultState, state = defaultState,
action: TLoginAction, action: TLoginAction,
): ILoginState { ): LoginState {
switch (action.type) { switch (action.type) {
// sync actions // sync actions
case 'LOGIN_REDIRECT_SET': case 'LOGIN_REDIRECT_SET':

View File

@ -1,20 +1,20 @@
import { NewUser, UserProfile } from '@rondo.dev/common'
import React from 'react' import React from 'react'
import {FaEnvelope, FaUser, FaLock} from 'react-icons/fa' import { FaEnvelope, FaLock, FaUser } from 'react-icons/fa'
import {INewUser, IUser} from '@rondo.dev/common' import { Link } from 'react-router-dom'
import {Input} from '../components/Input' import { Input } from '../components/Input'
import {Link} from 'react-router-dom' import { Redirect } from '../components/Redirect'
import {Redirect} from '../components/Redirect'
export interface IRegisterFormProps { export interface RegisterFormProps {
error?: string error?: string
onSubmit: () => void onSubmit: () => void
onChange: (name: string, value: string) => void onChange: (name: string, value: string) => void
data: INewUser data: NewUser
user?: IUser user?: UserProfile
redirectTo: string redirectTo: string
} }
export class RegisterForm extends React.PureComponent<IRegisterFormProps> { export class RegisterForm extends React.PureComponent<RegisterFormProps> {
render() { render() {
if (this.props.user) { if (this.props.user) {
return <Redirect to={this.props.redirectTo} /> return <Redirect to={this.props.redirectTo} />

View File

@ -1,11 +1,10 @@
import { IAPIDef } from '@rondo.dev/common' import { APIDef } from '@rondo.dev/common'
import { HTTPClientMock } from '@rondo.dev/http-client' import { HTTPClientMock } from '@rondo.dev/http-client'
import { getError } from '@rondo.dev/test-utils' import { getError } from '@rondo.dev/test-utils'
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom'
import T from 'react-dom/test-utils' import T from 'react-dom/test-utils'
import { MemoryRouter } from 'react-router-dom' import { MemoryRouter } from 'react-router-dom'
import { TestUtils } from '../test-utils' import { TestContainer, TestUtils } from '../test-utils'
import * as Feature from './' import * as Feature from './'
import { configureLogin } from './configureLogin' import { configureLogin } from './configureLogin'
@ -13,7 +12,7 @@ const test = new TestUtils()
describe('configureLogin', () => { describe('configureLogin', () => {
const http = new HTTPClientMock<IAPIDef>() const http = new HTTPClientMock<APIDef>()
const loginActions = new Feature.LoginActions(http) const loginActions = new Feature.LoginActions(http)
const createTestProvider = () => test.withProvider({ const createTestProvider = () => test.withProvider({
@ -40,7 +39,7 @@ describe('configureLogin', () => {
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 let component: TestContainer
beforeEach(() => { beforeEach(() => {
http.mockAdd({ http.mockAdd({
method: 'post', method: 'post',
@ -72,8 +71,7 @@ describe('configureLogin', () => {
}) })
expect(onSuccess.mock.calls.length).toBe(1) expect(onSuccess.mock.calls.length).toBe(1)
// TODO test clear username/password // TODO test clear username/password
node = ReactDOM.findDOMNode(component) as Element expect(component.ref.current!.innerHTML).toMatch(/<a href="\/">/)
expect(node.innerHTML).toMatch(/<a href="\/">/)
}) })
it('sets the error message on failure', async () => { it('sets the error message on failure', async () => {

View File

@ -1,13 +1,12 @@
import { ICredentials } from '@rondo.dev/common' import { Credentials } from '@rondo.dev/common'
import { TStateSelector, pack } from '@rondo.dev/redux' import { pack, TStateSelector } from '@rondo.dev/redux'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { Connector } from '../redux/Connector'
import { LoginActions } from './LoginActions' import { LoginActions } from './LoginActions'
import { LoginForm } from './LoginForm' import { LoginForm } from './LoginForm'
import { ILoginState } from './LoginReducer' import { ILoginState } from './LoginReducer'
import { withForm } from './withForm' import { withForm } from './withForm'
const defaultCredentials: ICredentials = { const defaultCredentials: Credentials = {
username: '', username: '',
password: '', password: '',
} }

View File

@ -1,6 +1,6 @@
import {Action} from './Action' import {Action} from './Action'
export type TGetAction<ActionTypes, T extends string> = export type GetAction<ActionTypes, T extends string> =
ActionTypes extends Action<infer U, T> ActionTypes extends Action<infer U, T>
? ActionTypes ? ActionTypes
: never : never

View File

@ -0,0 +1,5 @@
export type GetAllActions<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => infer R
? R
: never
}[keyof T]

View File

@ -1,9 +1,10 @@
export * from './Action' export * from './Action'
export * from './createPendingAction'
export * from './RejectedAction'
export * from './ResolvedAction'
export * from './PendingAction'
export * from './AsyncAction' export * from './AsyncAction'
export * from './createPendingAction'
export * from './GetAction' export * from './GetAction'
export * from './GetAllActions'
export * from './GetPendingAction' export * from './GetPendingAction'
export * from './GetResolvedAction' export * from './GetResolvedAction'
export * from './PendingAction'
export * from './RejectedAction'
export * from './ResolvedAction'