Compare commits
1 Commits
fbb65afc94
...
rw
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bac48d5dd |
38
src/Form/components/Form.tsx
Normal file
38
src/Form/components/Form.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { Card, Stack, LoadingOverlay } from '@mantine/core'
|
||||
import { FormSection } from './FormSection'
|
||||
|
||||
interface FormProps {
|
||||
children: ReactNode
|
||||
loading?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const Form: React.FC<FormProps> & {
|
||||
Section: typeof FormSection
|
||||
} = ({ children, loading, ...others }) => {
|
||||
return (
|
||||
<Card
|
||||
withBorder
|
||||
component={Stack}
|
||||
w="100%"
|
||||
h="100%"
|
||||
padding={0}
|
||||
styles={{
|
||||
root: {
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}}
|
||||
shadow="sm"
|
||||
radius="md"
|
||||
{...others}
|
||||
>
|
||||
<LoadingOverlay visible={loading || false} />
|
||||
{children}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
Form.Section = FormSection
|
||||
55
src/Form/components/FormLayout.tsx
Normal file
55
src/Form/components/FormLayout.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { Modal } from '@mantine/core'
|
||||
import { Form } from './Form'
|
||||
import { FormLayoutStoreProvider, useFormLayoutStore } from '../store/FormLayout.store'
|
||||
import type { RequestType } from '../types'
|
||||
|
||||
interface FormLayoutProps {
|
||||
children: ReactNode
|
||||
dirty?: boolean
|
||||
loading?: boolean
|
||||
onCancel?: () => void
|
||||
onSubmit?: () => void
|
||||
request?: RequestType
|
||||
modal?: boolean
|
||||
modalProps?: any
|
||||
nested?: boolean
|
||||
deleteFormProps?: any
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const LayoutComponent: React.FC<FormLayoutProps> = ({
|
||||
children,
|
||||
modal,
|
||||
modalProps,
|
||||
...others
|
||||
}) => {
|
||||
const { request } = useFormLayoutStore((state) => ({
|
||||
request: state.request,
|
||||
}))
|
||||
|
||||
const modalWidth = request === 'delete' ? 400 : modalProps?.width
|
||||
|
||||
return modal === true ? (
|
||||
<Modal
|
||||
onClose={() => modalProps?.onClose?.()}
|
||||
opened={modalProps?.opened || false}
|
||||
size="auto"
|
||||
withCloseButton={false}
|
||||
centered={request !== 'delete'}
|
||||
{...modalProps}
|
||||
>
|
||||
<div style={{ height: modalProps?.height, width: modalWidth }}>
|
||||
<Form {...others}>{children}</Form>
|
||||
</div>
|
||||
</Modal>
|
||||
) : (
|
||||
<Form {...others}>{children}</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export const FormLayout: React.FC<FormLayoutProps> = (props) => (
|
||||
<FormLayoutStoreProvider {...props}>
|
||||
<LayoutComponent {...props} />
|
||||
</FormLayoutStoreProvider>
|
||||
)
|
||||
117
src/Form/components/FormSection.tsx
Normal file
117
src/Form/components/FormSection.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { Stack, Group, Paper, Button, Title, Box } from '@mantine/core'
|
||||
import { useFormLayoutStore } from '../store/FormLayout.store'
|
||||
|
||||
interface FormSectionProps {
|
||||
type: 'header' | 'body' | 'footer' | 'error'
|
||||
title?: string
|
||||
rightSection?: ReactNode
|
||||
children?: ReactNode
|
||||
buttonTitles?: { submit?: string; cancel?: string }
|
||||
className?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const FormSection: React.FC<FormSectionProps> = ({
|
||||
type,
|
||||
title,
|
||||
rightSection,
|
||||
children,
|
||||
buttonTitles,
|
||||
className,
|
||||
...others
|
||||
}) => {
|
||||
const { onCancel, onSubmit, request, loading } = useFormLayoutStore((state) => ({
|
||||
onCancel: state.onCancel,
|
||||
onSubmit: state.onSubmit,
|
||||
request: state.request,
|
||||
loading: state.loading,
|
||||
}))
|
||||
|
||||
if (type === 'header') {
|
||||
return (
|
||||
<Group
|
||||
justify="space-between"
|
||||
p="md"
|
||||
style={{
|
||||
borderBottom: '1px solid var(--mantine-color-gray-3)',
|
||||
}}
|
||||
className={className}
|
||||
{...others}
|
||||
>
|
||||
<Title order={4} size="h5">
|
||||
{title}
|
||||
</Title>
|
||||
{rightSection && <Box>{rightSection}</Box>}
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'body') {
|
||||
return (
|
||||
<Stack
|
||||
gap="md"
|
||||
p="md"
|
||||
style={{ flex: 1, overflow: 'auto' }}
|
||||
className={className}
|
||||
{...others}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'footer') {
|
||||
return (
|
||||
<Group
|
||||
justify="flex-end"
|
||||
gap="xs"
|
||||
p="md"
|
||||
style={{
|
||||
borderTop: '1px solid var(--mantine-color-gray-3)',
|
||||
}}
|
||||
className={className}
|
||||
{...others}
|
||||
>
|
||||
{children}
|
||||
{request !== 'view' && (
|
||||
<>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={onCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
{buttonTitles?.cancel ?? 'Cancel'}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={onSubmit}
|
||||
loading={loading}
|
||||
>
|
||||
{buttonTitles?.submit ?? (request === 'delete' ? 'Delete' : 'Save')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'error') {
|
||||
return (
|
||||
<Paper
|
||||
p="sm"
|
||||
m="md"
|
||||
style={{
|
||||
backgroundColor: 'var(--mantine-color-red-0)',
|
||||
border: '1px solid var(--mantine-color-red-3)',
|
||||
}}
|
||||
className={className}
|
||||
{...others}
|
||||
>
|
||||
{children}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
34
src/Form/components/SuperForm.tsx
Normal file
34
src/Form/components/SuperForm.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { forwardRef, type ReactElement, type Ref } from 'react'
|
||||
import { FormProvider, useForm, type FieldValues } from 'react-hook-form'
|
||||
import { Provider } from '../store/SuperForm.store'
|
||||
import type { SuperFormProps, SuperFormRef } from '../types'
|
||||
import Layout from './SuperFormLayout'
|
||||
import SuperFormPersist from './SuperFormPersist'
|
||||
|
||||
const SuperForm = <T extends FieldValues>(
|
||||
{ useFormProps, gridRef, children, persist, ...others }: SuperFormProps<T>,
|
||||
ref
|
||||
) => {
|
||||
const form = useForm<T>({ ...useFormProps })
|
||||
|
||||
return (
|
||||
<Provider {...others}>
|
||||
<FormProvider {...form}>
|
||||
{persist && (
|
||||
<SuperFormPersist storageKey={typeof persist === 'object' ? persist.storageKey : null} />
|
||||
)}
|
||||
<Layout<T> gridRef={gridRef} ref={ref}>
|
||||
{children}
|
||||
</Layout>
|
||||
</FormProvider>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const FRSuperForm = forwardRef(SuperForm) as <T extends FieldValues>(
|
||||
props: SuperFormProps<T> & {
|
||||
ref?: Ref<SuperFormRef<T>>
|
||||
}
|
||||
) => ReactElement
|
||||
|
||||
export default FRSuperForm
|
||||
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
|
||||
66
src/Form/components/SuperFormPersist.tsx
Normal file
66
src/Form/components/SuperFormPersist.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useFormContext, useFormState } from 'react-hook-form'
|
||||
import { useDebouncedCallback } from '@mantine/hooks'
|
||||
import useSubscribe from '../hooks/use-subscribe'
|
||||
import { useSuperFormStore } from '../store/SuperForm.store'
|
||||
import { openConfirmModal } from '../utils/openConfirmModal'
|
||||
|
||||
const SuperFormPersist = ({ storageKey }: { storageKey?: string | null }) => {
|
||||
// Component store State
|
||||
const [persistKey, setPersistKey] = useState<string>('')
|
||||
const { isDirty, isReady, isSubmitted } = useFormState()
|
||||
|
||||
const { remote, request } = useSuperFormStore((state) => ({
|
||||
request: state.request,
|
||||
remote: state.remote,
|
||||
}))
|
||||
|
||||
// Component Hooks
|
||||
const { reset, setValue } = useFormContext()
|
||||
|
||||
const handleFormChange = useDebouncedCallback(({ values }) => {
|
||||
setPersistKey(() => {
|
||||
const key = `superform-persist-${storageKey?.length > 0 ? storageKey : `${remote?.tableName || 'local'}-${request}-${values[remote?.primaryKey] ?? ''}`}`
|
||||
|
||||
if (!isDirty) {
|
||||
return key
|
||||
}
|
||||
|
||||
window.localStorage.setItem(key, JSON.stringify(values))
|
||||
|
||||
return key
|
||||
})
|
||||
}, 250)
|
||||
|
||||
useSubscribe('', handleFormChange)
|
||||
|
||||
// Component use Effects
|
||||
useEffect(() => {
|
||||
if (isReady && persistKey) {
|
||||
const data = window.localStorage.getItem(persistKey)
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isSubmitted) {
|
||||
window.localStorage.removeItem(persistKey)
|
||||
return
|
||||
}
|
||||
|
||||
openConfirmModal(
|
||||
() => {
|
||||
reset(JSON.parse(data))
|
||||
setValue('_dirty', true, { shouldDirty: true })
|
||||
},
|
||||
() => {
|
||||
window.localStorage.removeItem(persistKey)
|
||||
},
|
||||
'Do you want to restore the previous data that was not submitted?'
|
||||
)
|
||||
}
|
||||
}, [isReady, isSubmitted, persistKey])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default SuperFormPersist
|
||||
43
src/Form/config/ApiConfig.tsx
Normal file
43
src/Form/config/ApiConfig.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { createContext, useContext, ReactNode, useState } from 'react'
|
||||
|
||||
interface ApiConfigContextValue {
|
||||
apiURL: string
|
||||
setApiURL: (url: string) => void
|
||||
}
|
||||
|
||||
const ApiConfigContext = createContext<ApiConfigContextValue | null>(null)
|
||||
|
||||
interface ApiConfigProviderProps {
|
||||
children: ReactNode
|
||||
defaultApiURL?: string
|
||||
}
|
||||
|
||||
export const ApiConfigProvider: React.FC<ApiConfigProviderProps> = ({
|
||||
children,
|
||||
defaultApiURL = '',
|
||||
}) => {
|
||||
const [apiURL, setApiURL] = useState(defaultApiURL)
|
||||
|
||||
return (
|
||||
<ApiConfigContext.Provider value={{ apiURL, setApiURL }}>
|
||||
{children}
|
||||
</ApiConfigContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useApiConfig = (): ApiConfigContextValue => {
|
||||
const context = useContext(ApiConfigContext)
|
||||
if (!context) {
|
||||
throw new Error('useApiConfig must be used within ApiConfigProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get API URL with optional override
|
||||
* @param overrideURL - Optional URL to use instead of context value
|
||||
*/
|
||||
export const useApiURL = (overrideURL?: string): string => {
|
||||
const { apiURL } = useApiConfig()
|
||||
return overrideURL ?? apiURL
|
||||
}
|
||||
146
src/Form/containers/Drawer/SuperFormDrawer.tsx
Normal file
146
src/Form/containers/Drawer/SuperFormDrawer.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactElement,
|
||||
type Ref,
|
||||
} from 'react'
|
||||
import { IconX } from '@tabler/icons-react'
|
||||
import type { FieldValues } from 'react-hook-form'
|
||||
import { ActionIcon, Drawer } from '@mantine/core'
|
||||
import type { SuperFormDrawerProps, SuperFormDrawerRef, SuperFormRef } from '../../types'
|
||||
import SuperForm from '../../components/SuperForm'
|
||||
import { openConfirmModal } from '../../utils/openConfirmModal'
|
||||
|
||||
const SuperFormDrawer = <T extends FieldValues>(
|
||||
{ drawerProps, noCloseOnSubmit, ...formProps }: SuperFormDrawerProps<T>,
|
||||
ref: Ref<SuperFormDrawerRef<T>>
|
||||
) => {
|
||||
// Component Refs
|
||||
const formRef = useRef<SuperFormRef<T>>(null)
|
||||
const drawerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Component store State
|
||||
// Tell drawer that form layout mounted to fix refs
|
||||
const [layoutMounted, setLayoutMounted] = useState(false)
|
||||
|
||||
// Component Callback Functions
|
||||
const onSubmit = (data: T, request, formData, form, closeForm: boolean = true) => {
|
||||
formProps?.onSubmit?.(data, request, formData, form, closeForm)
|
||||
|
||||
if (request === 'delete') {
|
||||
drawerProps?.onClose()
|
||||
}
|
||||
|
||||
if (!noCloseOnSubmit) {
|
||||
if (closeForm) {
|
||||
drawerProps?.onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = (request) => {
|
||||
if (formRef?.current?.getFormState().isDirty) {
|
||||
openConfirmModal(() => {
|
||||
drawerProps?.onClose()
|
||||
formProps?.onCancel?.(request)
|
||||
})
|
||||
} else {
|
||||
drawerProps?.onClose()
|
||||
formProps?.onCancel?.(request)
|
||||
}
|
||||
}
|
||||
|
||||
const onLayoutMounted = useCallback(() => {
|
||||
setLayoutMounted(true)
|
||||
formProps?.onLayoutMounted?.()
|
||||
}, [formProps?.onLayoutMounted])
|
||||
|
||||
const onLayoutUnMounted = useCallback(() => {
|
||||
setLayoutMounted(false)
|
||||
formProps?.onLayoutUnMounted?.()
|
||||
}, [formProps?.onLayoutUnMounted])
|
||||
|
||||
// Component use Effects
|
||||
useImperativeHandle<SuperFormDrawerRef<T>, SuperFormDrawerRef<T>>(
|
||||
ref,
|
||||
() => ({
|
||||
...formRef.current,
|
||||
drawer: drawerRef.current,
|
||||
} as SuperFormDrawerRef<T>),
|
||||
[layoutMounted]
|
||||
)
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
ref={drawerRef}
|
||||
onClose={onCancel}
|
||||
closeOnClickOutside={false}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape' && drawerProps.closeOnEscape !== false) {
|
||||
e.stopPropagation()
|
||||
onCancel(formProps.request)
|
||||
}
|
||||
}}
|
||||
overlayProps={{ backgroundOpacity: 0.5, blur: 0.5 }}
|
||||
padding={6}
|
||||
position='right'
|
||||
transitionProps={{
|
||||
transition: 'slide-left',
|
||||
duration: 150,
|
||||
timingFunction: 'linear',
|
||||
}}
|
||||
size={500}
|
||||
styles={{
|
||||
content: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'stretch',
|
||||
},
|
||||
body: {
|
||||
minHeight: '100px',
|
||||
flexGrow: 1,
|
||||
},
|
||||
}}
|
||||
keepMounted={false}
|
||||
{...drawerProps}
|
||||
closeOnEscape={false}
|
||||
withCloseButton={false}
|
||||
title={null}
|
||||
>
|
||||
<SuperForm<T>
|
||||
{...formProps}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
onLayoutMounted={onLayoutMounted}
|
||||
onLayoutUnMounted={onLayoutUnMounted}
|
||||
ref={formRef}
|
||||
layoutProps={{
|
||||
...formProps?.layoutProps,
|
||||
rightSection: (
|
||||
<ActionIcon
|
||||
size='xs'
|
||||
onClick={() => {
|
||||
onCancel(formProps?.request)
|
||||
}}
|
||||
>
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
),
|
||||
title:
|
||||
(drawerProps.title as string) ??
|
||||
formProps?.layoutProps?.title ??
|
||||
(formProps?.request as string),
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
const FRSuperFormDrawer = forwardRef(SuperFormDrawer) as <T extends FieldValues>(
|
||||
props: SuperFormDrawerProps<T> & { ref?: Ref<SuperFormDrawerRef<T>> }
|
||||
) => ReactElement
|
||||
|
||||
export default FRSuperFormDrawer
|
||||
105
src/Form/containers/Modal/SuperFormModal.tsx
Normal file
105
src/Form/containers/Modal/SuperFormModal.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactElement,
|
||||
type Ref,
|
||||
} from 'react'
|
||||
import type { FieldValues } from 'react-hook-form'
|
||||
import { Modal, ScrollArea } from '@mantine/core'
|
||||
import type { SuperFormModalProps, SuperFormModalRef, SuperFormRef } from '../../types'
|
||||
import SuperForm from '../../components/SuperForm'
|
||||
import { openConfirmModal } from '../../utils/openConfirmModal'
|
||||
|
||||
const SuperFormModal = <T extends FieldValues>(
|
||||
{ modalProps, noCloseOnSubmit, ...formProps }: SuperFormModalProps<T>,
|
||||
ref: Ref<SuperFormModalRef<T>>
|
||||
) => {
|
||||
// Component Refs
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const formRef = useRef<SuperFormRef<T>>(null)
|
||||
|
||||
// Component store State
|
||||
// Tell drawer that form layout mounted to fix refs
|
||||
const [layoutMounted, setLayoutMounted] = useState(false)
|
||||
|
||||
// Component Callback Functions
|
||||
const onSubmit = (data: T, request, formData, form, closeForm: boolean = true) => {
|
||||
formProps?.onSubmit?.(data, request, formData, form, closeForm)
|
||||
|
||||
if (request === 'delete') {
|
||||
modalProps?.onClose()
|
||||
}
|
||||
|
||||
if (!noCloseOnSubmit) {
|
||||
if (closeForm) {
|
||||
modalProps?.onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = (request) => {
|
||||
if (formRef?.current?.getFormState().isDirty) {
|
||||
openConfirmModal(() => {
|
||||
modalProps?.onClose()
|
||||
formProps?.onCancel?.(request)
|
||||
})
|
||||
} else {
|
||||
modalProps?.onClose()
|
||||
formProps?.onCancel?.(request)
|
||||
}
|
||||
}
|
||||
|
||||
const onLayoutMounted = useCallback(() => {
|
||||
setLayoutMounted(true)
|
||||
formProps?.onLayoutMounted?.()
|
||||
}, [formProps?.onLayoutMounted])
|
||||
|
||||
const onLayoutUnMounted = useCallback(() => {
|
||||
setLayoutMounted(false)
|
||||
formProps?.onLayoutUnMounted?.()
|
||||
}, [formProps?.onLayoutUnMounted])
|
||||
|
||||
// Component use Effects
|
||||
useImperativeHandle<SuperFormModalRef<T>, SuperFormModalRef<T>>(
|
||||
ref,
|
||||
() => ({
|
||||
...formRef.current,
|
||||
modal: modalRef.current,
|
||||
} as SuperFormModalRef<T>),
|
||||
[layoutMounted]
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
ref={modalRef}
|
||||
closeOnClickOutside={false}
|
||||
overlayProps={{
|
||||
backgroundOpacity: 0.5,
|
||||
blur: 4,
|
||||
}}
|
||||
padding='sm'
|
||||
scrollAreaComponent={ScrollArea.Autosize}
|
||||
size={500}
|
||||
keepMounted={false}
|
||||
{...modalProps}
|
||||
>
|
||||
<SuperForm<T>
|
||||
{...formProps}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
onLayoutMounted={onLayoutMounted}
|
||||
onLayoutUnMounted={onLayoutUnMounted}
|
||||
ref={formRef}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const FRSuperFormModal = forwardRef(SuperFormModal) as <T extends FieldValues>(
|
||||
props: SuperFormModalProps<T> & { ref?: Ref<SuperFormModalRef<T>> }
|
||||
) => ReactElement
|
||||
|
||||
export default FRSuperFormModal
|
||||
116
src/Form/containers/Popover/SuperFormPopover.tsx
Normal file
116
src/Form/containers/Popover/SuperFormPopover.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactElement,
|
||||
type Ref,
|
||||
} from 'react'
|
||||
import type { FieldValues } from 'react-hook-form'
|
||||
import { Box, Popover } from '@mantine/core'
|
||||
import { useUncontrolled } from '@mantine/hooks'
|
||||
import type { SuperFormPopoverProps, SuperFormPopoverRef, SuperFormRef } from '../../types'
|
||||
import SuperForm from '../../components/SuperForm'
|
||||
import { openConfirmModal } from '../../utils/openConfirmModal'
|
||||
|
||||
const SuperFormPopover = <T extends FieldValues>(
|
||||
{ popoverProps, target, noCloseOnSubmit, ...formProps }: SuperFormPopoverProps<T>,
|
||||
ref: Ref<SuperFormPopoverRef<T>>
|
||||
) => {
|
||||
// Component Refs
|
||||
const popoverRef = useRef<HTMLDivElement>(null)
|
||||
const formRef = useRef<SuperFormRef<T>>(null)
|
||||
|
||||
// Component store State
|
||||
// Tell drawer that form layout mounted to fix refs
|
||||
const [layoutMounted, setLayoutMounted] = useState(false)
|
||||
|
||||
// Component Hooks
|
||||
const [_value, _onChange] = useUncontrolled({
|
||||
value: popoverProps?.opened,
|
||||
onChange: popoverProps?.onChange,
|
||||
})
|
||||
|
||||
// Component Callback Functions
|
||||
const onSubmit = (data: T, request, formData, form, closeForm: boolean = true) => {
|
||||
formProps?.onSubmit?.(data, request, formData, form, closeForm)
|
||||
|
||||
if (request === 'delete') {
|
||||
_onChange(false)
|
||||
}
|
||||
|
||||
if (!noCloseOnSubmit) {
|
||||
if (closeForm) {
|
||||
_onChange(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = (request) => {
|
||||
if (formRef?.current?.getFormState().isDirty) {
|
||||
openConfirmModal(() => {
|
||||
_onChange(false)
|
||||
formProps?.onCancel?.(request)
|
||||
})
|
||||
} else {
|
||||
_onChange(false)
|
||||
formProps?.onCancel?.(request)
|
||||
}
|
||||
}
|
||||
|
||||
const onLayoutMounted = useCallback(() => {
|
||||
setLayoutMounted(true)
|
||||
formProps?.onLayoutMounted?.()
|
||||
}, [formProps?.onLayoutMounted])
|
||||
|
||||
const onLayoutUnMounted = useCallback(() => {
|
||||
setLayoutMounted(false)
|
||||
formProps?.onLayoutUnMounted?.()
|
||||
}, [formProps?.onLayoutUnMounted])
|
||||
|
||||
// Component use Effects
|
||||
useImperativeHandle<SuperFormPopoverRef<T>, SuperFormPopoverRef<T>>(
|
||||
ref,
|
||||
() => ({
|
||||
...formRef.current,
|
||||
popover: popoverRef.current,
|
||||
} as SuperFormPopoverRef<T>),
|
||||
[layoutMounted]
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover
|
||||
closeOnClickOutside={false}
|
||||
onClose={() => _onChange(false)}
|
||||
opened={_value}
|
||||
position='left'
|
||||
radius='md'
|
||||
withArrow
|
||||
withinPortal
|
||||
zIndex={200}
|
||||
keepMounted={false}
|
||||
{...popoverProps}
|
||||
>
|
||||
<Popover.Target>
|
||||
<Box onClick={() => _onChange(true)}>{target}</Box>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown p={0} m={0} ref={popoverRef}>
|
||||
<SuperForm
|
||||
{...formProps}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
onLayoutMounted={onLayoutMounted}
|
||||
onLayoutUnMounted={onLayoutUnMounted}
|
||||
ref={formRef}
|
||||
/>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const FRSuperFormPopover = forwardRef(SuperFormPopover) as <T extends FieldValues>(
|
||||
props: SuperFormPopoverProps<T> & { ref?: Ref<SuperFormPopoverRef<T>> }
|
||||
) => ReactElement
|
||||
|
||||
export default FRSuperFormPopover
|
||||
96
src/Form/hooks/use-drawer-form-state.tsx
Normal file
96
src/Form/hooks/use-drawer-form-state.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useState } from 'react'
|
||||
import { FieldValues } from 'react-hook-form'
|
||||
import { SuperFormProps, RequestType, ExtendedDrawerProps } from '../types'
|
||||
|
||||
interface UseDrawerFormState<T extends FieldValues> extends Partial<SuperFormProps<T>> {
|
||||
drawerProps: Partial<ExtendedDrawerProps>
|
||||
opened?: boolean
|
||||
onClose?: () => void
|
||||
request: RequestType
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type AskFunction = (request: RequestType, buffer: any) => void
|
||||
|
||||
const useDrawerFormState = <T extends FieldValues>(
|
||||
props?: Partial<UseDrawerFormState<T>>
|
||||
): {
|
||||
formProps: UseDrawerFormState<T>
|
||||
setFormProps: React.Dispatch<React.SetStateAction<UseDrawerFormState<T>>>
|
||||
open: (props: Partial<UseDrawerFormState<T>>) => void
|
||||
close: () => void
|
||||
ask: AskFunction
|
||||
} => {
|
||||
const [formProps, setFormProps] = useState<UseDrawerFormState<T>>({
|
||||
opened: false,
|
||||
request: 'insert',
|
||||
...props,
|
||||
onClose: () =>
|
||||
setFormProps((curr) => ({
|
||||
...curr,
|
||||
opened: false,
|
||||
drawerProps: { ...curr.drawerProps, opened: false },
|
||||
})),
|
||||
drawerProps: { opened: false, onClose: () => {}, ...props?.drawerProps },
|
||||
})
|
||||
|
||||
return {
|
||||
formProps,
|
||||
setFormProps,
|
||||
open: (props?: Partial<UseDrawerFormState<T>>) => {
|
||||
setFormProps((curr) => {
|
||||
return {
|
||||
...curr,
|
||||
...props,
|
||||
request: props.request ?? curr.request,
|
||||
opened: true,
|
||||
drawerProps: {
|
||||
...curr.drawerProps,
|
||||
...props?.drawerProps,
|
||||
opened: true,
|
||||
onClose: curr.onClose,
|
||||
},
|
||||
primeData: props?.primeData,
|
||||
useFormProps: {
|
||||
...curr.useFormProps,
|
||||
...props?.useFormProps,
|
||||
},
|
||||
layoutProps: {
|
||||
...curr.layoutProps,
|
||||
...props?.layoutProps,
|
||||
},
|
||||
useQueryOptions: {
|
||||
...curr.useQueryOptions,
|
||||
...props?.useQueryOptions,
|
||||
},
|
||||
meta: {
|
||||
...curr.meta,
|
||||
...props?.meta,
|
||||
},
|
||||
useMutationOptions: {
|
||||
...curr.useMutationOptions,
|
||||
...props?.useMutationOptions,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
close: () =>
|
||||
setFormProps((curr) => ({
|
||||
...curr,
|
||||
opened: false,
|
||||
drawerProps: { ...curr.drawerProps, opened: false, onClose: curr.onClose },
|
||||
})),
|
||||
ask: (request: RequestType, buffer: any) => {
|
||||
setFormProps((curr) => ({
|
||||
...curr,
|
||||
request,
|
||||
value: buffer,
|
||||
opened: true,
|
||||
drawerProps: { ...curr.drawerProps, opened: true, onClose: curr.onClose },
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default useDrawerFormState
|
||||
export type { UseDrawerFormState }
|
||||
97
src/Form/hooks/use-modal-form-state.tsx
Normal file
97
src/Form/hooks/use-modal-form-state.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useState } from 'react'
|
||||
import { FieldValues } from 'react-hook-form'
|
||||
import { ModalProps } from '@mantine/core'
|
||||
import { SuperFormProps, RequestType } from '../types'
|
||||
|
||||
interface UseModalFormState<T extends FieldValues> extends Partial<SuperFormProps<T>> {
|
||||
modalProps: ModalProps
|
||||
opened?: boolean
|
||||
onClose?: () => void
|
||||
request: RequestType
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type AskFunction = (request: RequestType, buffer: any) => void
|
||||
|
||||
const useModalFormState = <T extends FieldValues>(
|
||||
props?: Partial<UseModalFormState<T>>
|
||||
): {
|
||||
formProps: UseModalFormState<T>
|
||||
setFormProps: React.Dispatch<React.SetStateAction<UseModalFormState<T>>>
|
||||
open: (props: Partial<UseModalFormState<T>>) => void
|
||||
close: () => void
|
||||
ask: AskFunction
|
||||
} => {
|
||||
const [formProps, setFormProps] = useState<UseModalFormState<T>>({
|
||||
opened: false,
|
||||
request: 'insert',
|
||||
...props,
|
||||
onClose: () =>
|
||||
setFormProps((curr) => ({
|
||||
...curr,
|
||||
opened: false,
|
||||
modalProps: { ...curr.modalProps, opened: false },
|
||||
})),
|
||||
modalProps: { opened: false, onClose: () => {}, ...props?.modalProps },
|
||||
})
|
||||
|
||||
return {
|
||||
formProps,
|
||||
setFormProps,
|
||||
open: (props?: Partial<UseModalFormState<T>>) => {
|
||||
setFormProps((curr) => {
|
||||
return {
|
||||
...curr,
|
||||
...props,
|
||||
request: props.request ?? curr.request,
|
||||
opened: true,
|
||||
modalProps: {
|
||||
...curr.modalProps,
|
||||
...props?.modalProps,
|
||||
opened: true,
|
||||
onClose: curr.onClose,
|
||||
},
|
||||
primeData: props?.primeData,
|
||||
useFormProps: {
|
||||
...curr.useFormProps,
|
||||
...props?.useFormProps,
|
||||
},
|
||||
layoutProps: {
|
||||
...curr.layoutProps,
|
||||
...props?.layoutProps,
|
||||
},
|
||||
useQueryOptions: {
|
||||
...curr.useQueryOptions,
|
||||
...props?.useQueryOptions,
|
||||
},
|
||||
meta: {
|
||||
...curr.meta,
|
||||
...props?.meta,
|
||||
},
|
||||
useMutationOptions: {
|
||||
...curr.useMutationOptions,
|
||||
...props?.useMutationOptions,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
close: () =>
|
||||
setFormProps((curr) => ({
|
||||
...curr,
|
||||
opened: false,
|
||||
modalProps: { ...curr.modalProps, opened: false, onClose: curr.onClose },
|
||||
})),
|
||||
ask: (request: RequestType, buffer: any) => {
|
||||
setFormProps((curr) => ({
|
||||
...curr,
|
||||
request,
|
||||
value: buffer,
|
||||
opened: true,
|
||||
modalProps: { ...curr.modalProps, opened: true, onClose: curr.onClose },
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default useModalFormState
|
||||
export type { UseModalFormState }
|
||||
97
src/Form/hooks/use-popover-form-state.tsx
Normal file
97
src/Form/hooks/use-popover-form-state.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useState } from 'react'
|
||||
import { FieldValues } from 'react-hook-form'
|
||||
import { PopoverProps } from '@mantine/core'
|
||||
import { SuperFormProps, RequestType } from '../types'
|
||||
|
||||
interface UsePopoverFormState<T extends FieldValues> extends Partial<SuperFormProps<T>> {
|
||||
popoverProps: Omit<PopoverProps, 'children'>
|
||||
opened?: boolean
|
||||
onClose?: () => void
|
||||
request: RequestType
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type AskFunction = (request: RequestType, buffer: any) => void
|
||||
|
||||
const usePopoverFormState = <T extends FieldValues>(
|
||||
props?: Partial<UsePopoverFormState<T>>
|
||||
): {
|
||||
formProps: UsePopoverFormState<T>
|
||||
setFormProps: React.Dispatch<React.SetStateAction<UsePopoverFormState<T>>>
|
||||
open: (props: Partial<UsePopoverFormState<T>>) => void
|
||||
close: () => void
|
||||
ask: AskFunction
|
||||
} => {
|
||||
const [formProps, setFormProps] = useState<UsePopoverFormState<T>>({
|
||||
opened: false,
|
||||
request: 'insert',
|
||||
...props,
|
||||
popoverProps: { opened: false, onClose: () => {}, ...props?.popoverProps },
|
||||
onClose: () =>
|
||||
setFormProps((curr) => ({
|
||||
...curr,
|
||||
opened: false,
|
||||
popoverProps: { ...curr.popoverProps, opened: false },
|
||||
})),
|
||||
})
|
||||
|
||||
return {
|
||||
formProps,
|
||||
setFormProps,
|
||||
open: (props?: Partial<UsePopoverFormState<T>>) => {
|
||||
setFormProps((curr) => {
|
||||
return {
|
||||
...curr,
|
||||
...props,
|
||||
request: props.request ?? curr.request,
|
||||
opened: true,
|
||||
popoverProps: {
|
||||
...curr.popoverProps,
|
||||
...props?.popoverProps,
|
||||
opened: true,
|
||||
onClose: curr.onClose,
|
||||
},
|
||||
primeData: props?.primeData,
|
||||
useFormProps: {
|
||||
...curr.useFormProps,
|
||||
...props?.useFormProps,
|
||||
},
|
||||
layoutProps: {
|
||||
...curr.layoutProps,
|
||||
...props?.layoutProps,
|
||||
},
|
||||
useQueryOptions: {
|
||||
...curr.useQueryOptions,
|
||||
...props?.useQueryOptions,
|
||||
},
|
||||
meta: {
|
||||
...curr.meta,
|
||||
...props?.meta,
|
||||
},
|
||||
useMutationOptions: {
|
||||
...curr.useMutationOptions,
|
||||
...props?.useMutationOptions,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
close: () =>
|
||||
setFormProps((curr) => ({
|
||||
...curr,
|
||||
opened: false,
|
||||
popoverProps: { ...curr.popoverProps, opened: false, onClose: curr.onClose },
|
||||
})),
|
||||
ask: (request: RequestType, buffer: any) => {
|
||||
setFormProps((curr) => ({
|
||||
...curr,
|
||||
request,
|
||||
value: buffer,
|
||||
opened: true,
|
||||
popoverProps: { ...curr.popoverProps, opened: true, onClose: curr.onClose },
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default usePopoverFormState
|
||||
export type { UsePopoverFormState }
|
||||
40
src/Form/hooks/use-subscribe.tsx
Normal file
40
src/Form/hooks/use-subscribe.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
EventType,
|
||||
FieldValues,
|
||||
FormState,
|
||||
InternalFieldName,
|
||||
Path,
|
||||
ReadFormState,
|
||||
useFormContext,
|
||||
UseFormReturn,
|
||||
} from 'react-hook-form'
|
||||
|
||||
const useSubscribe = <T extends FieldValues>(
|
||||
name: Path<T> | readonly Path<T>[] | undefined,
|
||||
callback: (
|
||||
data: Partial<FormState<T>> & {
|
||||
values: FieldValues
|
||||
name?: InternalFieldName
|
||||
type?: EventType
|
||||
},
|
||||
form?: UseFormReturn<T, any, T>
|
||||
) => void,
|
||||
formState?: ReadFormState,
|
||||
deps?: unknown[]
|
||||
) => {
|
||||
const form = useFormContext<T>()
|
||||
|
||||
return useEffect(() => {
|
||||
const unsubscribe = form.subscribe({
|
||||
name,
|
||||
callback: (data) => callback(data, form),
|
||||
formState: { values: true, ...formState },
|
||||
exact: true,
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [form.subscribe, ...(deps || [])])
|
||||
}
|
||||
|
||||
export default useSubscribe
|
||||
38
src/Form/hooks/use-super-form-state.tsx
Normal file
38
src/Form/hooks/use-super-form-state.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useState } from 'react'
|
||||
import { FieldValues } from 'react-hook-form'
|
||||
import { SuperFormProps, RequestType } from '../types'
|
||||
|
||||
interface UseSuperFormState<T extends FieldValues> extends Partial<SuperFormProps<T>> {
|
||||
request: RequestType
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type AskFunction = (request: RequestType, buffer: any) => void
|
||||
|
||||
const useSuperFormState = <T extends FieldValues>(
|
||||
props?: UseSuperFormState<T>
|
||||
): {
|
||||
formProps: UseSuperFormState<any>
|
||||
setFormProps: React.Dispatch<React.SetStateAction<UseSuperFormState<T>>>
|
||||
ask: AskFunction
|
||||
} => {
|
||||
const [formProps, setFormProps] = useState<UseSuperFormState<T>>({
|
||||
request: 'insert',
|
||||
...props,
|
||||
})
|
||||
|
||||
return {
|
||||
formProps,
|
||||
setFormProps,
|
||||
ask: (request: RequestType, buffer: any) => {
|
||||
setFormProps((curr) => ({
|
||||
...curr,
|
||||
request,
|
||||
value: buffer,
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default useSuperFormState
|
||||
export type { UseSuperFormState }
|
||||
123
src/Form/hooks/useRemote.tsx
Normal file
123
src/Form/hooks/useRemote.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useEffect, type MutableRefObject } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useFormContext, useFormState, type FieldValues } from 'react-hook-form'
|
||||
import { useStore } from '../store/SuperForm.store'
|
||||
import { useApiURL } from '../config/ApiConfig'
|
||||
import { getNestedValue } from '../utils/getNestedValue'
|
||||
import { fetchClient, type FetchResponse, FetchError } from '../utils/fetchClient'
|
||||
import type { GridRef } from '../types'
|
||||
|
||||
const useRemote = <T extends FieldValues>(gridRef?: MutableRefObject<GridRef<any> | null>) => {
|
||||
// Component store State
|
||||
const { onResetForm, remote, request, useMutationOptions, useQueryOptions, value } = useStore(
|
||||
(state) => ({
|
||||
onResetForm: state.onResetForm,
|
||||
remote: state.remote,
|
||||
request: state.request,
|
||||
useMutationOptions: state.useMutationOptions,
|
||||
useQueryOptions: state.useQueryOptions,
|
||||
value: state.value,
|
||||
})
|
||||
)
|
||||
|
||||
// Component Hooks
|
||||
const form = useFormContext<T>()
|
||||
const { isDirty } = useFormState({ control: form.control })
|
||||
|
||||
// Component use Effects
|
||||
const qc = useQueryClient()
|
||||
|
||||
// Get API URL from context or override
|
||||
const contextApiURL = useApiURL()
|
||||
|
||||
const id = remote?.primaryKey?.includes('.')
|
||||
? getNestedValue(remote?.primaryKey, value)
|
||||
: value?.[remote?.primaryKey ?? '']
|
||||
|
||||
const queryKey = useQueryOptions?.queryKey || [remote?.tableName, id]
|
||||
|
||||
const enabled =
|
||||
useQueryOptions?.enabled ||
|
||||
!!(
|
||||
(remote?.enabled ?? true) &&
|
||||
(remote?.tableName ?? '').length > 0 &&
|
||||
(request === 'change' || request === 'delete') &&
|
||||
String(id) !== 'undefined' &&
|
||||
(String(id)?.length ?? 0) > 0
|
||||
)
|
||||
|
||||
let url = remote?.apiURL ?? `${contextApiURL}/${remote?.tableName}`
|
||||
url = url?.endsWith('/') ? url.substring(0, url.length - 1) : url
|
||||
|
||||
const { isSuccess, status, data, isFetching } = useQuery<FetchResponse<T>>({
|
||||
queryKey,
|
||||
queryFn: () => fetchClient.get<T>(`${url}/${id}`, remote?.apiOptions),
|
||||
enabled,
|
||||
refetchOnMount: 'always',
|
||||
refetchOnReconnect: !isDirty,
|
||||
refetchOnWindowFocus: !isDirty,
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
...useQueryOptions,
|
||||
})
|
||||
|
||||
const changeMut = useMutation({
|
||||
// @ts-ignore
|
||||
mutationFn: (mutVal: T) => {
|
||||
if (!remote?.tableName || !remote?.primaryKey) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
return request === 'insert'
|
||||
? fetchClient.post(url, mutVal, remote?.apiOptions)
|
||||
: request === 'change'
|
||||
? fetchClient.post(`${url}/${id}`, mutVal, remote?.apiOptions)
|
||||
: request === 'delete'
|
||||
? fetchClient.delete(`${url}/${id}`, remote?.apiOptions)
|
||||
: Promise.resolve(null)
|
||||
},
|
||||
onSettled: (response: FetchResponse | null) => {
|
||||
qc?.invalidateQueries({ queryKey: [remote?.tableName] })
|
||||
|
||||
if (request !== 'delete' && response) {
|
||||
if (onResetForm) {
|
||||
onResetForm(response?.data, form).then(() => {
|
||||
form.reset(response?.data, { keepDirty: false })
|
||||
})
|
||||
} else {
|
||||
form.reset(response?.data, { keepDirty: false })
|
||||
}
|
||||
}
|
||||
|
||||
gridRef?.current?.refresh?.()
|
||||
// @ts-ignore
|
||||
gridRef?.current?.selectRow?.(response?.data?.[remote?.primaryKey ?? ''])
|
||||
},
|
||||
...useMutationOptions,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess && status === 'success' && enabled && !isFetching) {
|
||||
if (!Object.keys(data?.data ?? {}).includes(remote?.primaryKey ?? '')) {
|
||||
throw new Error('Primary key not found in remote data')
|
||||
}
|
||||
|
||||
if (onResetForm) {
|
||||
onResetForm(data?.data, form).then((resetData) => {
|
||||
form.reset(resetData)
|
||||
})
|
||||
} else {
|
||||
form.reset(data?.data)
|
||||
}
|
||||
}
|
||||
}, [isSuccess, status, enabled, isFetching])
|
||||
|
||||
return {
|
||||
error: changeMut.error as FetchError,
|
||||
isFetching: (enabled ? isFetching : false) || changeMut?.isPending,
|
||||
mutateAsync: changeMut.mutateAsync,
|
||||
queryKey,
|
||||
}
|
||||
}
|
||||
|
||||
export default useRemote
|
||||
48
src/Form/store/FormLayout.store.tsx
Normal file
48
src/Form/store/FormLayout.store.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { createContext, useContext, type ReactNode } from 'react'
|
||||
import { create } from 'zustand'
|
||||
import type { RequestType } from '../types'
|
||||
|
||||
interface FormLayoutState {
|
||||
request: RequestType
|
||||
loading: boolean
|
||||
dirty: boolean
|
||||
onCancel?: () => void
|
||||
onSubmit?: () => void
|
||||
setState: (key: string, value: any) => void
|
||||
}
|
||||
|
||||
const createFormLayoutStore = (initialProps: any) =>
|
||||
create<FormLayoutState>((set) => ({
|
||||
request: initialProps.request || 'insert',
|
||||
loading: initialProps.loading || false,
|
||||
dirty: initialProps.dirty || false,
|
||||
onCancel: initialProps.onCancel,
|
||||
onSubmit: initialProps.onSubmit,
|
||||
setState: (key, value) => set({ [key]: value }),
|
||||
}))
|
||||
|
||||
const FormLayoutStoreContext = createContext<ReturnType<typeof createFormLayoutStore> | null>(null)
|
||||
|
||||
export const FormLayoutStoreProvider: React.FC<{ children: ReactNode; [key: string]: any }> = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const storeRef = React.useRef<ReturnType<typeof createFormLayoutStore>>()
|
||||
if (!storeRef.current) {
|
||||
storeRef.current = createFormLayoutStore(props)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormLayoutStoreContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</FormLayoutStoreContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useFormLayoutStore = <T,>(selector: (state: FormLayoutState) => T): T => {
|
||||
const store = useContext(FormLayoutStoreContext)
|
||||
if (!store) {
|
||||
throw new Error('useFormLayoutStore must be used within FormLayoutStoreProvider')
|
||||
}
|
||||
return store(selector)
|
||||
}
|
||||
22
src/Form/store/SuperForm.store.tsx
Normal file
22
src/Form/store/SuperForm.store.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { SuperFormProviderProps } from '../types'
|
||||
import { createSyncStore } from '@warkypublic/zustandsyncstore'
|
||||
|
||||
const { Provider, useStore } = createSyncStore<any, SuperFormProviderProps>((set) => ({
|
||||
request: 'insert',
|
||||
setRequest: (request) => {
|
||||
set({ request })
|
||||
},
|
||||
|
||||
value: undefined,
|
||||
setValue: (value) => {
|
||||
set({ value })
|
||||
},
|
||||
|
||||
noCloseOnSubmit: false,
|
||||
setNoCloseOnSubmit: (noCloseOnSubmit) => {
|
||||
set({ noCloseOnSubmit })
|
||||
},
|
||||
}))
|
||||
|
||||
export { Provider, useStore }
|
||||
export const useSuperFormStore = useStore
|
||||
10
src/Form/styles/Form.module.css
Normal file
10
src/Form/styles/Form.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.sticky {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
135
src/Form/types/form.types.ts
Normal file
135
src/Form/types/form.types.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { UseMutationOptions, UseMutationResult, UseQueryOptions } from '@tanstack/react-query'
|
||||
import type { FieldValues, UseFormProps, UseFormReturn, UseFormStateReturn } from 'react-hook-form'
|
||||
import type { ModalProps, PaperProps, PopoverProps, DrawerProps } from '@mantine/core'
|
||||
import type { RemoteConfig } from './remote.types'
|
||||
|
||||
export type RequestType = 'insert' | 'change' | 'view' | 'select' | 'delete' | 'get' | 'set'
|
||||
|
||||
// Grid integration types (simplified - removes BTGlideRef dependency)
|
||||
export interface GridRef<T = any> {
|
||||
refresh?: () => void
|
||||
selectRow?: (id: any) => void
|
||||
}
|
||||
|
||||
export interface FormSectionBodyProps {
|
||||
// Add properties as needed from original FormLayout
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface FormSectionFooterProps {
|
||||
// Add properties as needed from original FormLayout
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface BodyRightSection<T extends FieldValues> {
|
||||
opened?: boolean
|
||||
setOpened?: (opened: boolean) => void
|
||||
w: number | string
|
||||
hideToggleButton?: boolean
|
||||
paperProps?: PaperProps
|
||||
render: (props: {
|
||||
form: UseFormReturn<any, any, undefined>
|
||||
formValue: T
|
||||
isFetching: boolean
|
||||
opened: boolean
|
||||
queryKey: any
|
||||
setOpened: (opened: boolean) => void
|
||||
}) => React.ReactNode
|
||||
}
|
||||
|
||||
export interface SuperFormLayoutProps<T extends FieldValues> {
|
||||
buttonTitles?: { submit?: string; cancel?: string }
|
||||
extraButtons?: React.ReactNode | ((form: UseFormReturn<any, any, undefined>) => React.ReactNode)
|
||||
noFooter?: boolean
|
||||
noHeader?: boolean
|
||||
noLayout?: boolean
|
||||
bodySectionProps?: Partial<FormSectionBodyProps>
|
||||
footerSectionProps?:
|
||||
| Partial<FormSectionFooterProps>
|
||||
| ((ref: React.RefObject<SuperFormRef<T>>) => Partial<FormSectionFooterProps>)
|
||||
rightSection?: React.ReactNode
|
||||
bodyRightSection?: BodyRightSection<T>
|
||||
title?: string
|
||||
showErrorList?: boolean
|
||||
}
|
||||
|
||||
export interface CommonFormProps<T extends FieldValues> {
|
||||
gridRef?: React.MutableRefObject<GridRef<any> | null>
|
||||
layoutProps?: SuperFormLayoutProps<T>
|
||||
meta?: { [key: string]: any }
|
||||
nested?: boolean
|
||||
onCancel?: (request: RequestType) => void
|
||||
onLayoutMounted?: () => void
|
||||
onLayoutUnMounted?: () => void
|
||||
onResetForm?: (data: T, form?: UseFormReturn<any, any, undefined>) => Promise<T>
|
||||
onBeforeSubmit?: (
|
||||
data: T,
|
||||
request: RequestType,
|
||||
form?: UseFormReturn<any, any, undefined>
|
||||
) => Promise<T>
|
||||
onSubmit?: (
|
||||
data: T,
|
||||
request: RequestType,
|
||||
formData?: T,
|
||||
form?: UseFormReturn<any, any, undefined>,
|
||||
closeForm?: boolean
|
||||
) => void
|
||||
primeData?: any
|
||||
readonly?: boolean
|
||||
remote?: RemoteConfig
|
||||
request: RequestType
|
||||
persist?: boolean | { storageKey?: string }
|
||||
useMutationOptions?: UseMutationOptions<any, Error, T, unknown>
|
||||
useQueryOptions?: Partial<UseQueryOptions<any, Error, T>>
|
||||
value?: T | null
|
||||
}
|
||||
|
||||
export interface SuperFormProps<T extends FieldValues> extends CommonFormProps<T> {
|
||||
children: React.ReactNode | ((props: UseFormReturn<T, any, undefined>) => React.ReactNode)
|
||||
useFormProps?: UseFormProps<T>
|
||||
}
|
||||
|
||||
export interface SuperFormProviderProps extends Omit<SuperFormProps<any>, 'children'> {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export interface SuperFormModalProps<T extends FieldValues> extends SuperFormProps<T> {
|
||||
modalProps: ModalProps
|
||||
noCloseOnSubmit?: boolean
|
||||
}
|
||||
|
||||
export interface SuperFormPopoverProps<T extends FieldValues> extends SuperFormProps<T> {
|
||||
popoverProps?: Omit<PopoverProps, 'children'>
|
||||
target: any
|
||||
noCloseOnSubmit?: boolean
|
||||
}
|
||||
|
||||
export interface ExtendedDrawerProps extends DrawerProps {
|
||||
// Add any extended drawer props needed
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface SuperFormDrawerProps<T extends FieldValues> extends SuperFormProps<T> {
|
||||
drawerProps: ExtendedDrawerProps
|
||||
noCloseOnSubmit?: boolean
|
||||
}
|
||||
|
||||
export interface SuperFormRef<T extends FieldValues> {
|
||||
form: UseFormReturn<T, any, undefined>
|
||||
mutation: Partial<UseMutationResult<any, Error, any, unknown>>
|
||||
submit: (closeForm?: boolean, afterSubmit?: (data: T | any) => void) => Promise<void>
|
||||
queryKey?: any
|
||||
getFormState: () => UseFormStateReturn<T>
|
||||
}
|
||||
|
||||
export interface SuperFormDrawerRef<T extends FieldValues> extends SuperFormRef<T> {
|
||||
drawer: HTMLDivElement | null
|
||||
}
|
||||
|
||||
export interface SuperFormModalRef<T extends FieldValues> extends SuperFormRef<T> {
|
||||
modal: HTMLDivElement | null
|
||||
}
|
||||
|
||||
export interface SuperFormPopoverRef<T extends FieldValues> extends SuperFormRef<T> {
|
||||
popover: HTMLDivElement | null
|
||||
}
|
||||
2
src/Form/types/index.ts
Normal file
2
src/Form/types/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './form.types'
|
||||
export * from './remote.types'
|
||||
11
src/Form/types/remote.types.ts
Normal file
11
src/Form/types/remote.types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface RemoteConfig {
|
||||
apiOptions?: RequestInit
|
||||
apiURL?: string
|
||||
enabled?: boolean
|
||||
fetchSize?: number
|
||||
hotFields?: string[]
|
||||
primaryKey?: string
|
||||
sqlFilter?: string
|
||||
tableName: string
|
||||
uniqueKeys?: string[]
|
||||
}
|
||||
161
src/Form/utils/fetchClient.ts
Normal file
161
src/Form/utils/fetchClient.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
export interface FetchOptions extends RequestInit {
|
||||
params?: Record<string, any>
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export interface FetchResponse<T = any> {
|
||||
data: T
|
||||
status: number
|
||||
statusText: string
|
||||
ok: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
export class FetchError extends Error {
|
||||
constructor(
|
||||
public message: string,
|
||||
public status?: number,
|
||||
public response?: any
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'FetchError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch wrapper with timeout support and axios-like interface
|
||||
*/
|
||||
async function fetchWithTimeout(
|
||||
url: string,
|
||||
options: FetchOptions = {}
|
||||
): Promise<Response> {
|
||||
const { timeout = 30000, ...fetchOptions } = options
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...fetchOptions,
|
||||
signal: controller.signal,
|
||||
})
|
||||
clearTimeout(timeoutId)
|
||||
return response
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request
|
||||
*/
|
||||
export async function get<T = any>(
|
||||
url: string,
|
||||
options?: FetchOptions
|
||||
): Promise<FetchResponse<T>> {
|
||||
try {
|
||||
const response = await fetchWithTimeout(url, {
|
||||
...options,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
data,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
ok: response.ok,
|
||||
error: response.ok ? undefined : data?.message || data?.error || response.statusText,
|
||||
}
|
||||
} catch (error) {
|
||||
throw new FetchError(
|
||||
error instanceof Error ? error.message : 'Network request failed',
|
||||
undefined,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request
|
||||
*/
|
||||
export async function post<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
options?: FetchOptions
|
||||
): Promise<FetchResponse<T>> {
|
||||
try {
|
||||
const response = await fetchWithTimeout(url, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
const responseData = await response.json()
|
||||
|
||||
return {
|
||||
data: responseData,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
ok: response.ok,
|
||||
error: response.ok ? undefined : responseData?.message || responseData?.error || response.statusText,
|
||||
}
|
||||
} catch (error) {
|
||||
throw new FetchError(
|
||||
error instanceof Error ? error.message : 'Network request failed',
|
||||
undefined,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE request
|
||||
*/
|
||||
export async function del<T = any>(
|
||||
url: string,
|
||||
options?: FetchOptions
|
||||
): Promise<FetchResponse<T>> {
|
||||
try {
|
||||
const response = await fetchWithTimeout(url, {
|
||||
...options,
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await response.json().catch(() => ({}))
|
||||
|
||||
return {
|
||||
data,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
ok: response.ok,
|
||||
error: response.ok ? undefined : data?.message || data?.error || response.statusText,
|
||||
}
|
||||
} catch (error) {
|
||||
throw new FetchError(
|
||||
error instanceof Error ? error.message : 'Network request failed',
|
||||
undefined,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchClient = {
|
||||
get,
|
||||
post,
|
||||
delete: del,
|
||||
}
|
||||
9
src/Form/utils/getNestedValue.ts
Normal file
9
src/Form/utils/getNestedValue.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Retrieves a nested value from an object using dot notation path
|
||||
* @param path - Dot-separated path (e.g., "user.address.city")
|
||||
* @param obj - Object to extract value from
|
||||
* @returns The value at the specified path, or undefined if not found
|
||||
*/
|
||||
export const getNestedValue = (path: string, obj: any): any => {
|
||||
return path.split('.').reduce((prev, curr) => prev?.[curr], obj)
|
||||
}
|
||||
30
src/Form/utils/openConfirmModal.ts
Normal file
30
src/Form/utils/openConfirmModal.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import { Stack, Text } from '@mantine/core'
|
||||
import { modals } from '@mantine/modals'
|
||||
|
||||
export const openConfirmModal = (
|
||||
onConfirm: () => void,
|
||||
onCancel?: (() => void) | null,
|
||||
description?: string | null
|
||||
) =>
|
||||
modals.openConfirmModal({
|
||||
size: 'xs',
|
||||
children: (
|
||||
<Stack gap={4}>
|
||||
<Text size='xs' c={description ? 'blue' : 'red'} fw='bold'>
|
||||
You have unsaved changes in this form.
|
||||
</Text>
|
||||
<Text size='xs'>
|
||||
{description ??
|
||||
'Closing now will discard any modifications you have made. Are you sure you want to continue?'}
|
||||
</Text>
|
||||
</Stack>
|
||||
),
|
||||
labels: { confirm: description ? 'Restore' : 'Confirm', cancel: 'Cancel' },
|
||||
confirmProps: { color: description ? 'blue' : 'red', size: 'compact-xs' },
|
||||
cancelProps: { size: 'compact-xs' },
|
||||
groupProps: { gap: 'xs' },
|
||||
withCloseButton: false,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
})
|
||||
Reference in New Issue
Block a user