365 lines
10 KiB
TypeScript
365 lines
10 KiB
TypeScript
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
|