diff --git a/.storybook/previewDecorator.tsx b/.storybook/previewDecorator.tsx index 7b01dd9..36e0979 100644 --- a/.storybook/previewDecorator.tsx +++ b/.storybook/previewDecorator.tsx @@ -1,13 +1,16 @@ import { MantineProvider } from '@mantine/core'; +import { ModalsProvider } from '@mantine/modals'; import '@mantine/core/styles.css'; export function PreviewDecorator(Story: any, { parameters }: any) { console.log('Rendering decorator', parameters); return ( -
- -
+ +
+ +
+
); } diff --git a/eslint.config.ts b/eslint.config.ts index f5ee91a..74afdf7 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -16,19 +16,22 @@ const config = defineConfig([ plugins: { js }, }, // reactHooks.configs['recommended-latest'], - {...reactRefresh.configs.vite, ignores: ['**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],}, + { ...reactRefresh.configs.vite, ignores: ['**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] }, tseslint.configs.recommended, { ...pluginReact.configs.flat.recommended, - ignores: ['**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - rules: {...pluginReact.configs.flat.recommended.rules, + ignores: ['**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', '*stories.tsx'], + rules: { + ...pluginReact.configs.flat.recommended.rules, 'react/react-in-jsx-scope': 'off', - } + 'react-refresh/only-export-components': 'warn', + }, }, perfectionist.configs['recommended-alphabetical'], { rules: { '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/ban-ts-comment': 'off', }, }, ]); diff --git a/package.json b/package.json index 48521e8..5599ff1 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,9 @@ "./oranguru.css": "./src/oranguru.css" }, "dependencies": { + "moment": "^2.30.1" + }, "devDependencies": { "@changesets/cli": "^2.29.7", @@ -94,10 +96,13 @@ "@mantine/core": "^8.3.1", "@mantine/hooks": "^8.3.1", "@mantine/notifications": "^8.3.5", + "@mantine/modals": "^8.3.5", "@tabler/icons-react": "^3.35.0", "@tanstack/react-query": "^5.90.5", "@warkypublic/artemis-kit": "^1.0.10", "@warkypublic/zustandsyncstore": "^0.0.4", + "react-hook-form": "^7.71.0", + "immer": "^10.1.3", "react": ">= 19.0.0", "react-dom": ">= 19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35a63f7..962ddeb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@mantine/hooks': specifier: ^8.3.1 version: 8.3.1(react@19.2.0) + '@mantine/modals': + specifier: ^8.3.5 + version: 8.3.12(@mantine/core@8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mantine/hooks@8.3.1(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mantine/notifications': specifier: ^8.3.5 version: 8.3.5(@mantine/core@8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mantine/hooks@8.3.1(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -38,6 +41,9 @@ importers: moment: specifier: ^2.30.1 version: 2.30.1 + react-hook-form: + specifier: ^7.71.0 + version: 7.71.0(react@19.2.0) use-sync-external-store: specifier: '>= 1.4.0' version: 1.5.0(react@19.2.0) @@ -702,6 +708,14 @@ packages: peerDependencies: react: ^18.x || ^19.x + '@mantine/modals@8.3.12': + resolution: {integrity: sha512-+uRyGe2lLy601qlMk+8aR9d/Aibu+dZi6Jcmvm5z8Gw4ocviyMMlnd8BLSQ/Jvib2OX8fWj+yUQN7FMQ4Rbwjw==} + peerDependencies: + '@mantine/core': 8.3.12 + '@mantine/hooks': 8.3.12 + react: ^18.x || ^19.x + react-dom: ^18.x || ^19.x + '@mantine/notifications@8.3.5': resolution: {integrity: sha512-8TvzrPxfdtOLGTalv7Ei1hy2F6KbR3P7/V73yw3AOKhrf1ydS89sqV2ShbsucHGJk9Pto0wjdTPd8Q7pm5MAYw==} peerDependencies: @@ -2843,6 +2857,12 @@ packages: resolution: {integrity: sha512-OeR2jAxdoqUMHIn/nS9fgreI5hSpgGoL5ezdal4+oO7YSSgJR8ga+PkYGJrSrJ9MKlPcQjMQXnketrD7WNmNsg==} engines: {node: '>= 6'} + react-hook-form@7.71.0: + resolution: {integrity: sha512-oFDt/iIFMV9ZfV52waONXzg4xuSlbwKUPvXVH2jumL1me5qFhBMc4knZxuXiZ2+j6h546sYe3ZKJcg/900/iHw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-html-attributes@1.4.6: resolution: {integrity: sha512-uS3MmThNKFH2EZUQQw4k5pIcU7XIr208UE5dktrj/GOH1CMagqxDl4DCLpt3o2l9x+IB5nVYBeN3Cr4IutBXAg==} @@ -4285,6 +4305,13 @@ snapshots: dependencies: react: 19.2.0 + '@mantine/modals@8.3.12(@mantine/core@8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mantine/hooks@8.3.1(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@mantine/core': 8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mantine/hooks': 8.3.1(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + '@mantine/notifications@8.3.5(@mantine/core@8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mantine/hooks@8.3.1(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@mantine/core': 8.3.1(@mantine/hooks@8.3.1(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -6686,6 +6713,10 @@ snapshots: dependencies: prop-types: 15.8.1 + react-hook-form@7.71.0(react@19.2.0): + dependencies: + react: 19.2.0 + react-html-attributes@1.4.6: dependencies: html-element-attributes: 1.3.1 diff --git a/src/Form/components/Form.tsx b/src/Form/components/Form.tsx deleted file mode 100644 index 4c7c047..0000000 --- a/src/Form/components/Form.tsx +++ /dev/null @@ -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 & { - Section: typeof FormSection -} = ({ children, loading, ...others }) => { - return ( - - - {children} - - ) -} - -Form.Section = FormSection diff --git a/src/Form/components/FormLayout.tsx b/src/Form/components/FormLayout.tsx deleted file mode 100644 index 3fe1c74..0000000 --- a/src/Form/components/FormLayout.tsx +++ /dev/null @@ -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 = ({ - children, - modal, - modalProps, - ...others -}) => { - const { request } = useFormLayoutStore((state) => ({ - request: state.request, - })) - - const modalWidth = request === 'delete' ? 400 : modalProps?.width - - return modal === true ? ( - modalProps?.onClose?.()} - opened={modalProps?.opened || false} - size="auto" - withCloseButton={false} - centered={request !== 'delete'} - {...modalProps} - > -
-
{children}
-
-
- ) : ( -
{children}
- ) -} - -export const FormLayout: React.FC = (props) => ( - - - -) diff --git a/src/Form/components/FormSection.tsx b/src/Form/components/FormSection.tsx deleted file mode 100644 index 7fa9368..0000000 --- a/src/Form/components/FormSection.tsx +++ /dev/null @@ -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 = ({ - 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 ( - - - {title} - - {rightSection && {rightSection}} - - ) - } - - if (type === 'body') { - return ( - - {children} - - ) - } - - if (type === 'footer') { - return ( - - {children} - {request !== 'view' && ( - <> - - - - )} - - ) - } - - if (type === 'error') { - return ( - - {children} - - ) - } - - return null -} diff --git a/src/Form/components/SuperForm.tsx b/src/Form/components/SuperForm.tsx deleted file mode 100644 index 935a8f7..0000000 --- a/src/Form/components/SuperForm.tsx +++ /dev/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 = ( - { useFormProps, gridRef, children, persist, ...others }: SuperFormProps, - ref -) => { - const form = useForm({ ...useFormProps }) - - return ( - - - {persist && ( - - )} - gridRef={gridRef} ref={ref}> - {children} - - - - ) -} - -const FRSuperForm = forwardRef(SuperForm) as ( - props: SuperFormProps & { - ref?: Ref> - } -) => ReactElement - -export default FRSuperForm diff --git a/src/Form/components/SuperFormLayout.tsx b/src/Form/components/SuperFormLayout.tsx deleted file mode 100644 index 51dc6a2..0000000 --- a/src/Form/components/SuperFormLayout.tsx +++ /dev/null @@ -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 = ( - { - children, - gridRef, - }: { - children: React.ReactNode | ((props: UseFormReturn) => React.ReactNode) - gridRef?: MutableRefObject | 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() - 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 = ( - <> - - _setOpened(!_opened)} - radius='6' - m={2} - > - {_opened ? : } - - - - - {typeof children === 'function' ? children({ ...form }) : children} - - - {(transitionStyles) => ( - - {layoutProps?.bodyRightSection?.render?.({ - form, - formValue: form.getValues(), - isFetching, - opened: _opened, - queryKey, - setOpened: _setOpened, - })} - - )} - - - - ) - - // 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>(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 ( -
{ - e.stopPropagation() - e.preventDefault() - form.handleSubmit((data: T | any) => { - onFormSubmit(data) - })(e) - }} - style={{ height: '100%' }} - className={request === 'view' ? classes.disabled : ''} - > - {/* */} - {layoutProps?.noLayout ? ( - typeof layoutProps?.bodyRightSection?.render === 'function' ? ( - renderRightSection - ) : typeof children === 'function' ? ( - <> - - {children({ ...form })} - - ) : ( - <> - - {children} - - ) - ) : ( - onCancel?.(request)} - onSubmit={form.handleSubmit((data: T | any) => { - onFormSubmit(data) - })} - request={request} - modal={false} - nested={nested} - > - {!layoutProps?.noHeader && ( - - )} - {(Object.keys(formState.errors)?.length > 0 || error) && ( - - - {(error?.message?.length ?? 0) > 0 - ? 'Server Error' - : 'Required information is incomplete*'} - - {(error as any)?.response?.data?.msg || - (error as any)?.response?.data?._error || - error?.message} - {layoutProps?.showErrorList !== false && ( - - - {getErrorMessages(formState.errors)} - - - )} - - )} - {typeof layoutProps?.bodyRightSection?.render === 'function' ? ( - - {renderRightSection} - - ) : ( - - {typeof children === 'function' ? children({ ...form }) : children} - - )} - {!layoutProps?.noFooter && ( - >) - : layoutProps?.footerSectionProps)} - > - {typeof layoutProps?.extraButtons === 'function' - ? layoutProps?.extraButtons(form) - : layoutProps?.extraButtons} - - )} - - )} - - ) -} - -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 {errors[key]} - }) -} - -const FRSuperFormLayout = forwardRef(SuperFormLayout) as ( - props: { - children: React.ReactNode | ((props: UseFormReturn) => React.ReactNode) - gridRef?: MutableRefObject - } & { - ref?: Ref> - } -) => ReactElement - -export default FRSuperFormLayout diff --git a/src/Form/components/SuperFormPersist.tsx b/src/Form/components/SuperFormPersist.tsx deleted file mode 100644 index 8026511..0000000 --- a/src/Form/components/SuperFormPersist.tsx +++ /dev/null @@ -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('') - 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 diff --git a/src/Form/config/ApiConfig.tsx b/src/Form/config/ApiConfig.tsx deleted file mode 100644 index 588898b..0000000 --- a/src/Form/config/ApiConfig.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { createContext, useContext, ReactNode, useState } from 'react' - -interface ApiConfigContextValue { - apiURL: string - setApiURL: (url: string) => void -} - -const ApiConfigContext = createContext(null) - -interface ApiConfigProviderProps { - children: ReactNode - defaultApiURL?: string -} - -export const ApiConfigProvider: React.FC = ({ - children, - defaultApiURL = '', -}) => { - const [apiURL, setApiURL] = useState(defaultApiURL) - - return ( - - {children} - - ) -} - -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 -} diff --git a/src/Form/containers/Drawer/SuperFormDrawer.tsx b/src/Form/containers/Drawer/SuperFormDrawer.tsx deleted file mode 100644 index 111ad82..0000000 --- a/src/Form/containers/Drawer/SuperFormDrawer.tsx +++ /dev/null @@ -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 = ( - { drawerProps, noCloseOnSubmit, ...formProps }: SuperFormDrawerProps, - ref: Ref> -) => { - // Component Refs - const formRef = useRef>(null) - const drawerRef = useRef(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>( - ref, - () => ({ - ...formRef.current, - drawer: drawerRef.current, - } as SuperFormDrawerRef), - [layoutMounted] - ) - - return ( - { - 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} - > - - {...formProps} - onCancel={onCancel} - onSubmit={onSubmit} - onLayoutMounted={onLayoutMounted} - onLayoutUnMounted={onLayoutUnMounted} - ref={formRef} - layoutProps={{ - ...formProps?.layoutProps, - rightSection: ( - { - onCancel(formProps?.request) - }} - > - - - ), - title: - (drawerProps.title as string) ?? - formProps?.layoutProps?.title ?? - (formProps?.request as string), - }} - /> - - ) -} - -const FRSuperFormDrawer = forwardRef(SuperFormDrawer) as ( - props: SuperFormDrawerProps & { ref?: Ref> } -) => ReactElement - -export default FRSuperFormDrawer diff --git a/src/Form/containers/Modal/SuperFormModal.tsx b/src/Form/containers/Modal/SuperFormModal.tsx deleted file mode 100644 index 46bd768..0000000 --- a/src/Form/containers/Modal/SuperFormModal.tsx +++ /dev/null @@ -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 = ( - { modalProps, noCloseOnSubmit, ...formProps }: SuperFormModalProps, - ref: Ref> -) => { - // Component Refs - const modalRef = useRef(null) - const formRef = useRef>(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>( - ref, - () => ({ - ...formRef.current, - modal: modalRef.current, - } as SuperFormModalRef), - [layoutMounted] - ) - - return ( - - - {...formProps} - onCancel={onCancel} - onSubmit={onSubmit} - onLayoutMounted={onLayoutMounted} - onLayoutUnMounted={onLayoutUnMounted} - ref={formRef} - /> - - ) -} - -const FRSuperFormModal = forwardRef(SuperFormModal) as ( - props: SuperFormModalProps & { ref?: Ref> } -) => ReactElement - -export default FRSuperFormModal diff --git a/src/Form/containers/Popover/SuperFormPopover.tsx b/src/Form/containers/Popover/SuperFormPopover.tsx deleted file mode 100644 index 264af05..0000000 --- a/src/Form/containers/Popover/SuperFormPopover.tsx +++ /dev/null @@ -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 = ( - { popoverProps, target, noCloseOnSubmit, ...formProps }: SuperFormPopoverProps, - ref: Ref> -) => { - // Component Refs - const popoverRef = useRef(null) - const formRef = useRef>(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>( - ref, - () => ({ - ...formRef.current, - popover: popoverRef.current, - } as SuperFormPopoverRef), - [layoutMounted] - ) - - return ( - _onChange(false)} - opened={_value} - position='left' - radius='md' - withArrow - withinPortal - zIndex={200} - keepMounted={false} - {...popoverProps} - > - - _onChange(true)}>{target} - - - - - - ) -} - -const FRSuperFormPopover = forwardRef(SuperFormPopover) as ( - props: SuperFormPopoverProps & { ref?: Ref> } -) => ReactElement - -export default FRSuperFormPopover diff --git a/src/Form/hooks/use-drawer-form-state.tsx b/src/Form/hooks/use-drawer-form-state.tsx deleted file mode 100644 index 57dfbac..0000000 --- a/src/Form/hooks/use-drawer-form-state.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { useState } from 'react' -import { FieldValues } from 'react-hook-form' -import { SuperFormProps, RequestType, ExtendedDrawerProps } from '../types' - -interface UseDrawerFormState extends Partial> { - drawerProps: Partial - opened?: boolean - onClose?: () => void - request: RequestType - [key: string]: any -} - -type AskFunction = (request: RequestType, buffer: any) => void - -const useDrawerFormState = ( - props?: Partial> -): { - formProps: UseDrawerFormState - setFormProps: React.Dispatch>> - open: (props: Partial>) => void - close: () => void - ask: AskFunction -} => { - const [formProps, setFormProps] = useState>({ - 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>) => { - 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 } diff --git a/src/Form/hooks/use-modal-form-state.tsx b/src/Form/hooks/use-modal-form-state.tsx deleted file mode 100644 index 9156aa8..0000000 --- a/src/Form/hooks/use-modal-form-state.tsx +++ /dev/null @@ -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 extends Partial> { - modalProps: ModalProps - opened?: boolean - onClose?: () => void - request: RequestType - [key: string]: any -} - -type AskFunction = (request: RequestType, buffer: any) => void - -const useModalFormState = ( - props?: Partial> -): { - formProps: UseModalFormState - setFormProps: React.Dispatch>> - open: (props: Partial>) => void - close: () => void - ask: AskFunction -} => { - const [formProps, setFormProps] = useState>({ - 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>) => { - 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 } diff --git a/src/Form/hooks/use-popover-form-state.tsx b/src/Form/hooks/use-popover-form-state.tsx deleted file mode 100644 index 6b86607..0000000 --- a/src/Form/hooks/use-popover-form-state.tsx +++ /dev/null @@ -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 extends Partial> { - popoverProps: Omit - opened?: boolean - onClose?: () => void - request: RequestType - [key: string]: any -} - -type AskFunction = (request: RequestType, buffer: any) => void - -const usePopoverFormState = ( - props?: Partial> -): { - formProps: UsePopoverFormState - setFormProps: React.Dispatch>> - open: (props: Partial>) => void - close: () => void - ask: AskFunction -} => { - const [formProps, setFormProps] = useState>({ - 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>) => { - 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 } diff --git a/src/Form/hooks/use-subscribe.tsx b/src/Form/hooks/use-subscribe.tsx deleted file mode 100644 index 12dec90..0000000 --- a/src/Form/hooks/use-subscribe.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useEffect } from 'react' -import { - EventType, - FieldValues, - FormState, - InternalFieldName, - Path, - ReadFormState, - useFormContext, - UseFormReturn, -} from 'react-hook-form' - -const useSubscribe = ( - name: Path | readonly Path[] | undefined, - callback: ( - data: Partial> & { - values: FieldValues - name?: InternalFieldName - type?: EventType - }, - form?: UseFormReturn - ) => void, - formState?: ReadFormState, - deps?: unknown[] -) => { - const form = useFormContext() - - 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 diff --git a/src/Form/hooks/use-super-form-state.tsx b/src/Form/hooks/use-super-form-state.tsx deleted file mode 100644 index 90b6e25..0000000 --- a/src/Form/hooks/use-super-form-state.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useState } from 'react' -import { FieldValues } from 'react-hook-form' -import { SuperFormProps, RequestType } from '../types' - -interface UseSuperFormState extends Partial> { - request: RequestType - [key: string]: any -} - -type AskFunction = (request: RequestType, buffer: any) => void - -const useSuperFormState = ( - props?: UseSuperFormState -): { - formProps: UseSuperFormState - setFormProps: React.Dispatch>> - ask: AskFunction -} => { - const [formProps, setFormProps] = useState>({ - request: 'insert', - ...props, - }) - - return { - formProps, - setFormProps, - ask: (request: RequestType, buffer: any) => { - setFormProps((curr) => ({ - ...curr, - request, - value: buffer, - })) - }, - } -} - -export default useSuperFormState -export type { UseSuperFormState } diff --git a/src/Form/hooks/useRemote.tsx b/src/Form/hooks/useRemote.tsx deleted file mode 100644 index 07603a2..0000000 --- a/src/Form/hooks/useRemote.tsx +++ /dev/null @@ -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 = (gridRef?: MutableRefObject | 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() - 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>({ - queryKey, - queryFn: () => fetchClient.get(`${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 diff --git a/src/Form/store/FormLayout.store.tsx b/src/Form/store/FormLayout.store.tsx deleted file mode 100644 index 80c7dbe..0000000 --- a/src/Form/store/FormLayout.store.tsx +++ /dev/null @@ -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((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 | null>(null) - -export const FormLayoutStoreProvider: React.FC<{ children: ReactNode; [key: string]: any }> = ({ - children, - ...props -}) => { - const storeRef = React.useRef>() - if (!storeRef.current) { - storeRef.current = createFormLayoutStore(props) - } - - return ( - - {children} - - ) -} - -export const useFormLayoutStore = (selector: (state: FormLayoutState) => T): T => { - const store = useContext(FormLayoutStoreContext) - if (!store) { - throw new Error('useFormLayoutStore must be used within FormLayoutStoreProvider') - } - return store(selector) -} diff --git a/src/Form/store/SuperForm.store.tsx b/src/Form/store/SuperForm.store.tsx deleted file mode 100644 index 22d62ce..0000000 --- a/src/Form/store/SuperForm.store.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { SuperFormProviderProps } from '../types' -import { createSyncStore } from '@warkypublic/zustandsyncstore' - -const { Provider, useStore } = createSyncStore((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 diff --git a/src/Form/styles/Form.module.css b/src/Form/styles/Form.module.css deleted file mode 100644 index aa6f336..0000000 --- a/src/Form/styles/Form.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.disabled { - pointer-events: none; - opacity: 0.9; -} - -.sticky { - position: -webkit-sticky; - position: sticky; - bottom: 0; -} diff --git a/src/Form/types/form.types.ts b/src/Form/types/form.types.ts deleted file mode 100644 index c415c1b..0000000 --- a/src/Form/types/form.types.ts +++ /dev/null @@ -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 { - 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 { - opened?: boolean - setOpened?: (opened: boolean) => void - w: number | string - hideToggleButton?: boolean - paperProps?: PaperProps - render: (props: { - form: UseFormReturn - formValue: T - isFetching: boolean - opened: boolean - queryKey: any - setOpened: (opened: boolean) => void - }) => React.ReactNode -} - -export interface SuperFormLayoutProps { - buttonTitles?: { submit?: string; cancel?: string } - extraButtons?: React.ReactNode | ((form: UseFormReturn) => React.ReactNode) - noFooter?: boolean - noHeader?: boolean - noLayout?: boolean - bodySectionProps?: Partial - footerSectionProps?: - | Partial - | ((ref: React.RefObject>) => Partial) - rightSection?: React.ReactNode - bodyRightSection?: BodyRightSection - title?: string - showErrorList?: boolean -} - -export interface CommonFormProps { - gridRef?: React.MutableRefObject | null> - layoutProps?: SuperFormLayoutProps - meta?: { [key: string]: any } - nested?: boolean - onCancel?: (request: RequestType) => void - onLayoutMounted?: () => void - onLayoutUnMounted?: () => void - onResetForm?: (data: T, form?: UseFormReturn) => Promise - onBeforeSubmit?: ( - data: T, - request: RequestType, - form?: UseFormReturn - ) => Promise - onSubmit?: ( - data: T, - request: RequestType, - formData?: T, - form?: UseFormReturn, - closeForm?: boolean - ) => void - primeData?: any - readonly?: boolean - remote?: RemoteConfig - request: RequestType - persist?: boolean | { storageKey?: string } - useMutationOptions?: UseMutationOptions - useQueryOptions?: Partial> - value?: T | null -} - -export interface SuperFormProps extends CommonFormProps { - children: React.ReactNode | ((props: UseFormReturn) => React.ReactNode) - useFormProps?: UseFormProps -} - -export interface SuperFormProviderProps extends Omit, 'children'> { - children?: React.ReactNode -} - -export interface SuperFormModalProps extends SuperFormProps { - modalProps: ModalProps - noCloseOnSubmit?: boolean -} - -export interface SuperFormPopoverProps extends SuperFormProps { - popoverProps?: Omit - target: any - noCloseOnSubmit?: boolean -} - -export interface ExtendedDrawerProps extends DrawerProps { - // Add any extended drawer props needed - [key: string]: any -} - -export interface SuperFormDrawerProps extends SuperFormProps { - drawerProps: ExtendedDrawerProps - noCloseOnSubmit?: boolean -} - -export interface SuperFormRef { - form: UseFormReturn - mutation: Partial> - submit: (closeForm?: boolean, afterSubmit?: (data: T | any) => void) => Promise - queryKey?: any - getFormState: () => UseFormStateReturn -} - -export interface SuperFormDrawerRef extends SuperFormRef { - drawer: HTMLDivElement | null -} - -export interface SuperFormModalRef extends SuperFormRef { - modal: HTMLDivElement | null -} - -export interface SuperFormPopoverRef extends SuperFormRef { - popover: HTMLDivElement | null -} diff --git a/src/Form/types/index.ts b/src/Form/types/index.ts deleted file mode 100644 index 31e655c..0000000 --- a/src/Form/types/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './form.types' -export * from './remote.types' diff --git a/src/Form/types/remote.types.ts b/src/Form/types/remote.types.ts deleted file mode 100644 index 3101e33..0000000 --- a/src/Form/types/remote.types.ts +++ /dev/null @@ -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[] -} diff --git a/src/Form/utils/fetchClient.ts b/src/Form/utils/fetchClient.ts deleted file mode 100644 index 23e899f..0000000 --- a/src/Form/utils/fetchClient.ts +++ /dev/null @@ -1,161 +0,0 @@ -export interface FetchOptions extends RequestInit { - params?: Record - timeout?: number -} - -export interface FetchResponse { - 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 { - 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( - url: string, - options?: FetchOptions -): Promise> { - 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( - url: string, - data?: any, - options?: FetchOptions -): Promise> { - 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( - url: string, - options?: FetchOptions -): Promise> { - 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, -} diff --git a/src/Form/utils/getNestedValue.ts b/src/Form/utils/getNestedValue.ts deleted file mode 100644 index be6265f..0000000 --- a/src/Form/utils/getNestedValue.ts +++ /dev/null @@ -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) -} diff --git a/src/Form/utils/openConfirmModal.ts b/src/Form/utils/openConfirmModal.ts deleted file mode 100644 index baef0fa..0000000 --- a/src/Form/utils/openConfirmModal.ts +++ /dev/null @@ -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: ( - - - You have unsaved changes in this form. - - - {description ?? - 'Closing now will discard any modifications you have made. Are you sure you want to continue?'} - - - ), - 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, - }) diff --git a/src/Former/Former.store.tsx b/src/Former/Former.store.tsx new file mode 100644 index 0000000..3806971 --- /dev/null +++ b/src/Former/Former.store.tsx @@ -0,0 +1,188 @@ +import { createSyncStore } from '@warkypublic/zustandsyncstore'; +import { produce } from 'immer'; + +import type { FormerProps, FormerState } from './Former.types'; + +const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore< + FormerState & Partial>, + FormerProps +>( + (set, get) => ({ + getState: (key) => { + const current = get(); + return current?.[key]; + }, + load: async (reset?: boolean) => { + try { + set({ loading: true }); + const keyName = get()?.apiKeyField || 'id'; + const keyValue = (get().values as any)?.[keyName] ?? (get().primeData as any)?.[keyName]; + if (get().onAPICall && keyValue !== undefined) { + let data = await get().onAPICall!( + 'read', + get().request || 'insert', + get().values, + keyValue + ); + if (get().afterGet) { + data = await get().afterGet!({ ...data }); + } + set({ loading: false, values: data }); + get().onChange?.(data); + } + if (reset && get().getFormMethods) { + const formMethods = get().getFormMethods!(); + formMethods.reset(); + } + } catch (e) { + set({ error: (e as Error)?.message ?? e, loading: false }); + } + set({ loading: false }); + }, + + onChange: (values) => { + set({ values }); + }, + request: 'insert', + reset: async () => { + const state = get(); + if (state.getFormMethods) { + if (state.request !== 'insert') { + await state.load(true); + } + + const formMethods = state.getFormMethods!(); + formMethods.reset({ ...state.values, ...state.primeData }); + } + }, + save: async (e?: React.BaseSyntheticEvent | undefined) => { + try { + const keepOpen = get().keepOpen ?? false; + set({ loading: true }); + if (get().getFormMethods) { + const formMethods = get().getFormMethods!(); + + let data = formMethods.getValues(); + + if (get().beforeSave) { + const newData = await get().beforeSave!(data); + data = newData; + } + + let exit = false; + const handler = formMethods.handleSubmit( + (newdata) => { + data = newdata; + }, + (errors) => { + set({ error: errors.root?.message || 'Validation errors', loading: false }); + exit = true; + } + ); + + await handler(e); + + //console.log('Former.store.tsx save called', success, e, data, get().getFormMethods); + if (exit) { + set({ loading: false }); + return undefined; + } + + if (get().request === 'delete' && !get().deleteConfirmed) { + const confirmed = (await get().onConfirmDelete?.(data)) ?? false; + if (!confirmed) { + set({ loading: false }); + return undefined; + } + } + + if (get().onAPICall) { + const keyName = get()?.apiKeyField || 'id'; + const keyValue = + (get().values as any)?.[keyName] ?? (get().primeData as any)?.[keyName]; + const savedData = await get().onAPICall!( + 'mutate', + get().request || 'insert', + data, + keyValue + ); + if (get().afterSave) { + await get().afterSave!(savedData); + } + set({ loading: false, values: savedData }); + get().onChange?.(savedData); + if (!keepOpen) { + get().onClose?.(savedData); + } + return savedData; + } + + set({ loading: false, values: data }); + get().onChange?.(data); + if (!keepOpen) { + get().onClose?.(data); + } + + return data; + } + } catch (e) { + set({ error: (e as Error)?.message ?? e, loading: false }); + } + + return undefined; + }, + setRequest: (request) => { + set({ request }); + }, + setState: (key, value) => { + set( + produce((state) => { + state[key] = value; + }) + ); + }, + setStateFN: (key, value) => { + const p = new Promise((resolve, reject) => { + set( + produce((state) => { + if (typeof value === 'function') { + state[key] = (value as (value: unknown) => unknown)(state[key]); + } else { + reject(new Error(`Not a function ${value}`)); + throw Error(`Not a function ${value}`); + } + }) + ); + resolve(); + }); + + return p; + }, + validate: async () => { + if (get().getFormMethods) { + const formMethods = get().getFormMethods!(); + const isValid = await formMethods.trigger(); + return isValid; + } + return true; + }, + values: undefined, + }), + ({ onConfirmDelete, primeData, request, values }) => { + let _onConfirmDelete = onConfirmDelete; + if (!onConfirmDelete) { + _onConfirmDelete = async () => { + return confirm('Are you sure you want to delete this item?'); + }; + } + return { + onConfirmDelete: _onConfirmDelete, + primeData, + request: request || 'insert', + values: { ...primeData, ...values }, + }; + } +); + +export { FormerProvider }; +export { useFormerStore }; diff --git a/src/Former/Former.tsx b/src/Former/Former.tsx new file mode 100644 index 0000000..43017ee --- /dev/null +++ b/src/Former/Former.tsx @@ -0,0 +1,115 @@ +import { forwardRef, type PropsWithChildren, useEffect, useImperativeHandle } from 'react'; +import { type FieldValues, FormProvider, useForm } from 'react-hook-form'; + +import type { FormerProps, FormerRef } from './Former.types'; + +import { FormerProvider, useFormerStore } from './Former.store'; +import { FormerLayout } from './FormerLayout'; + +const FormerInner = forwardRef, Partial> & PropsWithChildren>( + function FormerInner( + props: Partial> & PropsWithChildren, + ref: any + ) { + const { + getState, + onChange, + onClose, + onOpen, + opened, + primeData, + reset, + save, + setState, + useFormProps, + validate, + values, + wrapper, + } = useFormerStore((state) => ({ + getState: state.getState, + onChange: state.onChange, + onClose: state.onClose, + onOpen: state.onOpen, + opened: state.opened, + primeData: state.primeData, + reset: state.reset, + save: state.save, + setState: state.setState, + useFormProps: state.useFormProps, + validate: state.validate, + values: state.values, + wrapper: state.wrapper, + })); + + const formMethods = useForm({ + defaultValues: primeData, + mode: 'all', + shouldUseNativeValidation: true, + values: values, + ...useFormProps, + }); + + useImperativeHandle( + ref, + () => ({ + close: async () => { + //console.log('close called'); + onClose?.(); + setState('opened', false); + }, + getValue: () => { + return getState('values'); + }, + reset: () => { + reset(); + }, + save: async () => { + return await save(); + }, + setValue: (value: T) => { + onChange?.(value); + }, + show: async () => { + //console.log('show called'); + setState('opened', true); + onOpen?.(); + }, + validate: async () => { + return await validate(); + }, + }), + [getState, onChange] + ); + + useEffect(() => { + setState('getFormMethods', () => formMethods); + }, [formMethods]); + + return ( + + {typeof wrapper === 'function' ? ( + wrapper({props.children}, opened, onClose, onOpen, getState) + ) : ( + {props.children || null} + )} + + ); + } +); + +export const Former = forwardRef, FormerProps & PropsWithChildren>( + function Former( + props: FormerProps & PropsWithChildren, + ref: any + ) { + //if opened is false and wrapper is defined as function, do not render anything + if (!props.opened && typeof props.wrapper === 'function') { + return null; + } + return ( + + {props.children} + + ); + } +); diff --git a/src/Former/Former.types.ts b/src/Former/Former.types.ts new file mode 100644 index 0000000..e36f874 --- /dev/null +++ b/src/Former/Former.types.ts @@ -0,0 +1,73 @@ +import type { LoadingOverlayProps, ScrollAreaAutosizeProps } from '@mantine/core'; +import type { FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form'; + +export interface FormerProps { + afterGet?: (data: T) => Promise | void; + afterSave?: (data: T) => Promise | void; + apiKeyField?: string; + beforeSave?: (data: T) => Promise | T; + disableHTMlForm?: boolean; + keepOpen?: boolean; + onAPICall?: ( + mode: 'mutate' | 'read', + request: RequestType, + value?: T, + key?: number | string + ) => Promise; + onCancel?: () => void; + onChange?: (value: T) => void; + onClose?: (data?: T) => void; + onConfirmDelete?: (values?: T) => Promise; + onOpen?: (data?: T) => void; + + opened?: boolean; + primeData?: T; + request: RequestType; + useFormProps?: UseFormProps; + values?: T; + wrapper?: ( + children: React.ReactNode, + opened: boolean | undefined, + onClose: ((data?: T) => void) | undefined, + onOpen: ((data?: T) => void) | undefined, + getState: >(key: K) => FormStateAndProps[K] + ) => React.ReactNode; +} + +export interface FormerRef { + close: () => Promise; + getValue: () => T | undefined; + reset: () => void; + save: () => Promise; + setValue: (value: T) => void; + show: () => Promise; + validate: () => Promise; +} + +export interface FormerState { + deleteConfirmed?: boolean; + error?: string; + getFormMethods?: () => UseFormReturn; + getState: >(key: K) => FormStateAndProps[K]; + load: (reset?: boolean) => Promise; + loading?: boolean; + loadingOverlayProps?: LoadingOverlayProps; + reset: (e?: React.BaseSyntheticEvent | undefined) => Promise; + save: (e?: React.BaseSyntheticEvent | undefined) => Promise; + scrollAreaProps?: ScrollAreaAutosizeProps; + setRequest: (request: RequestType) => void; + setState: >( + key: K, + value: Partial>[K] + ) => void; + setStateFN: >( + key: K, + value: (current: FormStateAndProps[K]) => Partial[K]> + ) => Promise; + validate: () => Promise; +} + +export type FormStateAndProps = FormerProps & + Partial>; + +export type RequestType = 'delete' | 'insert' | 'select' | 'update' | 'view'; diff --git a/src/Former/FormerLayout.tsx b/src/Former/FormerLayout.tsx new file mode 100644 index 0000000..6b76f46 --- /dev/null +++ b/src/Former/FormerLayout.tsx @@ -0,0 +1,93 @@ +import { LoadingOverlay, ScrollAreaAutosize } from '@mantine/core'; +import { type PropsWithChildren, useEffect } from 'react'; + +import { useFormerStore } from './Former.store'; + +export const FormerLayout = (props: PropsWithChildren) => { + const { + disableHTMlForm, + getFormMethods, + load, + loading, + loadingOverlayProps, + request, + reset, + save, + scrollAreaProps, + } = useFormerStore((state) => ({ + disableHTMlForm: state.disableHTMlForm, + getFormMethods: state.getFormMethods, + load: state.load, + loading: state.loading, + loadingOverlayProps: state.loadingOverlayProps, + request: state.request, + reset: state.reset, + save: state.save, + scrollAreaProps: state.scrollAreaProps, + })); + + useEffect(() => { + if (getFormMethods) { + const formMethods = getFormMethods(); + if (formMethods && request !== 'insert') { + load(true); + } + } + }, [getFormMethods, request]); + + if (disableHTMlForm) { + return ( + + {props.children} + + + ); + } + + return ( + +
reset(e)} onSubmit={(e) => save(e)}> + {props.children} + + +
+ ); +}; diff --git a/src/Former/index.ts b/src/Former/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/Former/stories/Gridler.goapi.stories.tsx b/src/Former/stories/Gridler.goapi.stories.tsx new file mode 100644 index 0000000..15ce215 --- /dev/null +++ b/src/Former/stories/Gridler.goapi.stories.tsx @@ -0,0 +1,42 @@ +//@ts-nocheck +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Box } from '@mantine/core'; +import { fn } from 'storybook/test'; + +import { FormTest } from './example'; + +const Renderable = (props: any) => { + return ( + + + + ); +}; + +const meta = { + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onClick: fn() }, + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + backgroundColor: { control: 'color' }, + }, + component: Renderable, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + //layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + title: 'Former/Former Basic', +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const BasicExample: Story = { + args: { + label: 'Test', + }, +}; diff --git a/src/Former/stories/example.tsx b/src/Former/stories/example.tsx new file mode 100644 index 0000000..2cf9316 --- /dev/null +++ b/src/Former/stories/example.tsx @@ -0,0 +1,118 @@ +import { Button, Drawer, Group, Paper, Select, Stack, Switch } from '@mantine/core'; +import { useRef, useState } from 'react'; +import { Controller } from 'react-hook-form'; + +import type { FormerRef } from '../Former.types'; + +import { Former } from '../Former'; + +export const FormTest = () => { + const [request, setRequest] = useState('insert'); + const [wrapped, setWrapped] = useState(false); + const [open, setOpen] = useState(false); + const [formData, setFormData] = useState({ a: 99 }); + console.log('formData', formData); + + const ref = useRef(null); + return ( + + } + /> + } + rules={{ required: 'Field is required' }} + /> + + + + + + + + + ); +}; diff --git a/src/Former/todo.md b/src/Former/todo.md new file mode 100644 index 0000000..890935f --- /dev/null +++ b/src/Former/todo.md @@ -0,0 +1,11 @@ +- [ ] Headerspec API +- [ ] Relspec API +- [ ] SocketSpec API +- [ ] Layout Tool + - [ ] Header Section + - [ ] Button Section + - [ ] Footer Section +- [ ] Different Loaded for saving vs loading +- [ ] Better Confirm Dialog +- [ ] Reset Confirm Dialog +- [ ] Request insert and save but keep open (must clear key from API, also add callback) diff --git a/src/Gridler/components/BottomBar.tsx b/src/Gridler/components/BottomBar.tsx index 6b294d6..00d4a71 100644 --- a/src/Gridler/components/BottomBar.tsx +++ b/src/Gridler/components/BottomBar.tsx @@ -1,4 +1,4 @@ -/* eslint-disable react/react-in-jsx-scope */ + import { useGridlerStore } from './GridlerStore'; export function BottomBar() { diff --git a/src/Gridler/stories/Gridler.goapi.stories.tsx b/src/Gridler/stories/Gridler.goapi.stories.tsx index ef2d243..046e9aa 100644 --- a/src/Gridler/stories/Gridler.goapi.stories.tsx +++ b/src/Gridler/stories/Gridler.goapi.stories.tsx @@ -1,3 +1,4 @@ +//@ts-nocheck import type { Meta, StoryObj } from '@storybook/react-vite'; import { Box } from '@mantine/core'; @@ -6,7 +7,12 @@ import { fn } from 'storybook/test'; import { GridlerGoAPIExampleEventlog } from './Examples.goapi'; const Renderable = (props: any) => { - return ; + return ( + + {' '} + + + ); }; const meta = { @@ -19,7 +25,7 @@ const meta = { component: Renderable, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout - layout: 'centered', + //layout: 'centered', }, // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs tags: ['autodocs'], diff --git a/src/Gridler/stories/Gridler.localdata.stories.tsx b/src/Gridler/stories/Gridler.localdata.stories.tsx index 40b46f9..0d70ade 100644 --- a/src/Gridler/stories/Gridler.localdata.stories.tsx +++ b/src/Gridler/stories/Gridler.localdata.stories.tsx @@ -1,3 +1,4 @@ +//@ts-nocheck import type { Meta, StoryObj } from '@storybook/react-vite'; import { Box } from '@mantine/core'; @@ -24,7 +25,7 @@ const meta = { component: Renderable, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout - layout: 'centered', + // layout: 'centered', }, // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs tags: ['autodocs'], diff --git a/src/MantineBetterMenu/MantineBetterMenu.stories.tsx b/src/MantineBetterMenu/MantineBetterMenu.stories.tsx index 1173e16..643e1ff 100644 --- a/src/MantineBetterMenu/MantineBetterMenu.stories.tsx +++ b/src/MantineBetterMenu/MantineBetterMenu.stories.tsx @@ -5,21 +5,20 @@ import { fn } from 'storybook/test'; import { MantineBetterMenusProvider, useMantineBetterMenus } from './'; - -const Renderable = (props: Record) => { +const Renderable = (props: Record) => { return ( - - - + + + ); -} +}; const Menu = () => { const menus = useMantineBetterMenus(); - //menus.setState("menus",[{id:"test",items:[{id:"1",label:"Test",onClick:()=>{console.log("Clicked")}}]}]) - - return ; -} + //menus.setState("menus",[{id:"test",items:[{id:"1",label:"Test",onClick:()=>{console.log("Clicked")}}]}]) + + return ; +}; const meta = { // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args @@ -31,7 +30,7 @@ const meta = { component: Renderable, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout - layout: 'centered', + //layout: 'centered', }, // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs tags: ['autodocs'], @@ -44,8 +43,6 @@ type Story = StoryObj; // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args export const BasicExample: Story = { args: { - label: 'Test', }, }; -