Compare commits
10 Commits
rw
...
bc7262cede
| Author | SHA1 | Date | |
|---|---|---|---|
| bc7262cede | |||
| 0825f739f4 | |||
| 0bd642e2d2 | |||
| 7cc09d6acb | |||
| 9df2f3b504 | |||
| e777e1fa3a | |||
| cd2f6db880 | |||
| e6507f44af | |||
| 400a193a58 | |||
| d935c6cf28 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
|||||||
# @warkypublic/zustandsyncstore
|
# @warkypublic/zustandsyncstore
|
||||||
|
|
||||||
|
## 0.0.25
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 0825f73: Bump
|
||||||
|
|
||||||
|
## 0.0.24
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 7cc09d6: Added form controllers - New button and input controller components for the FormerControllers module
|
||||||
|
|
||||||
## 0.0.23
|
## 0.0.23
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@warkypublic/oranguru",
|
"name": "@warkypublic/oranguru",
|
||||||
"author": "Warky Devs",
|
"author": "Warky Devs",
|
||||||
"version": "0.0.23",
|
"version": "0.0.25",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -49,9 +49,7 @@
|
|||||||
"./oranguru.css": "./src/oranguru.css"
|
"./oranguru.css": "./src/oranguru.css"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
||||||
"moment": "^2.30.1"
|
"moment": "^2.30.1"
|
||||||
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.29.7",
|
"@changesets/cli": "^2.29.7",
|
||||||
@@ -102,7 +100,6 @@
|
|||||||
"@warkypublic/artemis-kit": "^1.0.10",
|
"@warkypublic/artemis-kit": "^1.0.10",
|
||||||
"@warkypublic/zustandsyncstore": "^0.0.4",
|
"@warkypublic/zustandsyncstore": "^0.0.4",
|
||||||
"react-hook-form": "^7.71.0",
|
"react-hook-form": "^7.71.0",
|
||||||
|
|
||||||
"immer": "^10.1.3",
|
"immer": "^10.1.3",
|
||||||
"react": ">= 19.0.0",
|
"react": ">= 19.0.0",
|
||||||
"react-dom": ">= 19.0.0",
|
"react-dom": ">= 19.0.0",
|
||||||
|
|||||||
0
src/Boxer/index.ts
Normal file
0
src/Boxer/index.ts
Normal file
8
src/Boxer/todo.md
Normal file
8
src/Boxer/todo.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
- [ ] Auto Complete
|
||||||
|
- [ ] Multi Select
|
||||||
|
- [ ] Virtualize
|
||||||
|
- [ ] Search
|
||||||
|
- [ ] Clear, Menu buttons
|
||||||
|
- [ ] Headerspec API
|
||||||
|
- [ ] Relspec API
|
||||||
|
- [ ] SocketSpec API
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
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 }
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
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 }
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
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 }
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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 }
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
.disabled {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticky {
|
|
||||||
position: -webkit-sticky;
|
|
||||||
position: sticky;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './form.types'
|
|
||||||
export * from './remote.types'
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export interface RemoteConfig {
|
|
||||||
apiOptions?: RequestInit
|
|
||||||
apiURL?: string
|
|
||||||
enabled?: boolean
|
|
||||||
fetchSize?: number
|
|
||||||
hotFields?: string[]
|
|
||||||
primaryKey?: string
|
|
||||||
sqlFilter?: string
|
|
||||||
tableName: string
|
|
||||||
uniqueKeys?: string[]
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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,
|
|
||||||
})
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { newUUID } from '@warkypublic/artemis-kit';
|
||||||
import { createSyncStore } from '@warkypublic/zustandsyncstore';
|
import { createSyncStore } from '@warkypublic/zustandsyncstore';
|
||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
load: async (reset?: boolean) => {
|
load: async (reset?: boolean) => {
|
||||||
try {
|
try {
|
||||||
set({ loading: true });
|
set({ loading: true });
|
||||||
const keyName = get()?.apiKeyField || 'id';
|
const keyName = get()?.uniqueKeyField || 'id';
|
||||||
const keyValue = (get().values as any)?.[keyName] ?? (get().primeData as any)?.[keyName];
|
const keyValue = (get().values as any)?.[keyName] ?? (get().primeData as any)?.[keyName];
|
||||||
if (get().onAPICall && keyValue !== undefined) {
|
if (get().onAPICall && keyValue !== undefined) {
|
||||||
let data = await get().onAPICall!(
|
let data = await get().onAPICall!(
|
||||||
@@ -97,7 +98,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (get().onAPICall) {
|
if (get().onAPICall) {
|
||||||
const keyName = get()?.apiKeyField || 'id';
|
const keyName = get()?.uniqueKeyField || 'id';
|
||||||
const keyValue =
|
const keyValue =
|
||||||
(get().values as any)?.[keyName] ?? (get().primeData as any)?.[keyName];
|
(get().values as any)?.[keyName] ?? (get().primeData as any)?.[keyName];
|
||||||
const savedData = await get().onAPICall!(
|
const savedData = await get().onAPICall!(
|
||||||
@@ -111,6 +112,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
}
|
}
|
||||||
set({ loading: false, values: savedData });
|
set({ loading: false, values: savedData });
|
||||||
get().onChange?.(savedData);
|
get().onChange?.(savedData);
|
||||||
|
formMethods.reset(savedData); //reset with saved data to clear dirty state
|
||||||
if (!keepOpen) {
|
if (!keepOpen) {
|
||||||
get().onClose?.(savedData);
|
get().onClose?.(savedData);
|
||||||
}
|
}
|
||||||
@@ -118,6 +120,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
}
|
}
|
||||||
|
|
||||||
set({ loading: false, values: data });
|
set({ loading: false, values: data });
|
||||||
|
formMethods.reset(data); //reset with saved data to clear dirty state
|
||||||
get().onChange?.(data);
|
get().onChange?.(data);
|
||||||
if (!keepOpen) {
|
if (!keepOpen) {
|
||||||
get().onClose?.(data);
|
get().onClose?.(data);
|
||||||
@@ -168,17 +171,38 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
},
|
},
|
||||||
values: undefined,
|
values: undefined,
|
||||||
}),
|
}),
|
||||||
({ onConfirmDelete, primeData, request, values }) => {
|
({ id, onClose, onConfirmDelete, primeData, request, useStoreApi, values }) => {
|
||||||
let _onConfirmDelete = onConfirmDelete;
|
let _onConfirmDelete = onConfirmDelete;
|
||||||
if (!onConfirmDelete) {
|
if (!onConfirmDelete) {
|
||||||
_onConfirmDelete = async () => {
|
_onConfirmDelete = async () => {
|
||||||
return confirm('Are you sure you want to delete this item?');
|
return confirm('Are you sure you want to delete this item?');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
id: !id ? newUUID() : id,
|
||||||
|
onClose: () => {
|
||||||
|
const dirty = useStoreApi.getState().dirty;
|
||||||
|
const setState = useStoreApi.getState().setState;
|
||||||
|
if (dirty) {
|
||||||
|
if (confirm('You have unsaved changes. Are you sure you want to close?')) {
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
setState('opened', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
setState('opened', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
onConfirmDelete: _onConfirmDelete,
|
onConfirmDelete: _onConfirmDelete,
|
||||||
primeData,
|
primeData,
|
||||||
request: request || 'insert',
|
request: (request || 'insert').replace('change', 'update'),
|
||||||
values: { ...primeData, ...values },
|
values: { ...primeData, ...values },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,11 +78,20 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
|
|||||||
return await validate();
|
return await validate();
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[getState, onChange]
|
[getState, onChange, validate, save, reset, setState, onClose, onOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState('getFormMethods', () => formMethods);
|
setState('getFormMethods', () => formMethods);
|
||||||
|
|
||||||
|
if (formMethods) {
|
||||||
|
formMethods.subscribe({
|
||||||
|
callback: ({ isDirty }) => {
|
||||||
|
setState('dirty', isDirty);
|
||||||
|
},
|
||||||
|
formState: { isDirty: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
}, [formMethods]);
|
}, [formMethods]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,37 +1,53 @@
|
|||||||
import type { LoadingOverlayProps, ScrollAreaAutosizeProps } from '@mantine/core';
|
import type {
|
||||||
|
ButtonProps,
|
||||||
|
GroupProps,
|
||||||
|
LoadingOverlayProps,
|
||||||
|
ScrollAreaAutosizeProps,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import type React from 'react';
|
||||||
import type { FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form';
|
import type { FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form';
|
||||||
|
|
||||||
export interface FormerProps<T extends FieldValues = any> {
|
export type FormerAPICallType<T extends FieldValues = any> = (
|
||||||
afterGet?: (data: T) => Promise<T> | void;
|
|
||||||
afterSave?: (data: T) => Promise<void> | void;
|
|
||||||
apiKeyField?: string;
|
|
||||||
beforeSave?: (data: T) => Promise<T> | T;
|
|
||||||
disableHTMlForm?: boolean;
|
|
||||||
keepOpen?: boolean;
|
|
||||||
onAPICall?: (
|
|
||||||
mode: 'mutate' | 'read',
|
mode: 'mutate' | 'read',
|
||||||
request: RequestType,
|
request: RequestType,
|
||||||
value?: T,
|
value?: T,
|
||||||
key?: number | string
|
key?: number | string
|
||||||
) => Promise<T>;
|
) => Promise<T>;
|
||||||
|
|
||||||
|
export interface FormerProps<T extends FieldValues = any> {
|
||||||
|
afterGet?: (data: T) => Promise<T> | void;
|
||||||
|
afterSave?: (data: T) => Promise<void> | void;
|
||||||
|
beforeSave?: (data: T) => Promise<T> | T;
|
||||||
|
dirty?: boolean;
|
||||||
|
disableHTMlForm?: boolean;
|
||||||
|
id?: string;
|
||||||
|
keepOpen?: boolean;
|
||||||
|
layout?: {
|
||||||
|
buttonAreaGroupProps?: GroupProps;
|
||||||
|
buttonOnTop?: boolean;
|
||||||
|
closeButtonProps?: ButtonProps;
|
||||||
|
closeButtonTitle?: React.ReactNode;
|
||||||
|
renderBottom?: FormerSectionRender<T>;
|
||||||
|
renderTop?: FormerSectionRender<T>;
|
||||||
|
saveButtonProps?: ButtonProps;
|
||||||
|
saveButtonTitle?: React.ReactNode;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
onAPICall?: FormerAPICallType<T>;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onChange?: (value: T) => void;
|
onChange?: (value: T) => void;
|
||||||
onClose?: (data?: T) => void;
|
onClose?: (data?: T) => void;
|
||||||
onConfirmDelete?: (values?: T) => Promise<boolean>;
|
onConfirmDelete?: (values?: T) => Promise<boolean>;
|
||||||
onOpen?: (data?: T) => void;
|
|
||||||
|
|
||||||
|
onOpen?: (data?: T) => void;
|
||||||
opened?: boolean;
|
opened?: boolean;
|
||||||
primeData?: T;
|
primeData?: T;
|
||||||
request: RequestType;
|
request: RequestType;
|
||||||
|
uniqueKeyField?: string;
|
||||||
useFormProps?: UseFormProps<T>;
|
useFormProps?: UseFormProps<T>;
|
||||||
values?: T;
|
values?: T;
|
||||||
wrapper?: (
|
|
||||||
children: React.ReactNode,
|
wrapper?: FormerSectionRender<T>;
|
||||||
opened: boolean | undefined,
|
|
||||||
onClose: ((data?: T) => void) | undefined,
|
|
||||||
onOpen: ((data?: T) => void) | undefined,
|
|
||||||
getState: <K extends keyof FormStateAndProps<T>>(key: K) => FormStateAndProps<T>[K]
|
|
||||||
) => React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormerRef<T extends FieldValues = any> {
|
export interface FormerRef<T extends FieldValues = any> {
|
||||||
@@ -44,6 +60,14 @@ export interface FormerRef<T extends FieldValues = any> {
|
|||||||
validate: () => Promise<boolean>;
|
validate: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FormerSectionRender<T extends FieldValues = any> = (
|
||||||
|
children: React.ReactNode,
|
||||||
|
opened: boolean | undefined,
|
||||||
|
onClose: ((data?: T) => void) | undefined,
|
||||||
|
onOpen: ((data?: T) => void) | undefined,
|
||||||
|
getState: FormerState<T>['getState']
|
||||||
|
) => React.ReactNode;
|
||||||
|
|
||||||
export interface FormerState<T extends FieldValues = any> {
|
export interface FormerState<T extends FieldValues = any> {
|
||||||
deleteConfirmed?: boolean;
|
deleteConfirmed?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|||||||
85
src/Former/FormerButtonArea.tsx
Normal file
85
src/Former/FormerButtonArea.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { Button, Group, Tooltip } from '@mantine/core';
|
||||||
|
import { IconDeviceFloppy, IconX } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { useFormerStore } from './Former.store';
|
||||||
|
|
||||||
|
export const FormerButtonArea = () => {
|
||||||
|
const {
|
||||||
|
buttonAreaGroupProps,
|
||||||
|
closeButtonProps,
|
||||||
|
closeButtonTitle,
|
||||||
|
dirty,
|
||||||
|
onClose,
|
||||||
|
request,
|
||||||
|
save,
|
||||||
|
saveButtonProps,
|
||||||
|
saveButtonTitle,
|
||||||
|
} = useFormerStore((state) => ({
|
||||||
|
buttonAreaGroupProps: state.layout?.buttonAreaGroupProps,
|
||||||
|
closeButtonProps: state.layout?.closeButtonProps,
|
||||||
|
closeButtonTitle: state.layout?.closeButtonTitle,
|
||||||
|
dirty: state.dirty,
|
||||||
|
onClose: state.onClose,
|
||||||
|
request: state.request,
|
||||||
|
save: state.save,
|
||||||
|
saveButtonProps: state.layout?.saveButtonProps,
|
||||||
|
saveButtonTitle: state.layout?.saveButtonTitle,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const disabledSave =
|
||||||
|
['select', 'view'].includes(request || '') || (['update'].includes(request || '') && !dirty);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
justify="center"
|
||||||
|
p="xs"
|
||||||
|
style={{ boxShadow: '2px 2px 5px rgba(47, 47, 47, 0.1)' }}
|
||||||
|
w="100%"
|
||||||
|
{...buttonAreaGroupProps}
|
||||||
|
>
|
||||||
|
<Group grow justify="space-evenly">
|
||||||
|
{typeof onClose === 'function' && (
|
||||||
|
<Button
|
||||||
|
color="orange"
|
||||||
|
leftSection={<IconX />}
|
||||||
|
miw={'8rem'}
|
||||||
|
px="md"
|
||||||
|
size="sm"
|
||||||
|
{...closeButtonProps}
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{closeButtonTitle || 'Close'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Tooltip
|
||||||
|
label={
|
||||||
|
disabledSave ? (
|
||||||
|
<p>
|
||||||
|
Cannot save in view or select mode, or no changes made. <br />
|
||||||
|
Try changing some values.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p>Save the current record</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
bg={request === 'delete' ? 'red' : undefined}
|
||||||
|
color="green"
|
||||||
|
leftSection={<IconDeviceFloppy />}
|
||||||
|
miw={'8rem'}
|
||||||
|
px="md"
|
||||||
|
size="sm"
|
||||||
|
{...saveButtonProps}
|
||||||
|
disabled={disabledSave}
|
||||||
|
onClick={() => save()}
|
||||||
|
>
|
||||||
|
{saveButtonTitle || 'Save'}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,14 +2,18 @@ import { LoadingOverlay, ScrollAreaAutosize } from '@mantine/core';
|
|||||||
import { type PropsWithChildren, useEffect } from 'react';
|
import { type PropsWithChildren, useEffect } from 'react';
|
||||||
|
|
||||||
import { useFormerStore } from './Former.store';
|
import { useFormerStore } from './Former.store';
|
||||||
|
import { FormerLayoutBottom } from './FormerLayoutBottom';
|
||||||
|
import { FormerLayoutTop } from './FormerLayoutTop';
|
||||||
|
|
||||||
export const FormerLayout = (props: PropsWithChildren) => {
|
export const FormerLayout = (props: PropsWithChildren) => {
|
||||||
const {
|
const {
|
||||||
disableHTMlForm,
|
disableHTMlForm,
|
||||||
getFormMethods,
|
getFormMethods,
|
||||||
|
id,
|
||||||
load,
|
load,
|
||||||
loading,
|
loading,
|
||||||
loadingOverlayProps,
|
loadingOverlayProps,
|
||||||
|
opened,
|
||||||
request,
|
request,
|
||||||
reset,
|
reset,
|
||||||
save,
|
save,
|
||||||
@@ -17,12 +21,15 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
|||||||
} = useFormerStore((state) => ({
|
} = useFormerStore((state) => ({
|
||||||
disableHTMlForm: state.disableHTMlForm,
|
disableHTMlForm: state.disableHTMlForm,
|
||||||
getFormMethods: state.getFormMethods,
|
getFormMethods: state.getFormMethods,
|
||||||
|
id: state.id,
|
||||||
load: state.load,
|
load: state.load,
|
||||||
loading: state.loading,
|
loading: state.loading,
|
||||||
loadingOverlayProps: state.loadingOverlayProps,
|
loadingOverlayProps: state.loadingOverlayProps,
|
||||||
|
opened: state.opened,
|
||||||
request: state.request,
|
request: state.request,
|
||||||
reset: state.reset,
|
reset: state.reset,
|
||||||
save: state.save,
|
save: state.save,
|
||||||
|
|
||||||
scrollAreaProps: state.scrollAreaProps,
|
scrollAreaProps: state.scrollAreaProps,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -33,10 +40,11 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
|||||||
load(true);
|
load(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [getFormMethods, request]);
|
}, [getFormMethods, request, opened]);
|
||||||
|
|
||||||
if (disableHTMlForm) {
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<FormerLayoutTop />
|
||||||
<ScrollAreaAutosize
|
<ScrollAreaAutosize
|
||||||
offsetScrollbars
|
offsetScrollbars
|
||||||
scrollbarSize={4}
|
scrollbarSize={4}
|
||||||
@@ -44,50 +52,39 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
|||||||
{...scrollAreaProps}
|
{...scrollAreaProps}
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
maxHeight: '89vh',
|
|
||||||
padding: '0.25rem',
|
padding: '0.25rem',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
...scrollAreaProps?.style,
|
...scrollAreaProps?.style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{disableHTMlForm ? (
|
||||||
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
|
<div key={`former_d${id}`} x-data-request={request}>
|
||||||
{props.children}
|
{props.children}
|
||||||
<LoadingOverlay
|
</div>
|
||||||
loaderProps={{ type: 'bars' }}
|
) : (
|
||||||
overlayProps={{
|
<form
|
||||||
backgroundOpacity: 0.5,
|
id={`former_f${id}`}
|
||||||
}}
|
key={`former_${id}`}
|
||||||
{...loadingOverlayProps}
|
onReset={(e) => reset(e)}
|
||||||
visible={loading}
|
onSubmit={(e) => save(e)}
|
||||||
/>
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
</ScrollAreaAutosize>
|
x-data-request={request}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollAreaAutosize
|
|
||||||
offsetScrollbars
|
|
||||||
scrollbarSize={4}
|
|
||||||
type="auto"
|
|
||||||
{...scrollAreaProps}
|
|
||||||
style={{
|
|
||||||
height: '100%',
|
|
||||||
maxHeight: '89vh',
|
|
||||||
padding: '0.25rem',
|
|
||||||
width: '100%',
|
|
||||||
...scrollAreaProps?.style,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<form onReset={(e) => reset(e)} onSubmit={(e) => save(e)}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
<LoadingOverlay
|
|
||||||
loaderProps={{ type: 'bars' }}
|
|
||||||
overlayProps={{
|
|
||||||
backgroundOpacity: 0.5,
|
|
||||||
}}
|
|
||||||
{...loadingOverlayProps}
|
|
||||||
visible={loading}
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<LoadingOverlay
|
||||||
|
loaderProps={{ type: 'bars' }}
|
||||||
|
overlayProps={{
|
||||||
|
backgroundOpacity: 0.5,
|
||||||
|
}}
|
||||||
|
{...loadingOverlayProps}
|
||||||
|
visible={loading}
|
||||||
|
/>
|
||||||
</ScrollAreaAutosize>
|
</ScrollAreaAutosize>
|
||||||
|
<FormerLayoutBottom />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
23
src/Former/FormerLayoutBottom.tsx
Normal file
23
src/Former/FormerLayoutBottom.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useFormerStore } from './Former.store';
|
||||||
|
import { FormerButtonArea } from './FormerButtonArea';
|
||||||
|
|
||||||
|
export const FormerLayoutBottom = () => {
|
||||||
|
const { buttonOnTop, getState, opened, renderBottom } = useFormerStore((state) => ({
|
||||||
|
buttonOnTop: state.layout?.buttonOnTop,
|
||||||
|
getState: state.getState,
|
||||||
|
opened: state.opened,
|
||||||
|
renderBottom: state.layout?.renderBottom,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (renderBottom) {
|
||||||
|
return renderBottom(
|
||||||
|
<FormerButtonArea />,
|
||||||
|
opened,
|
||||||
|
getState('onClose'),
|
||||||
|
getState('onOpen'),
|
||||||
|
getState
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buttonOnTop ? <></> : <FormerButtonArea />;
|
||||||
|
};
|
||||||
22
src/Former/FormerLayoutTop.tsx
Normal file
22
src/Former/FormerLayoutTop.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useFormerStore } from './Former.store';
|
||||||
|
import { FormerButtonArea } from './FormerButtonArea';
|
||||||
|
|
||||||
|
export const FormerLayoutTop = () => {
|
||||||
|
const { buttonOnTop, getState, opened, renderTop } = useFormerStore((state) => ({
|
||||||
|
buttonOnTop: state.layout?.buttonOnTop,
|
||||||
|
getState: state.getState,
|
||||||
|
opened: state.opened,
|
||||||
|
renderTop: state.layout?.renderTop,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (renderTop) {
|
||||||
|
return renderTop(
|
||||||
|
<FormerButtonArea />,
|
||||||
|
opened,
|
||||||
|
getState('onClose'),
|
||||||
|
getState('onOpen'),
|
||||||
|
getState
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return buttonOnTop ? <FormerButtonArea /> : <></>;
|
||||||
|
};
|
||||||
72
src/Former/FormerResolveSpecAPI.ts
Normal file
72
src/Former/FormerResolveSpecAPI.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import type { FormerAPICallType } from './Former.types';
|
||||||
|
|
||||||
|
interface ResolveSpecRequest {
|
||||||
|
data?: Record<string, any>;
|
||||||
|
operation: 'create' | 'delete' | 'read' | 'update';
|
||||||
|
options?: {
|
||||||
|
columns?: string[];
|
||||||
|
computedColumns?: any[];
|
||||||
|
customOperators?: any[];
|
||||||
|
filters?: Array<{ column: string; operator: string; value: any }>;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
preload?: string[];
|
||||||
|
sort?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormerResolveSpecAPI(options: {
|
||||||
|
authToken: string;
|
||||||
|
fetchOptions?: Partial<RequestInit>;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
url: string;
|
||||||
|
}): FormerAPICallType {
|
||||||
|
return async (mode, request, value, key) => {
|
||||||
|
const baseUrl = options.url.replace(/\/$/, '');
|
||||||
|
|
||||||
|
// Build URL: /[schema]/[table_or_entity]/[id]
|
||||||
|
let url = `${baseUrl}`;
|
||||||
|
if (request !== 'insert' && key) {
|
||||||
|
url = `${url}/${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build ResolveSpec request body
|
||||||
|
const resolveSpecRequest: ResolveSpecRequest = {
|
||||||
|
operation:
|
||||||
|
mode === 'read'
|
||||||
|
? 'read'
|
||||||
|
: request === 'delete'
|
||||||
|
? 'delete'
|
||||||
|
: request === 'update'
|
||||||
|
? 'update'
|
||||||
|
: 'create',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mode === 'mutate') {
|
||||||
|
resolveSpecRequest.data = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
cache: 'no-cache',
|
||||||
|
signal: options.signal,
|
||||||
|
...options.fetchOptions,
|
||||||
|
body: JSON.stringify(resolveSpecRequest),
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${options.authToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.fetchOptions?.headers,
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, fetchOptions);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed with status ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data as any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FormerResolveSpecAPI, type ResolveSpecRequest };
|
||||||
50
src/Former/FormerRestHeadSpecAPI.ts
Normal file
50
src/Former/FormerRestHeadSpecAPI.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { FormerAPICallType } from './Former.types';
|
||||||
|
|
||||||
|
function FormerRestHeadSpecAPI(options: {
|
||||||
|
authToken: string;
|
||||||
|
fetchOptions?: Partial<RequestInit>;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
url: string;
|
||||||
|
}): FormerAPICallType {
|
||||||
|
return async (mode, request, value, key) => {
|
||||||
|
const baseUrl = options.url ?? ''; // Remove trailing slashes
|
||||||
|
let url = baseUrl;
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
cache: 'no-cache',
|
||||||
|
signal: options.signal,
|
||||||
|
...options.fetchOptions,
|
||||||
|
body: mode === 'mutate' && request !== 'delete' ? JSON.stringify(value) : undefined,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${options.authToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.fetchOptions?.headers,
|
||||||
|
},
|
||||||
|
method:
|
||||||
|
mode === 'read'
|
||||||
|
? 'GET'
|
||||||
|
: request === 'delete'
|
||||||
|
? 'DELETE'
|
||||||
|
: request === 'update'
|
||||||
|
? 'PUT'
|
||||||
|
: 'POST',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request !== 'insert') {
|
||||||
|
url = `${baseUrl}/${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, fetchOptions);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed with status ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'read') {
|
||||||
|
const data = await response.json();
|
||||||
|
return data as any;
|
||||||
|
} else {
|
||||||
|
return value as any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FormerRestHeadSpecAPI };
|
||||||
116
src/Former/FormerWrappers.tsx
Normal file
116
src/Former/FormerWrappers.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
type DrawerProps,
|
||||||
|
Modal,
|
||||||
|
type ModalProps,
|
||||||
|
Popover,
|
||||||
|
type PopoverProps,
|
||||||
|
} from '@mantine/core';
|
||||||
|
|
||||||
|
import type { FormerProps } from './Former.types';
|
||||||
|
|
||||||
|
import { Former } from './Former';
|
||||||
|
|
||||||
|
export const FormerDialog = (props: { former: FormerProps } & DrawerProps) => {
|
||||||
|
const { children, former, onClose, opened, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Former
|
||||||
|
{...former}
|
||||||
|
onClose={onClose}
|
||||||
|
opened={opened}
|
||||||
|
wrapper={(children, opened, onClose, _onOpen, getState) => {
|
||||||
|
const values = getState('values');
|
||||||
|
const request = getState('request');
|
||||||
|
const uniqueKeyField = getState('uniqueKeyField') ?? 'id';
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
closeOnClickOutside={false}
|
||||||
|
h={'100%'}
|
||||||
|
title={
|
||||||
|
request === 'delete'
|
||||||
|
? `Delete Record - ${values?.[uniqueKeyField]}`
|
||||||
|
: request === 'insert'
|
||||||
|
? 'New Record'
|
||||||
|
: `Edit Record - ${values?.[uniqueKeyField]}`
|
||||||
|
}
|
||||||
|
{...rest}
|
||||||
|
onClose={() => onClose?.()}
|
||||||
|
opened={opened ?? false}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Former>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormerModel = (props: { former: FormerProps } & ModalProps) => {
|
||||||
|
const { children, former, onClose, opened, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Former
|
||||||
|
{...former}
|
||||||
|
onClose={onClose}
|
||||||
|
opened={opened}
|
||||||
|
wrapper={(children, opened, onClose, _onOpen, getState) => {
|
||||||
|
const values = getState('values');
|
||||||
|
const request = getState('request');
|
||||||
|
const uniqueKeyField = getState('uniqueKeyField') ?? 'id';
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
closeOnClickOutside={false}
|
||||||
|
h={'100%'}
|
||||||
|
title={
|
||||||
|
request === 'delete'
|
||||||
|
? `Delete Record - ${values?.[uniqueKeyField]}`
|
||||||
|
: request === 'insert'
|
||||||
|
? 'New Record'
|
||||||
|
: `Edit Record - ${values?.[uniqueKeyField]}`
|
||||||
|
}
|
||||||
|
{...rest}
|
||||||
|
onClose={() => onClose?.()}
|
||||||
|
opened={opened ?? false}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Former>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormerPopover = (
|
||||||
|
props: { former: FormerProps; target: React.ReactNode } & PopoverProps
|
||||||
|
) => {
|
||||||
|
const { children, former, onClose, opened, target, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Former
|
||||||
|
{...former}
|
||||||
|
onClose={onClose}
|
||||||
|
opened={opened}
|
||||||
|
wrapper={(children, opened, onClose) => {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
closeOnClickOutside={false}
|
||||||
|
middlewares={{ inline: true }}
|
||||||
|
trapFocus
|
||||||
|
width={250}
|
||||||
|
withArrow
|
||||||
|
{...rest}
|
||||||
|
onClose={() => onClose?.()}
|
||||||
|
opened={opened ?? false}
|
||||||
|
>
|
||||||
|
<Popover.Target>{target}</Popover.Target>
|
||||||
|
<Popover.Dropdown>{children}</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Former>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export { Former } from './Former';
|
||||||
|
export type * from './Former.types';
|
||||||
|
export { FormerButtonArea } from './FormerButtonArea';
|
||||||
|
export { FormerResolveSpecAPI } from './FormerResolveSpecAPI';
|
||||||
|
export { FormerRestHeadSpecAPI } from './FormerRestHeadSpecAPI';
|
||||||
|
export { FormerDialog, FormerModel, FormerPopover } from './FormerWrappers';
|
||||||
|
|||||||
40
src/Former/stories/apiFormData.tsx
Normal file
40
src/Former/stories/apiFormData.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { TextInput } from '@mantine/core';
|
||||||
|
import { useUncontrolled } from '@mantine/hooks';
|
||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Former } from '../Former';
|
||||||
|
|
||||||
|
export const ApiFormData = (props: {
|
||||||
|
onChange?: (values: Record<string, unknown>) => void;
|
||||||
|
primeData?: Record<string, unknown>;
|
||||||
|
values?: Record<string, unknown>;
|
||||||
|
}) => {
|
||||||
|
const [values, setValues] = useUncontrolled<Record<string, unknown>>({
|
||||||
|
defaultValue: { authToken: '', url: '', ...props.primeData },
|
||||||
|
finalValue: { authToken: '', url: '', ...props.primeData },
|
||||||
|
onChange: props.onChange,
|
||||||
|
value: props.values,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Former
|
||||||
|
disableHTMlForm
|
||||||
|
id="api-form-data"
|
||||||
|
layout={{ saveButtonTitle: 'Save URL Parameters' }}
|
||||||
|
onChange={setValues}
|
||||||
|
primeData={props.primeData}
|
||||||
|
request="update"
|
||||||
|
uniqueKeyField="id"
|
||||||
|
values={values}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="url"
|
||||||
|
render={({ field }) => <TextInput label="URL" type="url" {...field} />}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="authToken"
|
||||||
|
render={({ field }) => <TextInput label="Auth Token" type="password" {...field} />}
|
||||||
|
/>
|
||||||
|
</Former>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,21 +1,53 @@
|
|||||||
import { Button, Drawer, Group, Paper, Select, Stack, Switch } from '@mantine/core';
|
import { Button, Group, Select, Stack, Switch } from '@mantine/core';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
import type { FormerRef } from '../Former.types';
|
import type { FormerAPICallType, FormerProps, FormerRef } from '../Former.types';
|
||||||
|
|
||||||
import { Former } from '../Former';
|
import { Former } from '../Former';
|
||||||
|
import { FormerRestHeadSpecAPI } from '../FormerRestHeadSpecAPI';
|
||||||
|
import { FormerModel } from '../FormerWrappers';
|
||||||
|
import { ApiFormData } from './apiFormData';
|
||||||
|
|
||||||
|
const StubAPI = (): FormerAPICallType => (mode, request, value) => {
|
||||||
|
console.log('API Call', mode, request, value);
|
||||||
|
if (mode === 'read') {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ a: 'Another Value', test: 'Loaded Value' });
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(value || {});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const FormTest = () => {
|
export const FormTest = () => {
|
||||||
const [request, setRequest] = useState<null | string>('insert');
|
const [request, setRequest] = useState<null | string>('insert');
|
||||||
const [wrapped, setWrapped] = useState(false);
|
const [wrapped, setWrapped] = useState(false);
|
||||||
|
const [disableHTML, setDisableHTML] = useState(false);
|
||||||
|
const [apiOptions, setApiOptions] = useState({
|
||||||
|
authToken: '',
|
||||||
|
type: '',
|
||||||
|
url: '',
|
||||||
|
});
|
||||||
|
const [layout, setLayout] = useState({
|
||||||
|
buttonAreaGroupProps: { justify: 'center' },
|
||||||
|
buttonOnTop: false,
|
||||||
|
title: 'Custom Former Title',
|
||||||
|
} as FormerProps['layout']);
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [formData, setFormData] = useState({ a: 99 });
|
const [formData, setFormData] = useState({ a: 99, rid_usernote: 3047 });
|
||||||
console.log('formData', formData);
|
//console.log('formData render', formData);
|
||||||
|
|
||||||
const ref = useRef<FormerRef>(null);
|
const ref = useRef<FormerRef>(null);
|
||||||
return (
|
return (
|
||||||
<Stack h="100%" mih="400px" w="90%">
|
<Stack h="100%" mih="400px" w="90%">
|
||||||
|
<Group>
|
||||||
<Select
|
<Select
|
||||||
data={['insert', 'update', 'delete', 'select', 'view']}
|
data={['insert', 'update', 'delete', 'select', 'view']}
|
||||||
onChange={setRequest}
|
onChange={setRequest}
|
||||||
@@ -26,6 +58,24 @@ export const FormTest = () => {
|
|||||||
label="Wrapped in Drawer"
|
label="Wrapped in Drawer"
|
||||||
onChange={(event) => setWrapped(event.currentTarget.checked)}
|
onChange={(event) => setWrapped(event.currentTarget.checked)}
|
||||||
/>
|
/>
|
||||||
|
<Switch
|
||||||
|
checked={disableHTML}
|
||||||
|
label="Disable HTML Form"
|
||||||
|
onChange={(event) => setDisableHTML(event.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
checked={layout?.buttonOnTop ?? false}
|
||||||
|
label="Button On Top"
|
||||||
|
onChange={(event) => setLayout({ ...layout, buttonOnTop: event.currentTarget.checked })}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
checked={apiOptions.type === 'api'}
|
||||||
|
label="Use API"
|
||||||
|
onChange={(event) =>
|
||||||
|
setApiOptions({ ...apiOptions, type: event.currentTarget.checked ? 'api' : '' })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
<Button onClick={() => setOpen(true)}>Open Former Drawer</Button>
|
<Button onClick={() => setOpen(true)}>Open Former Drawer</Button>
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Button
|
||||||
@@ -46,56 +96,53 @@ export const FormTest = () => {
|
|||||||
Test Show/Hide
|
Test Show/Hide
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
<FormerModel former={{ request: 'insert' }} onClose={() => setOpen(false)} opened={open}>
|
||||||
|
<div>Test</div>
|
||||||
|
</FormerModel>
|
||||||
<Former
|
<Former
|
||||||
//wrapper={(children, getState) => <div>{children}</div>}
|
disableHTMlForm={disableHTML}
|
||||||
//opened={true}
|
layout={layout}
|
||||||
apiKeyField="a"
|
onAPICall={
|
||||||
onAPICall={(mode, request, value) => {
|
apiOptions.type === 'api'
|
||||||
console.log('API Call', mode, request, value);
|
? FormerRestHeadSpecAPI({
|
||||||
if (mode === 'read') {
|
authToken: apiOptions.authToken,
|
||||||
return new Promise((resolve) => {
|
url: apiOptions.url,
|
||||||
setTimeout(() => {
|
})
|
||||||
resolve({ a: 'Another Value', test: 'Loaded Value' });
|
: StubAPI()
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve(value || {});
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onChange={setFormData}
|
onChange={setFormData}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
opened={open}
|
opened={open}
|
||||||
primeData={{ a: '66', test: 'primed' }}
|
primeData={{ a: '66', test: 'primed' }}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
request={request as any}
|
request={request as any}
|
||||||
|
//wrapper={(children, getState) => <div>{children}</div>}
|
||||||
|
//opened={true}
|
||||||
|
uniqueKeyField="rid_usernote"
|
||||||
useFormProps={{ criteriaMode: 'all', shouldUseNativeValidation: false }}
|
useFormProps={{ criteriaMode: 'all', shouldUseNativeValidation: false }}
|
||||||
values={formData}
|
values={formData}
|
||||||
wrapper={
|
// wrapper={
|
||||||
wrapped
|
// wrapped
|
||||||
? (children, opened, onClose, onOpen, getState) => {
|
// ? (children, opened, onClose, _onOpen, getState) => {
|
||||||
const values = getState('values');
|
// const values = getState('values');
|
||||||
return (
|
// return (
|
||||||
<Drawer
|
// <Drawer
|
||||||
h={'100%'}
|
// h={'100%'}
|
||||||
onClose={() => onClose?.()}
|
// onClose={() => onClose?.()}
|
||||||
opened={opened ?? false}
|
// opened={opened ?? false}
|
||||||
title={`Drawer Former - Current A Value: ${values?.a}`}
|
// title={`Drawer Former - Current A Value: ${values?.a}`}
|
||||||
w={'50%'}
|
// w={'50%'}
|
||||||
|
// >
|
||||||
|
// <Paper h="100%" shadow="sm" w="100%" withBorder>
|
||||||
|
// {children}
|
||||||
|
// </Paper>
|
||||||
|
// </Drawer>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// : undefined
|
||||||
|
// }
|
||||||
>
|
>
|
||||||
<Paper h="100%" shadow="sm" w="100%" withBorder>
|
<Stack pb={'400px'}>
|
||||||
{children}
|
|
||||||
<Button>Test Save</Button>
|
|
||||||
</Paper>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Stack h="1200px">
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<Controller
|
<Controller
|
||||||
name="test"
|
name="test"
|
||||||
@@ -106,13 +153,28 @@ export const FormTest = () => {
|
|||||||
render={({ field }) => <input type="text" {...field} placeholder="B" />}
|
render={({ field }) => <input type="text" {...field} placeholder="B" />}
|
||||||
rules={{ required: 'Field is required' }}
|
rules={{ required: 'Field is required' }}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
name="note"
|
||||||
|
render={({ field }) => <input type="text" {...field} placeholder="note" />}
|
||||||
|
rules={{ required: 'Field is required' }}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
{!disableHTML && (
|
||||||
<Stack>
|
<Stack>
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">HTML Submit</button>
|
||||||
<button type="reset">Reset</button>
|
<button type="reset">HTML Reset</button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Former>
|
</Former>
|
||||||
|
{apiOptions.type === 'api' && (
|
||||||
|
<ApiFormData
|
||||||
|
onChange={(values) => {
|
||||||
|
setApiOptions({ ...apiOptions, ...values });
|
||||||
|
}}
|
||||||
|
values={apiOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
- [ ] Headerspec API
|
- [x] Wrapper must receive button areas etc. Better scroll areas.
|
||||||
- [ ] Relspec API
|
- [x] Predefined wrappers (Model,Dialog,notification,popover)
|
||||||
|
- [x] Headerspec API
|
||||||
|
- [x] Relspec API
|
||||||
- [ ] SocketSpec API
|
- [ ] SocketSpec API
|
||||||
- [ ] Layout Tool
|
- [x] Layout Tool
|
||||||
- [ ] Header Section
|
- [x] Header Section
|
||||||
- [ ] Button Section
|
- [x] Button Section
|
||||||
- [ ] Footer Section
|
- [x] Footer Section
|
||||||
- [ ] Different Loaded for saving vs loading
|
- [ ] Different Loaded for saving vs loading
|
||||||
- [ ] Better Confirm Dialog
|
- [ ] Better Confirm Dialog
|
||||||
- [ ] Reset Confirm Dialog
|
- [ ] Reset Confirm Dialog
|
||||||
|
|||||||
35
src/FormerControllers/Buttons/ButtonCtrl.tsx
Normal file
35
src/FormerControllers/Buttons/ButtonCtrl.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Button, type ButtonProps, Tooltip } from '@mantine/core';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import type { SpecialIDProps } from '../FormerControllers.types';
|
||||||
|
|
||||||
|
const ButtonCtrl = (
|
||||||
|
props: {
|
||||||
|
onClick?: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<void>;
|
||||||
|
} & Omit<ButtonProps, 'onClick'> &
|
||||||
|
SpecialIDProps
|
||||||
|
) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
return (
|
||||||
|
<Tooltip label={props.tooltip ?? ''} withArrow>
|
||||||
|
<Button
|
||||||
|
loaderProps={{
|
||||||
|
type: 'bars',
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
loading={loading || props.loading}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (props.onClick) {
|
||||||
|
setLoading(true);
|
||||||
|
props.onClick(e).finally(() => setLoading(false));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ButtonCtrl };
|
||||||
|
export default ButtonCtrl;
|
||||||
36
src/FormerControllers/Buttons/IconButtonCtrl.tsx
Normal file
36
src/FormerControllers/Buttons/IconButtonCtrl.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { ActionIcon, type ActionIconProps, Tooltip, VisuallyHidden } from '@mantine/core';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import type { SpecialIDProps } from '../FormerControllers.types';
|
||||||
|
|
||||||
|
const IconButtonCtrl = (
|
||||||
|
props: {
|
||||||
|
onClick?: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<void>;
|
||||||
|
} & Omit<ActionIconProps, 'onClick'> &
|
||||||
|
SpecialIDProps
|
||||||
|
) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
return (
|
||||||
|
<Tooltip label={props.tooltip ?? ''} withArrow>
|
||||||
|
<ActionIcon
|
||||||
|
loaderProps={{
|
||||||
|
type: 'bars',
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
loading={loading || props.loading}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (props.onClick) {
|
||||||
|
setLoading(true);
|
||||||
|
props.onClick(e).finally(() => setLoading(false));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
<VisuallyHidden>Action Button: {props.tooltip ?? props.sid ?? ''}</VisuallyHidden>
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { IconButtonCtrl };
|
||||||
|
export default IconButtonCtrl;
|
||||||
8
src/FormerControllers/FormerControllers.types.ts
Normal file
8
src/FormerControllers/FormerControllers.types.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { ControllerProps } from 'react-hook-form';
|
||||||
|
|
||||||
|
export type FormerControllersProps = Omit<ControllerProps, 'render'>;
|
||||||
|
|
||||||
|
export interface SpecialIDProps {
|
||||||
|
sid?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
}
|
||||||
30
src/FormerControllers/Inputs/NativeSelectCtrl.tsx
Normal file
30
src/FormerControllers/Inputs/NativeSelectCtrl.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { NativeSelect, type NativeSelectProps, Tooltip } from '@mantine/core';
|
||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { FormerControllersProps, SpecialIDProps } from '../FormerControllers.types';
|
||||||
|
|
||||||
|
const NativeSelectCtrl = (props: FormerControllersProps & NativeSelectProps & SpecialIDProps) => {
|
||||||
|
const { control, name, sid, tooltip, ...innerProps } = props;
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field, formState }) => (
|
||||||
|
<Tooltip label={tooltip ?? ''} withArrow>
|
||||||
|
<NativeSelect
|
||||||
|
{...innerProps}
|
||||||
|
{...field}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
id={`field_${name}_${sid ?? ''}`}
|
||||||
|
key={`field_${name}_${sid ?? ''}`}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</NativeSelect>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NativeSelectCtrl };
|
||||||
|
export default NativeSelectCtrl;
|
||||||
36
src/FormerControllers/Inputs/NumberInputCtrl.tsx
Normal file
36
src/FormerControllers/Inputs/NumberInputCtrl.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { NumberInput, type NumberInputProps, Tooltip } from '@mantine/core';
|
||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { FormerControllersProps, SpecialIDProps } from '../FormerControllers.types';
|
||||||
|
|
||||||
|
const NumberInputCtrl = (props: FormerControllersProps & NumberInputProps & SpecialIDProps) => {
|
||||||
|
const { control, name, sid, tooltip, ...textProps } = props;
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field, formState }) => (
|
||||||
|
<Tooltip label={tooltip ?? ''} withArrow>
|
||||||
|
<NumberInput
|
||||||
|
{...textProps}
|
||||||
|
{...field}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
id={`field_${name}_${sid ?? ''}`}
|
||||||
|
key={`field_${name}_${sid ?? ''}`}
|
||||||
|
onChange={(num) =>
|
||||||
|
field.onChange(num !== undefined && num !== null ? Number(num) : undefined)
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
field.value !== undefined && field.value !== null ? Number(field.value) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</NumberInput>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NumberInputCtrl };
|
||||||
|
export default NumberInputCtrl;
|
||||||
30
src/FormerControllers/Inputs/PasswordInputCtrl.tsx
Normal file
30
src/FormerControllers/Inputs/PasswordInputCtrl.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { PasswordInput, type PasswordInputProps, Tooltip } from '@mantine/core';
|
||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { FormerControllersProps, SpecialIDProps } from '../FormerControllers.types';
|
||||||
|
|
||||||
|
const PasswordInputCtrl = (props: FormerControllersProps & PasswordInputProps & SpecialIDProps) => {
|
||||||
|
const { control, name, sid, tooltip, ...textProps } = props;
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field, formState }) => (
|
||||||
|
<Tooltip label={tooltip ?? ''} withArrow>
|
||||||
|
<PasswordInput
|
||||||
|
{...textProps}
|
||||||
|
{...field}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
id={`field_${name}_${sid ?? ''}`}
|
||||||
|
key={`field_${name}_${sid ?? ''}`}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</PasswordInput>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { PasswordInputCtrl };
|
||||||
|
export default PasswordInputCtrl;
|
||||||
32
src/FormerControllers/Inputs/SwitchCtrl.tsx
Normal file
32
src/FormerControllers/Inputs/SwitchCtrl.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Switch, type SwitchProps, Tooltip } from '@mantine/core';
|
||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { FormerControllersProps, SpecialIDProps } from '../FormerControllers.types';
|
||||||
|
|
||||||
|
const SwitchCtrl = (props: FormerControllersProps & SpecialIDProps & SwitchProps) => {
|
||||||
|
const { control, name, sid, tooltip, ...innerProps } = props;
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field, formState }) => (
|
||||||
|
<Tooltip label={tooltip ?? ''} withArrow>
|
||||||
|
<Switch
|
||||||
|
{...innerProps}
|
||||||
|
{...field}
|
||||||
|
checked={!!field.value}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
id={`field_${name}_${sid ?? ''}`}
|
||||||
|
key={`field_${name}_${sid ?? ''}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
field.onChange((e.currentTarget ?? e.target)?.checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SwitchCtrl };
|
||||||
|
export default SwitchCtrl;
|
||||||
31
src/FormerControllers/Inputs/TextAreaCtrl.tsx
Normal file
31
src/FormerControllers/Inputs/TextAreaCtrl.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Textarea, type TextareaProps, Tooltip } from '@mantine/core';
|
||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { FormerControllersProps, SpecialIDProps } from '../FormerControllers.types';
|
||||||
|
|
||||||
|
const TextAreaCtrl = (props: FormerControllersProps & SpecialIDProps & TextareaProps) => {
|
||||||
|
const { control, name, sid, tooltip, ...innerProps } = props;
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field, formState }) => (
|
||||||
|
<Tooltip label={tooltip ?? ''} withArrow>
|
||||||
|
<Textarea
|
||||||
|
minRows={4}
|
||||||
|
{...innerProps}
|
||||||
|
{...field}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
id={`field_${name}_${sid ?? ''}`}
|
||||||
|
key={`field_${name}_${sid ?? ''}`}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Textarea>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TextAreaCtrl };
|
||||||
|
export default TextAreaCtrl;
|
||||||
30
src/FormerControllers/Inputs/TextInputCtrl.tsx
Normal file
30
src/FormerControllers/Inputs/TextInputCtrl.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { TextInput, type TextInputProps, Tooltip } from '@mantine/core';
|
||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { FormerControllersProps, SpecialIDProps } from '../FormerControllers.types';
|
||||||
|
|
||||||
|
const TextInputCtrl = (props: FormerControllersProps & SpecialIDProps & TextInputProps) => {
|
||||||
|
const { control, name, sid, tooltip, ...textProps } = props;
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field, formState }) => (
|
||||||
|
<Tooltip label={tooltip ?? ''} withArrow>
|
||||||
|
<TextInput
|
||||||
|
{...textProps}
|
||||||
|
{...field}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
id={`field_${name}_${sid ?? ''}`}
|
||||||
|
key={`field_${name}_${sid ?? ''}`}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</TextInput>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TextInputCtrl };
|
||||||
|
export default TextInputCtrl;
|
||||||
7
src/FormerControllers/index.ts
Normal file
7
src/FormerControllers/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export { ButtonCtrl } from './Buttons/ButtonCtrl';
|
||||||
|
export { IconButtonCtrl } from './Buttons/IconButtonCtrl';
|
||||||
|
export { NativeSelectCtrl } from './Inputs/NativeSelectCtrl';
|
||||||
|
export { PasswordInputCtrl } from './Inputs/PasswordInputCtrl';
|
||||||
|
export { SwitchCtrl } from './Inputs/SwitchCtrl';
|
||||||
|
export { TextAreaCtrl } from './Inputs/TextAreaCtrl';
|
||||||
|
export { TextInputCtrl } from './Inputs/TextInputCtrl';
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './Gridler'
|
export * from './Former';
|
||||||
|
export * from './FormerControllers';
|
||||||
|
export * from './Gridler';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type MantineBetterMenuInstance,
|
type MantineBetterMenuInstance,
|
||||||
@@ -7,4 +8,4 @@ export {
|
|||||||
MantineBetterMenusProvider,
|
MantineBetterMenusProvider,
|
||||||
type MantineBetterMenuStoreState,
|
type MantineBetterMenuStoreState,
|
||||||
useMantineBetterMenus,
|
useMantineBetterMenus,
|
||||||
} from "./MantineBetterMenu";
|
} from './MantineBetterMenu';
|
||||||
|
|||||||
Reference in New Issue
Block a user