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/no-non-null-assertion': off
'@typescript-eslint/no-use-before-define': off
'@typescript-eslint/no-empty-interface': off
'@typescript-eslint/no-explicit-any':
- warn
- ignoreRestArgs: true
overrides:
- files:
- '*.test.ts'

View File

@ -2,7 +2,7 @@ import React from 'react'
import {Control, Field, Input as I, Heading} from 'bloomer'
import {IconType} from 'react-icons'
export interface IInputProps {
export interface InputProps {
name: string
type: 'text' | 'password' | 'hidden' | 'submit' | 'email'
value?: string
@ -14,7 +14,7 @@ export interface IInputProps {
required?: boolean
}
export class Input extends React.PureComponent<IInputProps> {
export class Input extends React.PureComponent<InputProps> {
handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (this.props.onChange) {
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 {History, Location} from 'history'
import {IWithRouterProps} from './IWithRouterProps'
import {Link as RouterLink, LinkProps} from 'react-router-dom'
import {URLFormatter} from '@rondo.dev/http-client'
import {withRouter} from 'react-router'
import { withRouter } from 'react-router'
import { Link as RouterLink } from 'react-router-dom'
import { WithRouterProps } from './WithRouterProps'
export interface ILinkProps
extends IWithRouterProps<Record<string, string>> {
export interface LinkProps
extends WithRouterProps<Record<string, string>> {
readonly className?: string
readonly to: string
}
class ContextLink extends React.PureComponent<ILinkProps> {
class ContextLink extends React.PureComponent<LinkProps> {
protected readonly urlFormatter = new URLFormatter()
render() {
const {
className,
history,
location,
match,
to,
children,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,69 +1,69 @@
import { HTTPClientMock } from '@rondo.dev/http-client'
import { TMethod } from '@rondo.dev/http-types'
import { IPendingAction } from '@rondo.dev/redux'
import { Method } from '@rondo.dev/http-types'
import { PendingAction } from '@rondo.dev/redux'
import { getError } from '@rondo.dev/test-utils'
import { AnyAction } from 'redux'
import { TestUtils } from '../test-utils'
import { CRUDReducer, TCRUDAsyncMethod } from './'
import { CRUDReducer, CRUDAsyncMethod } from './'
import { createCRUDActions } from './CRUDActions'
describe('CRUD', () => {
interface ITwo {
interface Two {
id: number
name: string
}
interface ITwoCreateBody {
interface TwoCreateBody {
name: string
}
interface ITwoListParams {
interface TwoListParams {
oneId: number
}
interface ITwoSpecificParams {
interface TwoSpecificParams {
oneId: number
twoId: number
}
interface ITestAPI {
interface TestAPI {
'/one/:oneId/two/:twoId': {
get: {
params: ITwoSpecificParams
response: ITwo
params: TwoSpecificParams
response: Two
}
put: {
params: ITwoSpecificParams
body: ITwoCreateBody
response: ITwo
params: TwoSpecificParams
body: TwoCreateBody
response: Two
}
delete: {
params: ITwoSpecificParams
params: TwoSpecificParams
response: {id: number} // TODO return ITwoSpecificParams
}
}
'/one/:oneId/two': {
get: {
params: ITwoListParams
response: ITwo[]
params: TwoListParams
response: Two[]
}
post: {
params: ITwoListParams
body: ITwoCreateBody
response: ITwo
params: TwoListParams
body: TwoCreateBody
response: Two
}
}
}
const http = new HTTPClientMock<ITestAPI>()
const http = new HTTPClientMock<TestAPI>()
const actions = createCRUDActions(
http,
'/one/:oneId/two/:twoId',
'/one/:oneId/two',
'TEST',
)
const crudReducer = new CRUDReducer<ITwo, 'TEST'>('TEST', {name: ''})
const crudReducer = new CRUDReducer<Two, 'TEST'>('TEST', {name: ''})
const Crud = crudReducer.reduce
const test = new TestUtils()
@ -101,8 +101,8 @@ describe('CRUD', () => {
})
function dispatch(
method: TCRUDAsyncMethod,
action: IPendingAction<unknown, string>,
method: CRUDAsyncMethod,
action: PendingAction<unknown, string>,
) {
store.dispatch(action)
expect(store.getState().Crud.status[method].isLoading).toBe(true)
@ -110,13 +110,13 @@ describe('CRUD', () => {
return action
}
function getUrl(method: TCRUDAsyncMethod) {
function getUrl(method: CRUDAsyncMethod) {
return method === 'save' || method === 'findMany'
? '/one/1/two'
: '/one/1/two/2'
}
function getHTTPMethod(method: TCRUDAsyncMethod): TMethod {
function getHTTPMethod(method: CRUDAsyncMethod): Method {
switch (method) {
case 'save':
return 'post'
@ -132,7 +132,7 @@ describe('CRUD', () => {
describe('Promise rejections', () => {
const testCases: Array<{
method: TCRUDAsyncMethod
method: CRUDAsyncMethod
params: any
}> = [{
method: 'findOne',
@ -204,7 +204,7 @@ describe('CRUD', () => {
const entity = {id: 100, name: 'test'}
const testCases: Array<{
method: TCRUDAsyncMethod,
method: CRUDAsyncMethod
params: any
body?: 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 { IHTTPClient } from '@rondo.dev/http-client'
import { IRoutes } from '@rondo.dev/http-types'
import { TCRUDAction, TCRUDChangeAction, TCRUDCreateAction, TCRUDEditAction } from './TCRUDAction'
import { TCRUDMethod } from './TCRUDMethod'
import { Filter, OnlyDefined } from '@rondo.dev/common'
import { HTTPClient } from '@rondo.dev/http-client'
import { Routes } from '@rondo.dev/http-types'
import { CRUDAction, CRUDChangeAction, CRUDCreateAction, CRUDEditAction } from './CRUDAction'
import { CRUDMethod } from './CRUDMethod'
type TAction <T, ActionType extends string, Method extends TCRUDMethod> =
TFilter<TCRUDAction<T, ActionType> , {method: Method, status: 'pending'}>
type Action <T, ActionType extends string, Method extends CRUDMethod> =
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
key: keyof T & string
value: string
}
export class SaveActionCreator<
T extends IRoutes,
T extends Routes,
Route extends keyof T & string,
ActionType extends string,
> {
constructor(
readonly http: IHTTPClient<T>,
readonly http: HTTPClient<T>,
readonly route: Route,
readonly type: ActionType,
) {}
save = (params: TOnlyDefined<{
body: T[Route]['post']['body'],
params: T[Route]['post']['params'],
}>): TAction<T[Route]['post']['response'], ActionType, 'save'> => {
const p = params as any
save = (params: OnlyDefined<{
body: T[Route]['post']['body']
params: T[Route]['post']['params']
}>): Action<T[Route]['post']['response'], ActionType, 'save'> => {
const p = params as PostParams
return {
payload: this.http.post(this.route, p.body, p.params),
type: this.type,
@ -40,22 +50,22 @@ export class SaveActionCreator<
}
export class FindOneActionCreator<
T extends IRoutes,
T extends Routes,
Route extends keyof T & string,
ActionType extends string,
> {
constructor(
readonly http: IHTTPClient<T>,
readonly http: HTTPClient<T>,
readonly route: Route,
readonly type: ActionType,
) {}
findOne = (params: TOnlyDefined<{
query: T[Route]['get']['query'],
params: T[Route]['get']['params'],
}>): TAction<T[Route]['get']['response'], ActionType, 'findOne'> => {
const p = params as any
findOne = (params: OnlyDefined<{
query: T[Route]['get']['query']
params: T[Route]['get']['params']
}>): Action<T[Route]['get']['response'], ActionType, 'findOne'> => {
const p = params as {query: unknown, params: unknown}
return {
payload: this.http.get(this.route, p.query, p.params),
type: this.type,
@ -67,22 +77,22 @@ export class FindOneActionCreator<
}
export class UpdateActionCreator<
T extends IRoutes,
T extends Routes,
Route extends keyof T & string,
ActionType extends string
> {
constructor(
readonly http: IHTTPClient<T>,
readonly http: HTTPClient<T>,
readonly route: Route,
readonly type: ActionType,
) {}
update = (params: TOnlyDefined<{
body: T[Route]['put']['body'],
params: T[Route]['put']['params'],
}>): TAction<T[Route]['put']['response'], ActionType, 'update'> => {
const p = params as any
update = (params: OnlyDefined<{
body: T[Route]['put']['body']
params: T[Route]['put']['params']
}>): Action<T[Route]['put']['response'], ActionType, 'update'> => {
const p = params as PostParams
return {
payload: this.http.put(this.route, p.body, p.params),
type: this.type,
@ -94,22 +104,22 @@ export class UpdateActionCreator<
}
export class RemoveActionCreator<
T extends IRoutes,
T extends Routes,
Route extends keyof T & string,
ActionType extends string,
> {
constructor(
readonly http: IHTTPClient<T>,
readonly http: HTTPClient<T>,
readonly route: Route,
readonly type: ActionType,
) {}
remove = (params: TOnlyDefined<{
body: T[Route]['delete']['body'],
params: T[Route]['delete']['params'],
}>): TAction<T[Route]['delete']['response'], ActionType, 'remove'> => {
const p = params as any
remove = (params: OnlyDefined<{
body: T[Route]['delete']['body']
params: T[Route]['delete']['params']
}>): Action<T[Route]['delete']['response'], ActionType, 'remove'> => {
const p = params as PostParams
return {
payload: this.http.delete(this.route, p.body, p.params),
type: this.type,
@ -120,27 +130,27 @@ export class RemoveActionCreator<
}
export class FindManyActionCreator<
T extends IRoutes,
T extends Routes,
Route extends keyof T & string,
ActionType extends string,
> {
constructor(
readonly http: IHTTPClient<T>,
readonly http: HTTPClient<T>,
readonly route: Route,
readonly type: ActionType,
) {}
findMany = (params: TOnlyDefined<{
query: T[Route]['get']['query'],
params: T[Route]['get']['params'],
findMany = (params: OnlyDefined<{
query: T[Route]['get']['query']
params: T[Route]['get']['params']
}>): {
payload: Promise<T[Route]['get']['response']>
type: ActionType
status: 'pending',
method: 'findMany',
status: 'pending'
method: 'findMany'
} => {
const p = params as any
const p = params as GetParams
return {
payload: this.http.get(this.route, p.query, p.params),
type: this.type,
@ -154,7 +164,7 @@ export class FindManyActionCreator<
export class FormActionCreator<T, ActionType extends string> {
constructor(readonly actionType: ActionType) {}
create = (item: Partial<T>): TCRUDCreateAction<T, ActionType> => {
create = (item: Partial<T>): CRUDCreateAction<T, ActionType> => {
return {
payload: item,
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 {
payload: {id: params.id},
type: this.actionType,
@ -170,8 +180,9 @@ export class FormActionCreator<T, ActionType extends string> {
}
}
change = (params: ICRUDChangeParams<T>)
: TCRUDChangeAction<T, ActionType> => {
change = (
params: CRUDChangeParams<T>,
): CRUDChangeAction<T, ActionType> => {
return {
payload: params,
type: this.actionType,
@ -181,12 +192,12 @@ export class FormActionCreator<T, ActionType extends string> {
}
export function createCRUDActions<
T extends IRoutes,
T extends Routes,
EntityRoute extends keyof T & string,
ListRoute extends keyof T & string,
ActionType extends string,
>(
http: IHTTPClient<T>,
http: HTTPClient<T>,
entityRoute: EntityRoute,
listRoute: ListRoute,
actionType: ActionType,

View File

@ -1,12 +1,12 @@
import React from 'react'
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 interface ICRUDFieldProps<T> {
export interface CRUDFieldProps<T> {
id?: number
onChange<K extends keyof T>(params: ICRUDChangeParams<T>): void
onChange<K extends keyof T>(params: CRUDChangeParams<T>): void
Icon?: React.ComponentType
error?: string
label: string
@ -18,7 +18,7 @@ export interface ICRUDFieldProps<T> {
export type TCRUDErrors<T> = Partial<Record<keyof T & string, string>>
export interface ICRUDField<T> {
export interface CRUDField<T> {
Icon?: React.ComponentType
label: string
placeholder?: string
@ -26,19 +26,19 @@ export interface ICRUDField<T> {
type: TCRUDFieldType
}
export interface ICRUDFormProps<T> {
export interface CRUDFormProps<T> {
errors: TCRUDErrors<T>
id?: number
item?: T
error: string
submitText: string
fields: Array<ICRUDField<T>>
fields: Array<CRUDField<T>>
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>) => {
const {onChange} = this.props
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 = {
errors: {},
}

View File

@ -3,7 +3,7 @@ import {Button, Panel, PanelHeading, PanelBlock} from 'bloomer'
import {FaPlus, FaEdit, FaTimes} from 'react-icons/fa'
import {Link} from '../components'
export interface ICRUDListProps<T> {
export interface CRUDListProps<T> {
nameKey: keyof T
editLink?: (item: T) => string
itemIds: ReadonlyArray<number>
@ -11,37 +11,37 @@ export interface ICRUDListProps<T> {
newLink?: string
onRemove?: (t: T) => void
title: string
Info?: React.ComponentType<ICRUDItemInfoProps<T>>
RowButtons?: React.ComponentType<ICRUDRowButtons<T>>
Info?: React.ComponentType<CRUDItemInfoProps<T>>
RowButtons?: React.ComponentType<CRUDRowButtons<T>>
}
export interface ICRUDRowButtons<T> {
export interface CRUDRowButtons<T> {
item: T
}
export interface ICRUDItemRowProps<T> {
Info?: React.ComponentType<ICRUDItemInfoProps<T>>
RowButtons?: React.ComponentType<ICRUDRowButtons<T>>
export interface CRUDItemRowProps<T> {
Info?: React.ComponentType<CRUDItemInfoProps<T>>
RowButtons?: React.ComponentType<CRUDRowButtons<T>>
nameKey: keyof T
editLink?: string
item: T
onRemove?: (t: T) => void
}
export interface ICRUDItemInfoProps<T> {
export interface CRUDItemInfoProps<T> {
item: T
nameKey: keyof T
}
export class CRUDItemInfo<T>
extends React.PureComponent<ICRUDItemInfoProps<T>> {
extends React.PureComponent<CRUDItemInfoProps<T>> {
render() {
const {item, nameKey} = this.props
return <span>{item[nameKey]}</span>
}
}
export class CRUDItemRow<T> extends React.PureComponent<ICRUDItemRowProps<T>> {
export class CRUDItemRow<T> extends React.PureComponent<CRUDItemRowProps<T>> {
handleRemove = () => {
const {onRemove, item} = this.props
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() {
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 {TCRUDAction} from './TCRUDAction'
import {TCRUDMethod} from './TCRUDMethod'
import {indexBy, without, TFilter} from '@rondo.dev/common'
import { indexBy, without } from '@rondo.dev/common'
import { CRUDAction } from './CRUDAction'
import { CRUDMethod } from './CRUDMethod'
export interface ICRUDEntity {
export interface CRUDEntity {
readonly id: number
}
export interface ICRUDMethodStatus {
export interface CRUDMethodStatus {
readonly isLoading: boolean
readonly error: string
}
export interface ICRUDState<T extends ICRUDEntity> {
export interface CRUDState<T extends CRUDEntity> {
readonly ids: ReadonlyArray<number>
readonly byId: Record<number, T>
readonly status: ICRUDStatus
readonly form: ICRUDForm<T>
readonly status: CRUDStatus
readonly form: CRUDForm<T>
}
export interface ICRUDForm<T extends ICRUDEntity> {
readonly createItem: Pick<T, Exclude<keyof T, 'id'>>,
export interface CRUDForm<T extends CRUDEntity> {
readonly createItem: Pick<T, Exclude<keyof T, 'id'>>
readonly createErrors: Partial<Record<keyof T, string>>
readonly itemsById: Record<number, T>
readonly errorsById: Record<number, Partial<Record<keyof T, string>>>
}
export interface ICRUDStatus {
readonly save: ICRUDMethodStatus
readonly update: ICRUDMethodStatus
readonly remove: ICRUDMethodStatus
readonly findOne: ICRUDMethodStatus
readonly findMany: ICRUDMethodStatus
export interface CRUDStatus {
readonly save: CRUDMethodStatus
readonly update: CRUDMethodStatus
readonly remove: CRUDMethodStatus
readonly findOne: CRUDMethodStatus
readonly findMany: CRUDMethodStatus
}
export class CRUDReducer<
T extends ICRUDEntity,
T extends CRUDEntity,
ActionType extends string,
> {
readonly defaultState: ICRUDState<T>
readonly defaultState: CRUDState<T>
constructor(
readonly actionName: ActionType,
@ -67,14 +66,14 @@ export class CRUDReducer<
}
}
getDefaultMethodStatus(): ICRUDMethodStatus {
getDefaultMethodStatus(): CRUDMethodStatus {
return {
error: '',
isLoading: false,
}
}
protected getSuccessStatus(): ICRUDMethodStatus {
protected getSuccessStatus(): CRUDMethodStatus {
return {
isLoading: false,
error: '',
@ -82,10 +81,10 @@ export class CRUDReducer<
}
handleRejected = (
state: ICRUDState<T>,
method: TCRUDMethod,
state: CRUDState<T>,
method: CRUDMethod,
error: Error,
): ICRUDState<T> => {
): CRUDState<T> => {
return {
...state,
status: {
@ -99,9 +98,9 @@ export class CRUDReducer<
}
handleLoading = (
state: ICRUDState<T>,
method: TCRUDMethod,
): ICRUDState<T> => {
state: CRUDState<T>,
method: CRUDMethod,
): CRUDState<T> => {
return {
...state,
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]
? [...state.ids, payload.id]
: 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 {
...state,
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 {
...state,
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!
return {
...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 {
...state,
ids: payload.map(item => item.id),
byId: indexBy(payload, 'id' as any),
byId: indexBy(payload, 'id' as any), // eslint-disable-line
status: {
...state.status,
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 {
...state,
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 {
...state,
form: {
@ -216,11 +215,11 @@ export class CRUDReducer<
}
}
handleChange = (state: ICRUDState<T>, payload: {
id?: number,
key: keyof T,
value: string,
}): ICRUDState<T> => {
handleChange = (state: CRUDState<T>, payload: {
id?: number
key: keyof T
value: string
}): CRUDState<T> => {
const {id, key, value} = payload
if (!id) {
@ -252,9 +251,9 @@ export class CRUDReducer<
}
reduce = (
state: ICRUDState<T> | undefined,
action: TCRUDAction<T, ActionType>,
): ICRUDState<T> => {
state: CRUDState<T> | undefined,
action: CRUDAction<T, ActionType>,
): CRUDState<T> => {
const {defaultState} = this
state = state || defaultState
@ -293,6 +292,7 @@ export class CRUDReducer<
case 'findMany':
return this.handleFindMany(state, action.payload)
}
return state
default:
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 './CRUDList'
export * from './CRUDReducer'
export * from './TCRUDAction'
export * from './TCRUDMethod'
export * from './CRUDAction'
export * from './CRUDMethod'

View File

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

View File

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

View File

@ -1,18 +1,15 @@
import {TGetAction, IAction} from '@rondo.dev/redux'
import {ICrumbLink} from './ICrumbLink'
import {GetAllActions, Action} from '@rondo.dev/redux'
import {CrumbLink} from './CrumbLink'
export interface ICrumbs {
links: ICrumbLink[]
export interface Crumbs {
links: CrumbLink[]
current: string
}
export type TCrumbsAction =
IAction<ICrumbs, 'BREADCRUMBS_SET'>
type Action<T extends string> = TGetAction<TCrumbsAction, T>
export type CrumbsAction = GetAllActions<CrumbsActions>
export class CrumbsActions {
setCrumbs(breadcrumbs: ICrumbs): Action<'BREADCRUMBS_SET'> {
setCrumbs(breadcrumbs: Crumbs): Action<Crumbs, 'BREADCRUMBS_SET'> {
return {
payload: breadcrumbs,
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: [],
current: 'Home',
}
export function Crumbs(state = defaultState, action: TCrumbsAction)
: ICrumbsState {
export function Crumbs(
state = defaultState,
action: CrumbsAction,
): CrumbsState {
switch (action.type) {
case 'BREADCRUMBS_SET':
return {

View File

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

View File

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

View File

@ -1,22 +1,22 @@
import React from 'react'
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 {Link} from 'react-router-dom'
import {Redirect} from '../components/Redirect'
export interface ILoginFormProps {
export interface LoginFormProps {
error?: string
onSubmit: () => void
onChange: (name: string, value: string) => void
data: ICredentials
user?: IUser
data: Credentials
user?: UserProfile
redirectTo: string
}
// TODO maybe replace this with Formik, which is recommended in React docs
// https://jaredpalmer.com/formik/docs/overview
export class LoginForm extends React.PureComponent<ILoginFormProps> {
export class LoginForm extends React.PureComponent<LoginFormProps> {
render() {
if (this.props.user) {
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'
export interface ILoginState {
export interface LoginState {
readonly error: string
readonly isLoading: boolean
readonly user?: IUser
readonly user?: UserProfile
readonly redirectTo: string
}
const defaultState: ILoginState = {
const defaultState: LoginState = {
error: '',
isLoading: false,
user: undefined,
@ -18,7 +18,7 @@ const defaultState: ILoginState = {
export function Login(
state = defaultState,
action: TLoginAction,
): ILoginState {
): LoginState {
switch (action.type) {
// sync actions
case 'LOGIN_REDIRECT_SET':

View File

@ -1,20 +1,20 @@
import { NewUser, UserProfile } from '@rondo.dev/common'
import React from 'react'
import {FaEnvelope, FaUser, FaLock} from 'react-icons/fa'
import {INewUser, IUser} from '@rondo.dev/common'
import {Input} from '../components/Input'
import {Link} from 'react-router-dom'
import {Redirect} from '../components/Redirect'
import { FaEnvelope, FaLock, FaUser } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { Input } from '../components/Input'
import { Redirect } from '../components/Redirect'
export interface IRegisterFormProps {
export interface RegisterFormProps {
error?: string
onSubmit: () => void
onChange: (name: string, value: string) => void
data: INewUser
user?: IUser
data: NewUser
user?: UserProfile
redirectTo: string
}
export class RegisterForm extends React.PureComponent<IRegisterFormProps> {
export class RegisterForm extends React.PureComponent<RegisterFormProps> {
render() {
if (this.props.user) {
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 { getError } from '@rondo.dev/test-utils'
import React from 'react'
import ReactDOM from 'react-dom'
import T from 'react-dom/test-utils'
import { MemoryRouter } from 'react-router-dom'
import { TestUtils } from '../test-utils'
import { TestContainer, TestUtils } from '../test-utils'
import * as Feature from './'
import { configureLogin } from './configureLogin'
@ -13,7 +12,7 @@ const test = new TestUtils()
describe('configureLogin', () => {
const http = new HTTPClientMock<IAPIDef>()
const http = new HTTPClientMock<APIDef>()
const loginActions = new Feature.LoginActions(http)
const createTestProvider = () => test.withProvider({
@ -40,7 +39,7 @@ describe('configureLogin', () => {
const data = {username: 'user', password: 'pass'}
const onSuccess = jest.fn()
let node: Element
let component: React.Component
let component: TestContainer
beforeEach(() => {
http.mockAdd({
method: 'post',
@ -72,8 +71,7 @@ describe('configureLogin', () => {
})
expect(onSuccess.mock.calls.length).toBe(1)
// TODO test clear username/password
node = ReactDOM.findDOMNode(component) as Element
expect(node.innerHTML).toMatch(/<a href="\/">/)
expect(component.ref.current!.innerHTML).toMatch(/<a href="\/">/)
})
it('sets the error message on failure', async () => {

View File

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

View File

@ -1,6 +1,6 @@
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
: 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 './createPendingAction'
export * from './RejectedAction'
export * from './ResolvedAction'
export * from './PendingAction'
export * from './AsyncAction'
export * from './createPendingAction'
export * from './GetAction'
export * from './GetAllActions'
export * from './GetPendingAction'
export * from './GetResolvedAction'
export * from './PendingAction'
export * from './RejectedAction'
export * from './ResolvedAction'