Add sync actions to CRUDActions
This commit is contained in:
parent
71f7687ab9
commit
0b1cd203c3
@ -7,6 +7,7 @@ import {withRouter} from 'react-router'
|
||||
|
||||
interface ILinkProps
|
||||
extends IWithRouterProps<Record<string, string>> {
|
||||
readonly className?: string
|
||||
readonly to: string
|
||||
}
|
||||
|
||||
@ -15,6 +16,7 @@ class ContextLink extends React.PureComponent<ILinkProps> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
history,
|
||||
location,
|
||||
match,
|
||||
@ -25,7 +27,7 @@ class ContextLink extends React.PureComponent<ILinkProps> {
|
||||
const href = this.urlFormatter.format(to, match.params)
|
||||
|
||||
return (
|
||||
<RouterLink to={href}>
|
||||
<RouterLink className={className} to={href}>
|
||||
{children}
|
||||
</RouterLink>
|
||||
)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {createCRUDActions} from './CRUDActions'
|
||||
import React from 'react'
|
||||
import {AnyAction} from 'redux'
|
||||
import {CRUDReducer, TCRUDMethod} from './'
|
||||
import {CRUDReducer, TCRUDMethod, TCRUDAsyncMethod} from './'
|
||||
import {HTTPClientMock, TestUtils, getError} from '../test-utils'
|
||||
import {TMethod} from '@rondo/common'
|
||||
import {IPendingAction} from '../actions'
|
||||
@ -62,7 +62,7 @@ describe('CRUD', () => {
|
||||
'/one/:oneId/two',
|
||||
'TEST',
|
||||
)
|
||||
const crudReducer = new CRUDReducer<ITwo, 'TEST'>('TEST')
|
||||
const crudReducer = new CRUDReducer<ITwo, 'TEST'>('TEST', {name: ''})
|
||||
const Crud = crudReducer.reduce
|
||||
|
||||
const test = new TestUtils()
|
||||
@ -100,7 +100,7 @@ describe('CRUD', () => {
|
||||
})
|
||||
|
||||
function dispatch(
|
||||
method: TCRUDMethod,
|
||||
method: TCRUDAsyncMethod,
|
||||
action: IPendingAction<unknown, string>,
|
||||
) {
|
||||
store.dispatch(action)
|
||||
@ -109,13 +109,13 @@ describe('CRUD', () => {
|
||||
return action
|
||||
}
|
||||
|
||||
function getUrl(method: TCRUDMethod) {
|
||||
function getUrl(method: TCRUDAsyncMethod) {
|
||||
return method === 'save' || method === 'findMany'
|
||||
? '/one/1/two'
|
||||
: '/one/1/two/2'
|
||||
}
|
||||
|
||||
function getHTTPMethod(method: TCRUDMethod): TMethod {
|
||||
function getHTTPMethod(method: TCRUDAsyncMethod): TMethod {
|
||||
switch (method) {
|
||||
case 'save':
|
||||
return 'post'
|
||||
@ -131,7 +131,7 @@ describe('CRUD', () => {
|
||||
|
||||
describe('Promise rejections', () => {
|
||||
const testCases: Array<{
|
||||
method: TCRUDMethod
|
||||
method: TCRUDAsyncMethod
|
||||
params: any
|
||||
}> = [{
|
||||
method: 'findOne',
|
||||
@ -203,7 +203,7 @@ describe('CRUD', () => {
|
||||
const entity = {id: 100, name: 'test'}
|
||||
|
||||
const testCases: Array<{
|
||||
method: TCRUDMethod
|
||||
method: TCRUDAsyncMethod,
|
||||
params: any
|
||||
body?: any
|
||||
response: any
|
||||
@ -307,4 +307,68 @@ describe('CRUD', () => {
|
||||
|
||||
})
|
||||
|
||||
describe('synchronous methods', () => {
|
||||
|
||||
describe('create', () => {
|
||||
it('resets form.create state', () => {
|
||||
store.dispatch(actions.create())
|
||||
expect(store.getState().Crud.form.create).toEqual({
|
||||
item: {
|
||||
name: '',
|
||||
},
|
||||
errors: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('change', () => {
|
||||
it('sets value', () => {
|
||||
store.dispatch(actions.change({key: 'name', value: 'test'}))
|
||||
expect(store.getState().Crud.form.create).toEqual({
|
||||
item: {
|
||||
name: 'test',
|
||||
},
|
||||
errors: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('edit', () => {
|
||||
beforeEach(() => {
|
||||
http.mockAdd({
|
||||
method: 'post',
|
||||
data: {name: 'test'},
|
||||
url: '/one/1/two',
|
||||
}, {id: 100, name: 'test'})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
http.mockClear()
|
||||
})
|
||||
|
||||
it('sets item as edited', async () => {
|
||||
await store.dispatch(actions.save({
|
||||
params: {oneId: 1},
|
||||
body: {name: 'test'},
|
||||
})).payload
|
||||
store.dispatch(actions.edit({id: 100}))
|
||||
expect(store.getState().Crud.form.byId[100]).toEqual({
|
||||
item: {
|
||||
id: 100,
|
||||
name: 'test',
|
||||
},
|
||||
errors: {},
|
||||
})
|
||||
store.dispatch(actions.change({id: 100, key: 'name', value: 'grrr'}))
|
||||
expect(store.getState().Crud.form.byId[100]).toEqual({
|
||||
item: {
|
||||
id: 100,
|
||||
name: 'grrr',
|
||||
},
|
||||
errors: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import {TCRUDAction} from './TCRUDAction'
|
||||
import {TCRUDMethod} from './TCRUDMethod'
|
||||
import {IHTTPClient, ITypedRequestParams} from '../http'
|
||||
import {IRoutes, TFilter, TOnlyDefined} from '@rondo/common'
|
||||
import {TCRUDAction} from './TCRUDAction'
|
||||
import {TCRUDChangeAction} from './TCRUDAction'
|
||||
import {TCRUDCreateAction} from './TCRUDAction'
|
||||
import {TCRUDEditAction} from './TCRUDAction'
|
||||
import {TCRUDMethod} from './TCRUDMethod'
|
||||
|
||||
type TAction<T, ActionType extends string, Method extends TCRUDMethod> =
|
||||
TFilter<TCRUDAction<T, ActionType>, {method: Method, status: 'pending'}>
|
||||
@ -144,6 +147,38 @@ export class FindManyActionCreator<
|
||||
|
||||
}
|
||||
|
||||
export class FormActionCreator<T, ActionType extends string> {
|
||||
constructor(readonly actionType: ActionType) {}
|
||||
|
||||
create = (): TCRUDCreateAction<ActionType> => {
|
||||
return {
|
||||
payload: undefined,
|
||||
type: this.actionType,
|
||||
method: 'create',
|
||||
}
|
||||
}
|
||||
|
||||
edit = (params: {id: number}): TCRUDEditAction<ActionType> => {
|
||||
return {
|
||||
payload: {id: params.id},
|
||||
type: this.actionType,
|
||||
method: 'edit',
|
||||
}
|
||||
}
|
||||
|
||||
change = (params: {
|
||||
id?: number,
|
||||
key: keyof T,
|
||||
value: string
|
||||
}): TCRUDChangeAction<T, ActionType> => {
|
||||
return {
|
||||
payload: params,
|
||||
type: this.actionType,
|
||||
method: 'change',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createCRUDActions<
|
||||
T extends IRoutes,
|
||||
EntityRoute extends keyof T & string,
|
||||
@ -161,5 +196,17 @@ export function createCRUDActions<
|
||||
const {findOne} = new FindOneActionCreator(http, entityRoute, actionType)
|
||||
const {findMany} = new FindManyActionCreator(http, listRoute, actionType)
|
||||
|
||||
return {save, update, remove, findOne, findMany}
|
||||
const {create, edit, change} = new FormActionCreator
|
||||
<T[ListRoute]['post']['body'], ActionType>(actionType)
|
||||
|
||||
return {
|
||||
save,
|
||||
update,
|
||||
remove,
|
||||
findOne,
|
||||
findMany,
|
||||
create,
|
||||
edit,
|
||||
change,
|
||||
}
|
||||
}
|
||||
|
||||
101
packages/client/src/crud/CRUDForm.tsx
Normal file
101
packages/client/src/crud/CRUDForm.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import React from 'react'
|
||||
import {Control, Field, Label, Icon, Input} from 'bloomer'
|
||||
|
||||
export type TCRUDFieldType = 'text' | 'password' | 'number' | 'email' | 'tel'
|
||||
|
||||
export interface ICRUDFieldProps<T> {
|
||||
onChange<K extends keyof T>(key: K, value: string): void
|
||||
Icon?: React.ComponentType
|
||||
error?: string
|
||||
label: string
|
||||
placeholder?: string
|
||||
name: keyof T & string
|
||||
type: TCRUDFieldType
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface ICRUDFormProps<T> {
|
||||
errors: Partial<Record<keyof T & string, string>>
|
||||
item: T
|
||||
error: string
|
||||
submitText: string
|
||||
fields: Array<{
|
||||
Icon?: React.ComponentType
|
||||
label: string
|
||||
placeholder?: string
|
||||
name: keyof T & string
|
||||
type: TCRUDFieldType
|
||||
}>
|
||||
|
||||
onSubmit: (t: T) => void
|
||||
onChange<K extends keyof T>(key: K, value: string): void
|
||||
}
|
||||
|
||||
export class CRUDField<T> extends React.PureComponent<ICRUDFieldProps<T>> {
|
||||
handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const {onChange} = this.props
|
||||
const {value} = e.target
|
||||
onChange(this.props.name, value)
|
||||
}
|
||||
render() {
|
||||
const {label, name, value, placeholder} = this.props
|
||||
return (
|
||||
<Field>
|
||||
<Label>{label}</Label>
|
||||
<Control hasIcons={!!this.props.Icon}>
|
||||
<Input
|
||||
name={name}
|
||||
onChange={this.handleChange}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
/>
|
||||
{!!this.props.Icon && (
|
||||
<Icon isSize='small' isAlign='left'>
|
||||
<this.props.Icon />
|
||||
</Icon>
|
||||
)}
|
||||
</Control>
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class CRUDForm<T> extends React.PureComponent<ICRUDFormProps<T>> {
|
||||
handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
const {onSubmit, item} = this.props
|
||||
onSubmit(item)
|
||||
}
|
||||
render() {
|
||||
const {fields, item} = this.props
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<p className='error'>{this.props.error}</p>
|
||||
{fields.map(field => {
|
||||
const error = this.props.errors[field.name]
|
||||
const value = item[field.name]
|
||||
return (
|
||||
<CRUDField<T>
|
||||
key={field.name}
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
onChange={this.props.onChange}
|
||||
error={error}
|
||||
Icon={field.Icon}
|
||||
value={String(value)}
|
||||
type={field.type}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<div className='center'>
|
||||
<input
|
||||
className='button is-primary'
|
||||
name='submit'
|
||||
type='submit'
|
||||
value={this.props.submitText}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import {Button, Panel, PanelHeading, PanelBlock} from 'bloomer'
|
||||
import {FaPlus, FaEdit, FaTimes} from 'react-icons/fa'
|
||||
import {Link} from 'react-router-dom'
|
||||
import {Link} from '../components'
|
||||
|
||||
export interface ICRUDListProps<T> {
|
||||
nameKey: keyof T
|
||||
|
||||
@ -15,7 +15,19 @@ export interface ICRUDMethodStatus {
|
||||
export interface ICRUDState<T extends ICRUDEntity> {
|
||||
readonly ids: ReadonlyArray<number>
|
||||
readonly byId: Record<number, T>
|
||||
status: ICRUDStatus
|
||||
readonly status: ICRUDStatus
|
||||
readonly form: ICRUDForm<T>
|
||||
}
|
||||
|
||||
interface ICRUDForm<T extends ICRUDEntity> {
|
||||
readonly create: {
|
||||
readonly item: Pick<T, Exclude<keyof T, 'id'>>,
|
||||
readonly errors: Partial<Record<keyof T, string>>
|
||||
}
|
||||
readonly byId: Record<number, {
|
||||
readonly item: T,
|
||||
readonly errors: Partial<Record<keyof T, string>>
|
||||
}>
|
||||
}
|
||||
|
||||
export interface ICRUDStatus {
|
||||
@ -32,12 +44,22 @@ export class CRUDReducer<
|
||||
> {
|
||||
readonly defaultState: ICRUDState<T>
|
||||
|
||||
constructor(readonly actionName: ActionType) {
|
||||
constructor(
|
||||
readonly actionName: ActionType,
|
||||
readonly newItem: Pick<T, Exclude<keyof T, 'id'>>,
|
||||
) {
|
||||
|
||||
const defaultMethodStatus = this.getDefaultMethodStatus()
|
||||
this.defaultState = {
|
||||
ids: [],
|
||||
byId: {},
|
||||
form: {
|
||||
byId: {},
|
||||
create: {
|
||||
item: newItem,
|
||||
errors: {},
|
||||
},
|
||||
},
|
||||
|
||||
status: {
|
||||
save: defaultMethodStatus,
|
||||
@ -161,7 +183,77 @@ export class CRUDReducer<
|
||||
}
|
||||
}
|
||||
|
||||
reduce = (
|
||||
handleCreate = (state: ICRUDState<T>): ICRUDState<T> => {
|
||||
return {
|
||||
...state,
|
||||
form: {
|
||||
...state.form,
|
||||
create: {
|
||||
item: this.newItem,
|
||||
errors: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
handleEdit = (state: ICRUDState<T>, id: number): ICRUDState<T> => {
|
||||
return {
|
||||
...state,
|
||||
form: {
|
||||
...state.form,
|
||||
byId: {
|
||||
...state.form.byId,
|
||||
[id]: {
|
||||
item: state.byId[id],
|
||||
errors: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (state: ICRUDState<T>, payload: {
|
||||
id?: number,
|
||||
key: keyof T,
|
||||
value: string,
|
||||
}): ICRUDState<T> => {
|
||||
const {id, key, value} = payload
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
...state,
|
||||
form: {
|
||||
...state.form,
|
||||
create: {
|
||||
...state.form.create,
|
||||
item: {
|
||||
...state.form.create.item,
|
||||
[key]: value,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
form: {
|
||||
...state.form,
|
||||
byId: {
|
||||
...state.form.byId,
|
||||
[id]: {
|
||||
...state.form.byId[id],
|
||||
item: {
|
||||
...state.form.byId[id].item,
|
||||
[key]: value,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
reduce = (
|
||||
state: ICRUDState<T> | undefined,
|
||||
action: TCRUDAction<T, ActionType>,
|
||||
): ICRUDState<T> => {
|
||||
@ -172,6 +264,19 @@ export class CRUDReducer<
|
||||
return state
|
||||
}
|
||||
|
||||
if (!('status' in action)) {
|
||||
switch (action.method) {
|
||||
case 'change':
|
||||
return this.handleChange(state, action.payload)
|
||||
case 'edit':
|
||||
return this.handleEdit(state, action.payload.id)
|
||||
case 'create':
|
||||
return this.handleCreate(state)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
switch (action.status) {
|
||||
case 'pending':
|
||||
return this.handleLoading(state, action.method)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import {TAsyncAction} from '../actions'
|
||||
import {IAction, TAsyncAction} from '../actions'
|
||||
import {TCRUDMethod} from './TCRUDMethod'
|
||||
|
||||
// Async actions
|
||||
|
||||
export type TCRUDSaveAction<T, ActionType extends string> =
|
||||
TAsyncAction<T, ActionType> & {method: Extract<TCRUDMethod, 'save'>}
|
||||
|
||||
@ -16,9 +18,24 @@ export type TCRUDFindOneAction<T, ActionType extends string> =
|
||||
export type TCRUDFindManyAction<T, ActionType extends string> =
|
||||
TAsyncAction<T[], ActionType> & {method: Extract<TCRUDMethod, 'findMany'>}
|
||||
|
||||
// Synchronous actions
|
||||
|
||||
export type TCRUDCreateAction<ActionType extends string> =
|
||||
IAction<undefined, 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<ActionType>
|
||||
| TCRUDEditAction<ActionType>
|
||||
| TCRUDChangeAction<T, ActionType>
|
||||
|
||||
@ -1 +1,15 @@
|
||||
export type TCRUDMethod = 'save' | 'update' | 'findOne' | 'findMany' | 'remove'
|
||||
export type TCRUDAsyncMethod =
|
||||
'save'
|
||||
| 'update'
|
||||
| 'findOne'
|
||||
| 'findMany'
|
||||
| 'remove'
|
||||
|
||||
export type TCRUDSyncMethod =
|
||||
| 'create'
|
||||
| 'edit'
|
||||
| 'change'
|
||||
|
||||
export type TCRUDMethod =
|
||||
TCRUDAsyncMethod
|
||||
| TCRUDSyncMethod
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user