Form prototype
This commit is contained in:
364
src/Form/components/SuperFormLayout.tsx
Normal file
364
src/Form/components/SuperFormLayout.tsx
Normal file
@@ -0,0 +1,364 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
RefObject,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
type MutableRefObject,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
type Ref,
|
||||
} from 'react'
|
||||
import { useFormContext, useFormState, type FieldValues, type UseFormReturn } from 'react-hook-form'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import {
|
||||
ActionIcon,
|
||||
Group,
|
||||
List,
|
||||
LoadingOverlay,
|
||||
Paper,
|
||||
Spoiler,
|
||||
Stack,
|
||||
Title,
|
||||
Tooltip,
|
||||
Transition,
|
||||
} from '@mantine/core'
|
||||
import { IconChevronsLeft, IconChevronsRight } from '@tabler/icons-react'
|
||||
import { useUncontrolled } from '@mantine/hooks'
|
||||
import useRemote from '../hooks/useRemote'
|
||||
import { useStore } from '../store/SuperForm.store'
|
||||
import classes from '../styles/Form.module.css'
|
||||
import { Form } from './Form'
|
||||
import { FormLayout } from './FormLayout'
|
||||
import type { GridRef, SuperFormRef } from '../types'
|
||||
|
||||
const SuperFormLayout = <T extends FieldValues>(
|
||||
{
|
||||
children,
|
||||
gridRef,
|
||||
}: {
|
||||
children: React.ReactNode | ((props: UseFormReturn<T, any, undefined>) => React.ReactNode)
|
||||
gridRef?: MutableRefObject<GridRef<any> | null>
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
// Component store State
|
||||
const {
|
||||
layoutProps,
|
||||
meta,
|
||||
nested,
|
||||
onBeforeSubmit,
|
||||
onCancel,
|
||||
onLayoutMounted,
|
||||
onLayoutUnMounted,
|
||||
onResetForm,
|
||||
onSubmit,
|
||||
primeData,
|
||||
request,
|
||||
tableName,
|
||||
value,
|
||||
} = useStore((state) => ({
|
||||
extraButtons: state.extraButtons,
|
||||
layoutProps: state.layoutProps,
|
||||
meta: state.meta,
|
||||
nested: state.nested,
|
||||
onBeforeSubmit: state.onBeforeSubmit,
|
||||
onCancel: state.onCancel,
|
||||
onLayoutMounted: state.onLayoutMounted,
|
||||
onLayoutUnMounted: state.onLayoutUnMounted,
|
||||
onResetForm: state.onResetForm,
|
||||
onSubmit: state.onSubmit,
|
||||
primeData: state.primeData,
|
||||
request: state.request,
|
||||
tableName: state.remote?.tableName,
|
||||
value: state.value,
|
||||
}))
|
||||
|
||||
const [_opened, _setOpened] = useUncontrolled({
|
||||
value: layoutProps?.bodyRightSection?.opened,
|
||||
defaultValue: false,
|
||||
onChange: layoutProps?.bodyRightSection?.setOpened,
|
||||
})
|
||||
|
||||
// Component Hooks
|
||||
const form = useFormContext<T, any, undefined>()
|
||||
const formState = useFormState({ control: form.control })
|
||||
|
||||
const { isFetching, mutateAsync, error, queryKey } = useRemote(gridRef)
|
||||
|
||||
// Component variables
|
||||
const formUID = useMemo(() => {
|
||||
return meta?.id ?? uuid()
|
||||
}, [])
|
||||
|
||||
const requestString = request?.charAt(0).toUpperCase() + request?.slice(1)
|
||||
|
||||
const renderRightSection = (
|
||||
<>
|
||||
<Tooltip label={`${_opened ? 'Close' : 'Open'} Right Section`} withArrow>
|
||||
<ActionIcon
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
zIndex: 5,
|
||||
|
||||
display: layoutProps?.bodyRightSection?.hideToggleButton ? 'none' : 'block',
|
||||
}}
|
||||
variant='filled'
|
||||
size='sm'
|
||||
onClick={() => _setOpened(!_opened)}
|
||||
radius='6'
|
||||
m={2}
|
||||
>
|
||||
{_opened ? <IconChevronsRight /> : <IconChevronsLeft />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Group wrap='nowrap' h='100%' align='flex-start' gap={2} w={'100%'}>
|
||||
<Stack gap={0} h='100%' style={{ flex: 1 }}>
|
||||
{typeof children === 'function' ? children({ ...form }) : children}
|
||||
</Stack>
|
||||
<Transition transition='slide-left' mounted={_opened}>
|
||||
{(transitionStyles) => (
|
||||
<Paper
|
||||
style={transitionStyles}
|
||||
h='100%'
|
||||
w={layoutProps?.bodyRightSection?.w}
|
||||
shadow='xs'
|
||||
radius='xs'
|
||||
mr='xs'
|
||||
mt='xs'
|
||||
ml={0}
|
||||
{...layoutProps?.bodyRightSection?.paperProps}
|
||||
>
|
||||
{layoutProps?.bodyRightSection?.render?.({
|
||||
form,
|
||||
formValue: form.getValues(),
|
||||
isFetching,
|
||||
opened: _opened,
|
||||
queryKey,
|
||||
setOpened: _setOpened,
|
||||
})}
|
||||
</Paper>
|
||||
)}
|
||||
</Transition>
|
||||
</Group>
|
||||
</>
|
||||
)
|
||||
|
||||
// Component Callback Functions
|
||||
const onFormSubmit = async (data: T | any, closeForm: boolean = true) => {
|
||||
const res: any =
|
||||
typeof onBeforeSubmit === 'function'
|
||||
? await mutateAsync?.(await onBeforeSubmit(data, request, form))
|
||||
: await mutateAsync?.(data)
|
||||
|
||||
if ((tableName?.length ?? 0) > 0) {
|
||||
if (res?.ok || (res?.status >= 200 && res?.status < 300)) {
|
||||
onSubmit?.(res?.data, request, data, form, closeForm)
|
||||
} else {
|
||||
form.setError('root', {
|
||||
message: res.status === 401 ? 'Username or password is incorrect' : res?.error,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
onSubmit?.(data, request, data, form, closeForm)
|
||||
}
|
||||
}
|
||||
|
||||
// Component use Effects
|
||||
useEffect(() => {
|
||||
if (request === 'insert') {
|
||||
if (onResetForm) {
|
||||
onResetForm(primeData, form).then((resetData) => {
|
||||
form.reset(resetData)
|
||||
})
|
||||
} else {
|
||||
form.reset(primeData)
|
||||
}
|
||||
} else if ((request === 'change' || request === 'delete') && (tableName?.length ?? 0) === 0) {
|
||||
if (onResetForm) {
|
||||
onResetForm(value, form).then((resetData) => {
|
||||
form.reset(resetData)
|
||||
})
|
||||
} else {
|
||||
form.reset(value)
|
||||
}
|
||||
}
|
||||
onLayoutMounted?.()
|
||||
return onLayoutUnMounted
|
||||
}, [
|
||||
request,
|
||||
primeData,
|
||||
tableName,
|
||||
value,
|
||||
form.reset,
|
||||
onResetForm,
|
||||
onLayoutMounted,
|
||||
onLayoutUnMounted,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(Object.keys(formState.errors)?.length > 0 || error) &&
|
||||
_opened === false &&
|
||||
layoutProps?.showErrorList !== false
|
||||
) {
|
||||
_setOpened(true)
|
||||
}
|
||||
}, [Object.keys(formState.errors)?.length > 0, error, layoutProps?.showErrorList])
|
||||
|
||||
useImperativeHandle<SuperFormRef<T>, SuperFormRef<T>>(ref, () => ({
|
||||
form,
|
||||
mutation: { isFetching, mutateAsync, error },
|
||||
submit: (closeForm: boolean = true, afterSubmit?: (data: T | any) => void) => {
|
||||
return form.handleSubmit(async (data: T | any) => {
|
||||
await onFormSubmit(data, closeForm)
|
||||
afterSubmit?.(data)
|
||||
})()
|
||||
},
|
||||
queryKey,
|
||||
getFormState: () => formState,
|
||||
}))
|
||||
|
||||
return (
|
||||
<form
|
||||
name={formUID}
|
||||
onSubmit={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
form.handleSubmit((data: T | any) => {
|
||||
onFormSubmit(data)
|
||||
})(e)
|
||||
}}
|
||||
style={{ height: '100%' }}
|
||||
className={request === 'view' ? classes.disabled : ''}
|
||||
>
|
||||
{/* <LoadingOverlay
|
||||
visible={isFetching}
|
||||
overlayProps={{
|
||||
backgroundOpacity: 0.5,
|
||||
}}
|
||||
/> */}
|
||||
{layoutProps?.noLayout ? (
|
||||
typeof layoutProps?.bodyRightSection?.render === 'function' ? (
|
||||
renderRightSection
|
||||
) : typeof children === 'function' ? (
|
||||
<>
|
||||
<LoadingOverlay
|
||||
visible={isFetching}
|
||||
overlayProps={{
|
||||
backgroundOpacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
{children({ ...form })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LoadingOverlay
|
||||
visible={isFetching}
|
||||
overlayProps={{
|
||||
backgroundOpacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<FormLayout
|
||||
dirty={formState.isDirty}
|
||||
loading={isFetching}
|
||||
onCancel={() => onCancel?.(request)}
|
||||
onSubmit={form.handleSubmit((data: T | any) => {
|
||||
onFormSubmit(data)
|
||||
})}
|
||||
request={request}
|
||||
modal={false}
|
||||
nested={nested}
|
||||
>
|
||||
{!layoutProps?.noHeader && (
|
||||
<Form.Section
|
||||
type='header'
|
||||
title={`${layoutProps?.title || requestString}`}
|
||||
rightSection={layoutProps?.rightSection}
|
||||
/>
|
||||
)}
|
||||
{(Object.keys(formState.errors)?.length > 0 || error) && (
|
||||
<Form.Section
|
||||
className={classes.sticky}
|
||||
buttonTitles={layoutProps?.buttonTitles}
|
||||
type='error'
|
||||
>
|
||||
<Title order={6} size='sm' c='red'>
|
||||
{(error?.message?.length ?? 0) > 0
|
||||
? 'Server Error'
|
||||
: 'Required information is incomplete*'}
|
||||
</Title>
|
||||
{(error as any)?.response?.data?.msg ||
|
||||
(error as any)?.response?.data?._error ||
|
||||
error?.message}
|
||||
{layoutProps?.showErrorList !== false && (
|
||||
<Spoiler maxHeight={50} showLabel='Show more' hideLabel='Hide'>
|
||||
<List
|
||||
size='xs'
|
||||
style={{
|
||||
color: 'light-dark(var(--mantine-color-dark-7), var(--mantine-color-gray-2))',
|
||||
}}
|
||||
>
|
||||
{getErrorMessages(formState.errors)}
|
||||
</List>
|
||||
</Spoiler>
|
||||
)}
|
||||
</Form.Section>
|
||||
)}
|
||||
{typeof layoutProps?.bodyRightSection?.render === 'function' ? (
|
||||
<Form.Section type='body' {...layoutProps?.bodySectionProps}>
|
||||
{renderRightSection}
|
||||
</Form.Section>
|
||||
) : (
|
||||
<Form.Section type='body' {...layoutProps?.bodySectionProps}>
|
||||
{typeof children === 'function' ? children({ ...form }) : children}
|
||||
</Form.Section>
|
||||
)}
|
||||
{!layoutProps?.noFooter && (
|
||||
<Form.Section
|
||||
className={classes.sticky}
|
||||
buttonTitles={layoutProps?.buttonTitles}
|
||||
type='footer'
|
||||
{...(typeof layoutProps?.footerSectionProps === 'function'
|
||||
? layoutProps?.footerSectionProps(ref as RefObject<SuperFormRef<T>>)
|
||||
: layoutProps?.footerSectionProps)}
|
||||
>
|
||||
{typeof layoutProps?.extraButtons === 'function'
|
||||
? layoutProps?.extraButtons(form)
|
||||
: layoutProps?.extraButtons}
|
||||
</Form.Section>
|
||||
)}
|
||||
</FormLayout>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
const getErrorMessages = (errors: any): ReactNode | null => {
|
||||
return Object.keys(errors ?? {}).map((key) => {
|
||||
if (typeof errors[key] === 'object' && key !== 'ref') {
|
||||
return getErrorMessages(errors[key])
|
||||
}
|
||||
if (key !== 'message') {
|
||||
return null
|
||||
}
|
||||
|
||||
return <List.Item key={key}>{errors[key]}</List.Item>
|
||||
})
|
||||
}
|
||||
|
||||
const FRSuperFormLayout = forwardRef(SuperFormLayout) as <T extends FieldValues>(
|
||||
props: {
|
||||
children: React.ReactNode | ((props: UseFormReturn<T, any, undefined>) => React.ReactNode)
|
||||
gridRef?: MutableRefObject<GridRef | null>
|
||||
} & {
|
||||
ref?: Ref<SuperFormRef<T>>
|
||||
}
|
||||
) => ReactElement
|
||||
|
||||
export default FRSuperFormLayout
|
||||
Reference in New Issue
Block a user