chore(form): 🗑️ remove unused form components and types

* Refactor Former components to streamline functionality
* Update stories to reflect changes in form structure
This commit is contained in:
2026-01-14 21:56:55 +02:00
parent cd2f6db880
commit e777e1fa3a
38 changed files with 133 additions and 2125 deletions

View File

@@ -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

View File

@@ -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>
)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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

View File

@@ -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 }

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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

View File

@@ -1,10 +0,0 @@
.disabled {
pointer-events: none;
opacity: 0.9;
}
.sticky {
position: -webkit-sticky;
position: sticky;
bottom: 0;
}

View File

@@ -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
}

View File

@@ -1,2 +0,0 @@
export * from './form.types'
export * from './remote.types'

View File

@@ -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[]
}

View File

@@ -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,
}

View File

@@ -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)
}

View File

@@ -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,
})

View File

@@ -1,8 +1,8 @@
import { newUUID } from '@warkypublic/artemis-kit';
import { createSyncStore } from '@warkypublic/zustandsyncstore'; import { createSyncStore } from '@warkypublic/zustandsyncstore';
import { produce } from 'immer'; import { produce } from 'immer';
import type { FormerProps, FormerState } from './Former.types'; import type { FormerProps, FormerState } from './Former.types';
import { newUUID } from '@warkypublic/artemis-kit';
const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore< const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
FormerState<any> & Partial<FormerProps<any>>, FormerState<any> & Partial<FormerProps<any>>,
@@ -171,7 +171,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
}, },
values: undefined, values: undefined,
}), }),
({ onConfirmDelete, primeData, request, values, id, onClose, useStoreApi }) => { ({ id, onClose, onConfirmDelete, primeData, request, useStoreApi, values }) => {
let _onConfirmDelete = onConfirmDelete; let _onConfirmDelete = onConfirmDelete;
if (!onConfirmDelete) { if (!onConfirmDelete) {
_onConfirmDelete = async () => { _onConfirmDelete = async () => {
@@ -180,10 +180,6 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
} }
return { return {
onConfirmDelete: _onConfirmDelete,
primeData,
request: (request || 'insert').replace('change', 'update'),
values: { ...primeData, ...values },
id: !id ? newUUID() : id, id: !id ? newUUID() : id,
onClose: () => { onClose: () => {
const dirty = useStoreApi.getState().dirty; const dirty = useStoreApi.getState().dirty;
@@ -204,6 +200,10 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
} }
} }
}, },
onConfirmDelete: _onConfirmDelete,
primeData,
request: (request || 'insert').replace('change', 'update'),
values: { ...primeData, ...values },
}; };
} }
); );

View File

@@ -86,10 +86,10 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
if (formMethods) { if (formMethods) {
formMethods.subscribe({ formMethods.subscribe({
formState: { isDirty: true },
callback: ({ isDirty }) => { callback: ({ isDirty }) => {
setState('dirty', isDirty); setState('dirty', isDirty);
}, },
formState: { isDirty: true },
}); });
} }
}, [formMethods]); }, [formMethods]);

View File

@@ -7,14 +7,6 @@ import type {
import type React from 'react'; import type React from 'react';
import type { FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form'; import type { FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form';
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 type FormerAPICallType<T extends FieldValues = any> = ( export type FormerAPICallType<T extends FieldValues = any> = (
mode: 'mutate' | 'read', mode: 'mutate' | 'read',
request: RequestType, request: RequestType,
@@ -23,39 +15,39 @@ export type FormerAPICallType<T extends FieldValues = any> = (
) => Promise<T>; ) => Promise<T>;
export interface FormerProps<T extends FieldValues = any> { export interface FormerProps<T extends FieldValues = any> {
id?: string;
afterGet?: (data: T) => Promise<T> | void; afterGet?: (data: T) => Promise<T> | void;
afterSave?: (data: T) => Promise<void> | void; afterSave?: (data: T) => Promise<void> | void;
uniqueKeyField?: string;
beforeSave?: (data: T) => Promise<T> | T; beforeSave?: (data: T) => Promise<T> | T;
dirty?: boolean;
disableHTMlForm?: boolean; disableHTMlForm?: boolean;
id?: string;
keepOpen?: boolean; 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>; 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;
dirty?: boolean;
primeData?: T; primeData?: T;
request: RequestType; request: RequestType;
uniqueKeyField?: string;
useFormProps?: UseFormProps<T>; useFormProps?: UseFormProps<T>;
values?: T; values?: T;
wrapper?: FormerSectionRender<T>;
layout?: { wrapper?: FormerSectionRender<T>;
renderTop?: FormerSectionRender<T>;
renderBottom?: FormerSectionRender<T>;
saveButtonTitle?: React.ReactNode;
closeButtonTitle?: React.ReactNode;
saveButtonProps?: ButtonProps;
closeButtonProps?: ButtonProps;
buttonOnTop?: boolean;
buttonAreaGroupProps?: GroupProps;
title?: string;
};
} }
export interface FormerRef<T extends FieldValues = any> { export interface FormerRef<T extends FieldValues = any> {
@@ -68,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;

View File

@@ -1,28 +1,29 @@
import { Group, Button, Tooltip } from '@mantine/core'; import { Button, Group, Tooltip } from '@mantine/core';
import { IconX, IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy, IconX } from '@tabler/icons-react';
import { useFormerStore } from './Former.store'; import { useFormerStore } from './Former.store';
export const FormerButtonArea = () => { export const FormerButtonArea = () => {
const { const {
save,
onClose,
buttonAreaGroupProps, buttonAreaGroupProps,
saveButtonProps,
closeButtonProps, closeButtonProps,
closeButtonTitle, closeButtonTitle,
saveButtonTitle,
request,
dirty, dirty,
onClose,
request,
save,
saveButtonProps,
saveButtonTitle,
} = useFormerStore((state) => ({ } = useFormerStore((state) => ({
save: state.save,
onClose: state.onClose,
buttonAreaGroupProps: state.layout?.buttonAreaGroupProps, buttonAreaGroupProps: state.layout?.buttonAreaGroupProps,
saveButtonProps: state.layout?.saveButtonProps,
closeButtonProps: state.layout?.closeButtonProps, closeButtonProps: state.layout?.closeButtonProps,
closeButtonTitle: state.layout?.closeButtonTitle, closeButtonTitle: state.layout?.closeButtonTitle,
saveButtonTitle: state.layout?.saveButtonTitle,
request: state.request,
dirty: state.dirty, dirty: state.dirty,
onClose: state.onClose,
request: state.request,
save: state.save,
saveButtonProps: state.layout?.saveButtonProps,
saveButtonTitle: state.layout?.saveButtonTitle,
})); }));
const disabledSave = const disabledSave =
@@ -31,19 +32,19 @@ export const FormerButtonArea = () => {
return ( return (
<Group <Group
justify="center" justify="center"
w="100%"
p="xs" p="xs"
style={{ boxShadow: '2px 2px 5px rgba(47, 47, 47, 0.1)' }} style={{ boxShadow: '2px 2px 5px rgba(47, 47, 47, 0.1)' }}
w="100%"
{...buttonAreaGroupProps} {...buttonAreaGroupProps}
> >
<Group justify="space-evenly" grow> <Group grow justify="space-evenly">
{typeof onClose === 'function' && ( {typeof onClose === 'function' && (
<Button <Button
color="orange" color="orange"
leftSection={<IconX />} leftSection={<IconX />}
size="sm"
px="md"
miw={'8rem'} miw={'8rem'}
px="md"
size="sm"
{...closeButtonProps} {...closeButtonProps}
onClick={() => { onClick={() => {
onClose(); onClose();
@@ -65,12 +66,12 @@ export const FormerButtonArea = () => {
} }
> >
<Button <Button
bg={request === 'delete' ? 'red' : undefined}
color="green" color="green"
leftSection={<IconDeviceFloppy />} leftSection={<IconDeviceFloppy />}
size="sm"
px="md"
miw={'8rem'} miw={'8rem'}
bg={request === 'delete' ? 'red' : undefined} px="md"
size="sm"
{...saveButtonProps} {...saveButtonProps}
disabled={disabledSave} disabled={disabledSave}
onClick={() => save()} onClick={() => save()}

View File

@@ -9,28 +9,28 @@ 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,
scrollAreaProps, scrollAreaProps,
id,
opened,
} = 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,
id: state.id,
opened: state.opened, scrollAreaProps: state.scrollAreaProps,
})); }));
useEffect(() => { useEffect(() => {
@@ -58,15 +58,17 @@ export const FormerLayout = (props: PropsWithChildren) => {
}} }}
> >
{disableHTMlForm ? ( {disableHTMlForm ? (
<div x-data-request={request} key={`former_d${id}`}> // eslint-disable-next-line react/no-unknown-property
<div key={`former_d${id}`} x-data-request={request}>
{props.children} {props.children}
</div> </div>
) : ( ) : (
<form <form
key={`former_${id}`}
id={`former_f${id}`} id={`former_f${id}`}
key={`former_${id}`}
onReset={(e) => reset(e)} onReset={(e) => reset(e)}
onSubmit={(e) => save(e)} onSubmit={(e) => save(e)}
// eslint-disable-next-line react/no-unknown-property
x-data-request={request} x-data-request={request}
> >
{props.children} {props.children}

View File

@@ -2,11 +2,11 @@ import { useFormerStore } from './Former.store';
import { FormerButtonArea } from './FormerButtonArea'; import { FormerButtonArea } from './FormerButtonArea';
export const FormerLayoutBottom = () => { export const FormerLayoutBottom = () => {
const { renderBottom, getState, opened, buttonOnTop } = useFormerStore((state) => ({ const { buttonOnTop, getState, opened, renderBottom } = useFormerStore((state) => ({
renderBottom: state.layout?.renderBottom,
buttonOnTop: state.layout?.buttonOnTop, buttonOnTop: state.layout?.buttonOnTop,
getState: state.getState, getState: state.getState,
opened: state.opened, opened: state.opened,
renderBottom: state.layout?.renderBottom,
})); }));
if (renderBottom) { if (renderBottom) {

View File

@@ -2,11 +2,11 @@ import { useFormerStore } from './Former.store';
import { FormerButtonArea } from './FormerButtonArea'; import { FormerButtonArea } from './FormerButtonArea';
export const FormerLayoutTop = () => { export const FormerLayoutTop = () => {
const { renderTop, getState, opened, buttonOnTop } = useFormerStore((state) => ({ const { buttonOnTop, getState, opened, renderTop } = useFormerStore((state) => ({
renderTop: state.layout?.renderTop,
buttonOnTop: state.layout?.buttonOnTop, buttonOnTop: state.layout?.buttonOnTop,
getState: state.getState, getState: state.getState,
opened: state.opened, opened: state.opened,
renderTop: state.layout?.renderTop,
})); }));
if (renderTop) { if (renderTop) {

View File

@@ -1,25 +1,25 @@
import type { FormerAPICallType } from './Former.types'; import type { FormerAPICallType } from './Former.types';
interface ResolveSpecRequest { interface ResolveSpecRequest {
operation: 'read' | 'create' | 'update' | 'delete';
data?: Record<string, any>; data?: Record<string, any>;
operation: 'create' | 'delete' | 'read' | 'update';
options?: { options?: {
preload?: string[];
columns?: string[]; columns?: string[];
computedColumns?: any[];
customOperators?: any[];
filters?: Array<{ column: string; operator: string; value: any }>; filters?: Array<{ column: string; operator: string; value: any }>;
sort?: string[];
limit?: number; limit?: number;
offset?: number; offset?: number;
customOperators?: any[]; preload?: string[];
computedColumns?: any[]; sort?: string[];
}; };
} }
function FormerResolveSpecAPI(options: { function FormerResolveSpecAPI(options: {
url: string;
authToken: string; authToken: string;
signal?: AbortSignal;
fetchOptions?: Partial<RequestInit>; fetchOptions?: Partial<RequestInit>;
signal?: AbortSignal;
url: string;
}): FormerAPICallType { }): FormerAPICallType {
return async (mode, request, value, key) => { return async (mode, request, value, key) => {
const baseUrl = options.url.replace(/\/$/, ''); const baseUrl = options.url.replace(/\/$/, '');
@@ -50,13 +50,13 @@ function FormerResolveSpecAPI(options: {
cache: 'no-cache', cache: 'no-cache',
signal: options.signal, signal: options.signal,
...options.fetchOptions, ...options.fetchOptions,
method: 'POST', body: JSON.stringify(resolveSpecRequest),
headers: { headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${options.authToken}`, Authorization: `Bearer ${options.authToken}`,
'Content-Type': 'application/json',
...options.fetchOptions?.headers, ...options.fetchOptions?.headers,
}, },
body: JSON.stringify(resolveSpecRequest), method: 'POST',
}; };
const response = await fetch(url, fetchOptions); const response = await fetch(url, fetchOptions);

View File

@@ -1,18 +1,24 @@
import type { FormerAPICallType } from './Former.types'; import type { FormerAPICallType } from './Former.types';
function FormerRestHeadSpecAPI(options: { function FormerRestHeadSpecAPI(options: {
url: string;
authToken: string; authToken: string;
signal?: AbortSignal;
fetchOptions?: Partial<RequestInit>; fetchOptions?: Partial<RequestInit>;
signal?: AbortSignal;
url: string;
}): FormerAPICallType { }): FormerAPICallType {
return async (mode, request, value, key) => { return async (mode, request, value, key) => {
const baseUrl = options.url ?? ''; // Remove trailing slashes const baseUrl = options.url ?? ''; // Remove trailing slashes
let url = baseUrl; let url = baseUrl;
let fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
cache: 'no-cache', cache: 'no-cache',
signal: options.signal, signal: options.signal,
...options.fetchOptions, ...options.fetchOptions,
body: mode === 'mutate' && request !== 'delete' ? JSON.stringify(value) : undefined,
headers: {
Authorization: `Bearer ${options.authToken}`,
'Content-Type': 'application/json',
...options.fetchOptions?.headers,
},
method: method:
mode === 'read' mode === 'read'
? 'GET' ? 'GET'
@@ -21,12 +27,6 @@ function FormerRestHeadSpecAPI(options: {
: request === 'update' : request === 'update'
? 'PUT' ? 'PUT'
: 'POST', : 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${options.authToken}`,
...options.fetchOptions?.headers,
},
body: mode === 'mutate' && request !== 'delete' ? JSON.stringify(value) : undefined,
}; };
if (request !== 'insert') { if (request !== 'insert') {

View File

@@ -1,27 +1,30 @@
import { import {
Drawer, Drawer,
Modal,
Popover,
type DrawerProps, type DrawerProps,
Modal,
type ModalProps, type ModalProps,
Popover,
type PopoverProps, type PopoverProps,
} from '@mantine/core'; } from '@mantine/core';
import type { FormerProps } from './Former.types'; import type { FormerProps } from './Former.types';
import { Former } from './Former'; import { Former } from './Former';
export const FormerDialog = (props: DrawerProps & { former: FormerProps }) => { export const FormerDialog = (props: { former: FormerProps } & DrawerProps) => {
const { former, children, opened, onClose, ...rest } = props; const { children, former, onClose, opened, ...rest } = props;
return ( return (
<Former <Former
{...former} {...former}
opened={opened}
onClose={onClose} onClose={onClose}
opened={opened}
wrapper={(children, opened, onClose, _onOpen, getState) => { wrapper={(children, opened, onClose, _onOpen, getState) => {
const values = getState('values'); const values = getState('values');
const request = getState('request'); const request = getState('request');
const uniqueKeyField = getState('uniqueKeyField') ?? 'id'; const uniqueKeyField = getState('uniqueKeyField') ?? 'id';
return ( return (
<Drawer <Drawer
closeOnClickOutside={false}
h={'100%'} h={'100%'}
title={ title={
request === 'delete' request === 'delete'
@@ -30,7 +33,6 @@ export const FormerDialog = (props: DrawerProps & { former: FormerProps }) => {
? 'New Record' ? 'New Record'
: `Edit Record - ${values?.[uniqueKeyField]}` : `Edit Record - ${values?.[uniqueKeyField]}`
} }
closeOnClickOutside={false}
{...rest} {...rest}
onClose={() => onClose?.()} onClose={() => onClose?.()}
opened={opened ?? false} opened={opened ?? false}
@@ -45,19 +47,20 @@ export const FormerDialog = (props: DrawerProps & { former: FormerProps }) => {
); );
}; };
export const FormerModel = (props: ModalProps & { former: FormerProps }) => { export const FormerModel = (props: { former: FormerProps } & ModalProps) => {
const { former, children, opened, onClose, ...rest } = props; const { children, former, onClose, opened, ...rest } = props;
return ( return (
<Former <Former
{...former} {...former}
opened={opened}
onClose={onClose} onClose={onClose}
opened={opened}
wrapper={(children, opened, onClose, _onOpen, getState) => { wrapper={(children, opened, onClose, _onOpen, getState) => {
const values = getState('values'); const values = getState('values');
const request = getState('request'); const request = getState('request');
const uniqueKeyField = getState('uniqueKeyField') ?? 'id'; const uniqueKeyField = getState('uniqueKeyField') ?? 'id';
return ( return (
<Modal <Modal
closeOnClickOutside={false}
h={'100%'} h={'100%'}
title={ title={
request === 'delete' request === 'delete'
@@ -66,7 +69,6 @@ export const FormerModel = (props: ModalProps & { former: FormerProps }) => {
? 'New Record' ? 'New Record'
: `Edit Record - ${values?.[uniqueKeyField]}` : `Edit Record - ${values?.[uniqueKeyField]}`
} }
closeOnClickOutside={false}
{...rest} {...rest}
onClose={() => onClose?.()} onClose={() => onClose?.()}
opened={opened ?? false} opened={opened ?? false}
@@ -82,22 +84,22 @@ export const FormerModel = (props: ModalProps & { former: FormerProps }) => {
}; };
export const FormerPopover = ( export const FormerPopover = (
props: PopoverProps & { former: FormerProps; target: React.ReactNode } props: { former: FormerProps; target: React.ReactNode } & PopoverProps
) => { ) => {
const { former, children, opened, onClose, target, ...rest } = props; const { children, former, onClose, opened, target, ...rest } = props;
return ( return (
<Former <Former
{...former} {...former}
opened={opened}
onClose={onClose} onClose={onClose}
wrapper={(children, opened, onClose, _onOpen, _getState) => { opened={opened}
wrapper={(children, opened, onClose) => {
return ( return (
<Popover <Popover
withArrow
closeOnClickOutside={false} closeOnClickOutside={false}
width={250}
trapFocus
middlewares={{ inline: true }} middlewares={{ inline: true }}
trapFocus
width={250}
withArrow
{...rest} {...rest}
onClose={() => onClose?.()} onClose={() => onClose?.()}
opened={opened ?? false} opened={opened ?? false}

View File

@@ -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';

View File

@@ -1,39 +1,39 @@
import { TextInput } from '@mantine/core'; import { TextInput } from '@mantine/core';
import { Former } from '../Former';
import { useUncontrolled } from '@mantine/hooks'; import { useUncontrolled } from '@mantine/hooks';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import { Former } from '../Former';
export const ApiFormData = (props: { export const ApiFormData = (props: {
values?: Record<string, unknown>;
onChange?: (values: Record<string, unknown>) => void; onChange?: (values: Record<string, unknown>) => void;
primeData?: Record<string, unknown>; primeData?: Record<string, unknown>;
values?: Record<string, unknown>;
}) => { }) => {
const [values, setValues] = useUncontrolled<Record<string, unknown>>({ const [values, setValues] = useUncontrolled<Record<string, unknown>>({
value: props.values, defaultValue: { authToken: '', url: '', ...props.primeData },
defaultValue: { url: '', authToken: '', ...props.primeData }, finalValue: { authToken: '', url: '', ...props.primeData },
finalValue: { url: '', authToken: '', ...props.primeData },
onChange: props.onChange, onChange: props.onChange,
value: props.values,
}); });
return ( return (
<Former <Former
disableHTMlForm
id="api-form-data"
layout={{ saveButtonTitle: 'Save URL Parameters' }}
onChange={setValues}
primeData={props.primeData}
request="update" request="update"
uniqueKeyField="id" uniqueKeyField="id"
disableHTMlForm
primeData={props.primeData}
values={values} values={values}
onChange={setValues}
layout={{ saveButtonTitle: 'Save URL Parameters' }}
id="api-form-data"
> >
<Controller <Controller
name="url" name="url"
render={({ field }) => <TextInput type="url" label="URL" {...field} />} render={({ field }) => <TextInput label="URL" type="url" {...field} />}
/> />
<Controller <Controller
name="authToken" name="authToken"
render={({ field }) => <TextInput type="password" label="Auth Token" {...field} />} render={({ field }) => <TextInput label="Auth Token" type="password" {...field} />}
/> />
</Former> </Former>
); );

View File

@@ -1,13 +1,13 @@
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 { FormerAPICallType, FormerProps, FormerRef } from '../Former.types'; import type { FormerAPICallType, FormerProps, FormerRef } from '../Former.types';
import { Former } from '../Former'; import { Former } from '../Former';
import { ApiFormData } from './apiFormData';
import { FormerRestHeadSpecAPI } from '../FormerRestHeadSpecAPI'; import { FormerRestHeadSpecAPI } from '../FormerRestHeadSpecAPI';
import { FormerDialog, FormerModel } from '../FormerWrappers'; import { FormerModel } from '../FormerWrappers';
import { ApiFormData } from './apiFormData';
const StubAPI = (): FormerAPICallType => (mode, request, value) => { const StubAPI = (): FormerAPICallType => (mode, request, value) => {
console.log('API Call', mode, request, value); console.log('API Call', mode, request, value);
@@ -30,14 +30,14 @@ export const FormTest = () => {
const [wrapped, setWrapped] = useState(false); const [wrapped, setWrapped] = useState(false);
const [disableHTML, setDisableHTML] = useState(false); const [disableHTML, setDisableHTML] = useState(false);
const [apiOptions, setApiOptions] = useState({ const [apiOptions, setApiOptions] = useState({
url: '',
authToken: '', authToken: '',
type: '', type: '',
url: '',
}); });
const [layout, setLayout] = useState({ const [layout, setLayout] = useState({
buttonAreaGroupProps: { justify: 'center' },
buttonOnTop: false, buttonOnTop: false,
title: 'Custom Former Title', title: 'Custom Former Title',
buttonAreaGroupProps: { justify: 'center' },
} as FormerProps['layout']); } as FormerProps['layout']);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -96,13 +96,12 @@ export const FormTest = () => {
Test Show/Hide Test Show/Hide
</Button> </Button>
</Group> </Group>
<FormerModel opened={open} onClose={() => setOpen(false)} former={{ request: 'insert' }}> <FormerModel former={{ request: 'insert' }} onClose={() => setOpen(false)} opened={open}>
<div>Test</div> <div>Test</div>
</FormerModel> </FormerModel>
<Former <Former
//wrapper={(children, getState) => <div>{children}</div>} disableHTMlForm={disableHTML}
//opened={true} layout={layout}
uniqueKeyField="rid_usernote"
onAPICall={ onAPICall={
apiOptions.type === 'api' apiOptions.type === 'api'
? FormerRestHeadSpecAPI({ ? FormerRestHeadSpecAPI({
@@ -111,16 +110,17 @@ export const FormTest = () => {
}) })
: StubAPI() : StubAPI()
} }
disableHTMlForm={disableHTML}
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}
layout={layout}
// wrapper={ // wrapper={
// wrapped // wrapped
// ? (children, opened, onClose, _onOpen, getState) => { // ? (children, opened, onClose, _onOpen, getState) => {
@@ -169,10 +169,10 @@ export const FormTest = () => {
</Former> </Former>
{apiOptions.type === 'api' && ( {apiOptions.type === 'api' && (
<ApiFormData <ApiFormData
values={apiOptions}
onChange={(values) => { onChange={(values) => {
setApiOptions({ ...apiOptions, ...values }); setApiOptions({ ...apiOptions, ...values });
}} }}
values={apiOptions}
/> />
)} )}
</Stack> </Stack>