feat(form): ✨ enhance form functionality and API integration
* Refactor key handling to use uniqueKeyField * Add reset functionality to clear dirty state after save * Introduce new API call specifications for REST and resolve * Implement predefined wrappers for dialogs and popovers * Update todo list to reflect completed tasks
This commit is contained in:
@@ -16,7 +16,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
load: async (reset?: boolean) => {
|
load: async (reset?: boolean) => {
|
||||||
try {
|
try {
|
||||||
set({ loading: true });
|
set({ loading: true });
|
||||||
const keyName = get()?.apiKeyField || 'id';
|
const keyName = get()?.uniqueKeyField || 'id';
|
||||||
const keyValue = (get().values as any)?.[keyName] ?? (get().primeData as any)?.[keyName];
|
const keyValue = (get().values as any)?.[keyName] ?? (get().primeData as any)?.[keyName];
|
||||||
if (get().onAPICall && keyValue !== undefined) {
|
if (get().onAPICall && keyValue !== undefined) {
|
||||||
let data = await get().onAPICall!(
|
let data = await get().onAPICall!(
|
||||||
@@ -98,7 +98,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (get().onAPICall) {
|
if (get().onAPICall) {
|
||||||
const keyName = get()?.apiKeyField || 'id';
|
const keyName = get()?.uniqueKeyField || 'id';
|
||||||
const keyValue =
|
const keyValue =
|
||||||
(get().values as any)?.[keyName] ?? (get().primeData as any)?.[keyName];
|
(get().values as any)?.[keyName] ?? (get().primeData as any)?.[keyName];
|
||||||
const savedData = await get().onAPICall!(
|
const savedData = await get().onAPICall!(
|
||||||
@@ -112,6 +112,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
}
|
}
|
||||||
set({ loading: false, values: savedData });
|
set({ loading: false, values: savedData });
|
||||||
get().onChange?.(savedData);
|
get().onChange?.(savedData);
|
||||||
|
formMethods.reset(savedData); //reset with saved data to clear dirty state
|
||||||
if (!keepOpen) {
|
if (!keepOpen) {
|
||||||
get().onClose?.(savedData);
|
get().onClose?.(savedData);
|
||||||
}
|
}
|
||||||
@@ -119,6 +120,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
}
|
}
|
||||||
|
|
||||||
set({ loading: false, values: data });
|
set({ loading: false, values: data });
|
||||||
|
formMethods.reset(data); //reset with saved data to clear dirty state
|
||||||
get().onChange?.(data);
|
get().onChange?.(data);
|
||||||
if (!keepOpen) {
|
if (!keepOpen) {
|
||||||
get().onClose?.(data);
|
get().onClose?.(data);
|
||||||
@@ -169,7 +171,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
},
|
},
|
||||||
values: undefined,
|
values: undefined,
|
||||||
}),
|
}),
|
||||||
({ onConfirmDelete, primeData, request, values, id }) => {
|
({ onConfirmDelete, primeData, request, values, id, onClose, useStoreApi }) => {
|
||||||
let _onConfirmDelete = onConfirmDelete;
|
let _onConfirmDelete = onConfirmDelete;
|
||||||
if (!onConfirmDelete) {
|
if (!onConfirmDelete) {
|
||||||
_onConfirmDelete = async () => {
|
_onConfirmDelete = async () => {
|
||||||
@@ -180,9 +182,28 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
|
|||||||
return {
|
return {
|
||||||
onConfirmDelete: _onConfirmDelete,
|
onConfirmDelete: _onConfirmDelete,
|
||||||
primeData,
|
primeData,
|
||||||
request: request || 'insert',
|
request: (request || 'insert').replace('change', 'update'),
|
||||||
values: { ...primeData, ...values },
|
values: { ...primeData, ...values },
|
||||||
id: !id ? newUUID() : id,
|
id: !id ? newUUID() : id,
|
||||||
|
onClose: () => {
|
||||||
|
const dirty = useStoreApi.getState().dirty;
|
||||||
|
const setState = useStoreApi.getState().setState;
|
||||||
|
if (dirty) {
|
||||||
|
if (confirm('You have unsaved changes. Are you sure you want to close?')) {
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
setState('opened', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
setState('opened', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -78,11 +78,20 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
|
|||||||
return await validate();
|
return await validate();
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[getState, onChange]
|
[getState, onChange, validate, save, reset, setState, onClose, onOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState('getFormMethods', () => formMethods);
|
setState('getFormMethods', () => formMethods);
|
||||||
|
|
||||||
|
if (formMethods) {
|
||||||
|
formMethods.subscribe({
|
||||||
|
formState: { isDirty: true },
|
||||||
|
callback: ({ isDirty }) => {
|
||||||
|
setState('dirty', isDirty);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}, [formMethods]);
|
}, [formMethods]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
LoadingOverlayProps,
|
LoadingOverlayProps,
|
||||||
ScrollAreaAutosizeProps,
|
ScrollAreaAutosizeProps,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import type React from 'react';
|
||||||
import type { FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form';
|
import type { FieldValues, UseFormProps, UseFormReturn } from 'react-hook-form';
|
||||||
|
|
||||||
export type FormerSectionRender<T extends FieldValues = any> = (
|
export type FormerSectionRender<T extends FieldValues = any> = (
|
||||||
@@ -14,20 +15,22 @@ export type FormerSectionRender<T extends FieldValues = any> = (
|
|||||||
getState: FormerState<T>['getState']
|
getState: FormerState<T>['getState']
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
|
|
||||||
export interface FormerProps<T extends FieldValues = any> {
|
export type FormerAPICallType<T extends FieldValues = any> = (
|
||||||
id?: string;
|
|
||||||
afterGet?: (data: T) => Promise<T> | void;
|
|
||||||
afterSave?: (data: T) => Promise<void> | void;
|
|
||||||
apiKeyField?: string;
|
|
||||||
beforeSave?: (data: T) => Promise<T> | T;
|
|
||||||
disableHTMlForm?: boolean;
|
|
||||||
keepOpen?: boolean;
|
|
||||||
onAPICall?: (
|
|
||||||
mode: 'mutate' | 'read',
|
mode: 'mutate' | 'read',
|
||||||
request: RequestType,
|
request: RequestType,
|
||||||
value?: T,
|
value?: T,
|
||||||
key?: number | string
|
key?: number | string
|
||||||
) => Promise<T>;
|
) => Promise<T>;
|
||||||
|
|
||||||
|
export interface FormerProps<T extends FieldValues = any> {
|
||||||
|
id?: string;
|
||||||
|
afterGet?: (data: T) => Promise<T> | void;
|
||||||
|
afterSave?: (data: T) => Promise<void> | void;
|
||||||
|
uniqueKeyField?: string;
|
||||||
|
beforeSave?: (data: T) => Promise<T> | T;
|
||||||
|
disableHTMlForm?: boolean;
|
||||||
|
keepOpen?: boolean;
|
||||||
|
onAPICall?: FormerAPICallType<T>;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onChange?: (value: T) => void;
|
onChange?: (value: T) => void;
|
||||||
onClose?: (data?: T) => void;
|
onClose?: (data?: T) => void;
|
||||||
@@ -35,6 +38,7 @@ export interface FormerProps<T extends FieldValues = any> {
|
|||||||
onOpen?: (data?: T) => void;
|
onOpen?: (data?: T) => void;
|
||||||
|
|
||||||
opened?: boolean;
|
opened?: boolean;
|
||||||
|
dirty?: boolean;
|
||||||
primeData?: T;
|
primeData?: T;
|
||||||
request: RequestType;
|
request: RequestType;
|
||||||
useFormProps?: UseFormProps<T>;
|
useFormProps?: UseFormProps<T>;
|
||||||
@@ -44,6 +48,8 @@ export interface FormerProps<T extends FieldValues = any> {
|
|||||||
layout?: {
|
layout?: {
|
||||||
renderTop?: FormerSectionRender<T>;
|
renderTop?: FormerSectionRender<T>;
|
||||||
renderBottom?: FormerSectionRender<T>;
|
renderBottom?: FormerSectionRender<T>;
|
||||||
|
saveButtonTitle?: React.ReactNode;
|
||||||
|
closeButtonTitle?: React.ReactNode;
|
||||||
saveButtonProps?: ButtonProps;
|
saveButtonProps?: ButtonProps;
|
||||||
closeButtonProps?: ButtonProps;
|
closeButtonProps?: ButtonProps;
|
||||||
buttonOnTop?: boolean;
|
buttonOnTop?: boolean;
|
||||||
|
|||||||
@@ -1,14 +1,33 @@
|
|||||||
import { Group, Button } from '@mantine/core';
|
import { Group, Button, Tooltip } from '@mantine/core';
|
||||||
import { IconX, IconDeviceFloppy } from '@tabler/icons-react';
|
import { IconX, IconDeviceFloppy } from '@tabler/icons-react';
|
||||||
import { useFormerStore } from './Former.store';
|
import { useFormerStore } from './Former.store';
|
||||||
|
|
||||||
export const FormerButtonArea = () => {
|
export const FormerButtonArea = () => {
|
||||||
const { save, onClose, buttonAreaGroupProps } = useFormerStore((state) => ({
|
const {
|
||||||
|
save,
|
||||||
|
onClose,
|
||||||
|
buttonAreaGroupProps,
|
||||||
|
saveButtonProps,
|
||||||
|
closeButtonProps,
|
||||||
|
closeButtonTitle,
|
||||||
|
saveButtonTitle,
|
||||||
|
request,
|
||||||
|
dirty,
|
||||||
|
} = useFormerStore((state) => ({
|
||||||
save: state.save,
|
save: state.save,
|
||||||
onClose: state.onClose,
|
onClose: state.onClose,
|
||||||
buttonAreaGroupProps: state.layout?.buttonAreaGroupProps,
|
buttonAreaGroupProps: state.layout?.buttonAreaGroupProps,
|
||||||
|
saveButtonProps: state.layout?.saveButtonProps,
|
||||||
|
closeButtonProps: state.layout?.closeButtonProps,
|
||||||
|
closeButtonTitle: state.layout?.closeButtonTitle,
|
||||||
|
saveButtonTitle: state.layout?.saveButtonTitle,
|
||||||
|
request: state.request,
|
||||||
|
dirty: state.dirty,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const disabledSave =
|
||||||
|
['select', 'view'].includes(request || '') || (['update'].includes(request || '') && !dirty);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group
|
||||||
justify="center"
|
justify="center"
|
||||||
@@ -24,22 +43,41 @@ export const FormerButtonArea = () => {
|
|||||||
leftSection={<IconX />}
|
leftSection={<IconX />}
|
||||||
size="sm"
|
size="sm"
|
||||||
px="md"
|
px="md"
|
||||||
onClick={() => onClose()}
|
|
||||||
miw={'8rem'}
|
miw={'8rem'}
|
||||||
|
{...closeButtonProps}
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Close
|
{closeButtonTitle || 'Close'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Tooltip
|
||||||
|
label={
|
||||||
|
disabledSave ? (
|
||||||
|
<p>
|
||||||
|
Cannot save in view or select mode, or no changes made. <br />
|
||||||
|
Try changing some values.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p>Save the current record</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
leftSection={<IconDeviceFloppy />}
|
leftSection={<IconDeviceFloppy />}
|
||||||
size="sm"
|
size="sm"
|
||||||
px="md"
|
px="md"
|
||||||
onClick={() => save()}
|
|
||||||
miw={'8rem'}
|
miw={'8rem'}
|
||||||
|
bg={request === 'delete' ? 'red' : undefined}
|
||||||
|
{...saveButtonProps}
|
||||||
|
disabled={disabledSave}
|
||||||
|
onClick={() => save()}
|
||||||
>
|
>
|
||||||
Save
|
{saveButtonTitle || 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
|||||||
save,
|
save,
|
||||||
scrollAreaProps,
|
scrollAreaProps,
|
||||||
id,
|
id,
|
||||||
layout,
|
opened,
|
||||||
getState,
|
|
||||||
} = useFormerStore((state) => ({
|
} = useFormerStore((state) => ({
|
||||||
disableHTMlForm: state.disableHTMlForm,
|
disableHTMlForm: state.disableHTMlForm,
|
||||||
getFormMethods: state.getFormMethods,
|
getFormMethods: state.getFormMethods,
|
||||||
@@ -30,8 +29,8 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
|||||||
save: state.save,
|
save: state.save,
|
||||||
scrollAreaProps: state.scrollAreaProps,
|
scrollAreaProps: state.scrollAreaProps,
|
||||||
id: state.id,
|
id: state.id,
|
||||||
layout: state.layout,
|
|
||||||
getState: state.getState,
|
opened: state.opened,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -41,7 +40,7 @@ export const FormerLayout = (props: PropsWithChildren) => {
|
|||||||
load(true);
|
load(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [getFormMethods, request]);
|
}, [getFormMethods, request, opened]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
72
src/Former/FormerResolveSpecAPI.ts
Normal file
72
src/Former/FormerResolveSpecAPI.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import type { FormerAPICallType } from './Former.types';
|
||||||
|
|
||||||
|
interface ResolveSpecRequest {
|
||||||
|
operation: 'read' | 'create' | 'update' | 'delete';
|
||||||
|
data?: Record<string, any>;
|
||||||
|
options?: {
|
||||||
|
preload?: string[];
|
||||||
|
columns?: string[];
|
||||||
|
filters?: Array<{ column: string; operator: string; value: any }>;
|
||||||
|
sort?: string[];
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
customOperators?: any[];
|
||||||
|
computedColumns?: any[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormerResolveSpecAPI(options: {
|
||||||
|
url: string;
|
||||||
|
authToken: string;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
fetchOptions?: Partial<RequestInit>;
|
||||||
|
}): FormerAPICallType {
|
||||||
|
return async (mode, request, value, key) => {
|
||||||
|
const baseUrl = options.url.replace(/\/$/, '');
|
||||||
|
|
||||||
|
// Build URL: /[schema]/[table_or_entity]/[id]
|
||||||
|
let url = `${baseUrl}`;
|
||||||
|
if (request !== 'insert' && key) {
|
||||||
|
url = `${url}/${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build ResolveSpec request body
|
||||||
|
const resolveSpecRequest: ResolveSpecRequest = {
|
||||||
|
operation:
|
||||||
|
mode === 'read'
|
||||||
|
? 'read'
|
||||||
|
: request === 'delete'
|
||||||
|
? 'delete'
|
||||||
|
: request === 'update'
|
||||||
|
? 'update'
|
||||||
|
: 'create',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mode === 'mutate') {
|
||||||
|
resolveSpecRequest.data = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
cache: 'no-cache',
|
||||||
|
signal: options.signal,
|
||||||
|
...options.fetchOptions,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${options.authToken}`,
|
||||||
|
...options.fetchOptions?.headers,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(resolveSpecRequest),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, fetchOptions);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed with status ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data as any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FormerResolveSpecAPI, type ResolveSpecRequest };
|
||||||
50
src/Former/FormerRestHeadSpecAPI.ts
Normal file
50
src/Former/FormerRestHeadSpecAPI.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { FormerAPICallType } from './Former.types';
|
||||||
|
|
||||||
|
function FormerRestHeadSpecAPI(options: {
|
||||||
|
url: string;
|
||||||
|
authToken: string;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
fetchOptions?: Partial<RequestInit>;
|
||||||
|
}): FormerAPICallType {
|
||||||
|
return async (mode, request, value, key) => {
|
||||||
|
const baseUrl = options.url ?? ''; // Remove trailing slashes
|
||||||
|
let url = baseUrl;
|
||||||
|
let fetchOptions: RequestInit = {
|
||||||
|
cache: 'no-cache',
|
||||||
|
signal: options.signal,
|
||||||
|
...options.fetchOptions,
|
||||||
|
method:
|
||||||
|
mode === 'read'
|
||||||
|
? 'GET'
|
||||||
|
: request === 'delete'
|
||||||
|
? 'DELETE'
|
||||||
|
: request === 'update'
|
||||||
|
? 'PUT'
|
||||||
|
: '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') {
|
||||||
|
url = `${baseUrl}/${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, fetchOptions);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed with status ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'read') {
|
||||||
|
const data = await response.json();
|
||||||
|
return data as any;
|
||||||
|
} else {
|
||||||
|
return value as any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FormerRestHeadSpecAPI };
|
||||||
114
src/Former/FormerWrappers.tsx
Normal file
114
src/Former/FormerWrappers.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
Modal,
|
||||||
|
Popover,
|
||||||
|
type DrawerProps,
|
||||||
|
type ModalProps,
|
||||||
|
type PopoverProps,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import type { FormerProps } from './Former.types';
|
||||||
|
import { Former } from './Former';
|
||||||
|
|
||||||
|
export const FormerDialog = (props: DrawerProps & { former: FormerProps }) => {
|
||||||
|
const { former, children, opened, onClose, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Former
|
||||||
|
{...former}
|
||||||
|
opened={opened}
|
||||||
|
onClose={onClose}
|
||||||
|
wrapper={(children, opened, onClose, _onOpen, getState) => {
|
||||||
|
const values = getState('values');
|
||||||
|
const request = getState('request');
|
||||||
|
const uniqueKeyField = getState('uniqueKeyField') ?? 'id';
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
h={'100%'}
|
||||||
|
title={
|
||||||
|
request === 'delete'
|
||||||
|
? `Delete Record - ${values?.[uniqueKeyField]}`
|
||||||
|
: request === 'insert'
|
||||||
|
? 'New Record'
|
||||||
|
: `Edit Record - ${values?.[uniqueKeyField]}`
|
||||||
|
}
|
||||||
|
closeOnClickOutside={false}
|
||||||
|
{...rest}
|
||||||
|
onClose={() => onClose?.()}
|
||||||
|
opened={opened ?? false}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Former>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormerModel = (props: ModalProps & { former: FormerProps }) => {
|
||||||
|
const { former, children, opened, onClose, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Former
|
||||||
|
{...former}
|
||||||
|
opened={opened}
|
||||||
|
onClose={onClose}
|
||||||
|
wrapper={(children, opened, onClose, _onOpen, getState) => {
|
||||||
|
const values = getState('values');
|
||||||
|
const request = getState('request');
|
||||||
|
const uniqueKeyField = getState('uniqueKeyField') ?? 'id';
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
h={'100%'}
|
||||||
|
title={
|
||||||
|
request === 'delete'
|
||||||
|
? `Delete Record - ${values?.[uniqueKeyField]}`
|
||||||
|
: request === 'insert'
|
||||||
|
? 'New Record'
|
||||||
|
: `Edit Record - ${values?.[uniqueKeyField]}`
|
||||||
|
}
|
||||||
|
closeOnClickOutside={false}
|
||||||
|
{...rest}
|
||||||
|
onClose={() => onClose?.()}
|
||||||
|
opened={opened ?? false}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Former>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormerPopover = (
|
||||||
|
props: PopoverProps & { former: FormerProps; target: React.ReactNode }
|
||||||
|
) => {
|
||||||
|
const { former, children, opened, onClose, target, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Former
|
||||||
|
{...former}
|
||||||
|
opened={opened}
|
||||||
|
onClose={onClose}
|
||||||
|
wrapper={(children, opened, onClose, _onOpen, _getState) => {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
withArrow
|
||||||
|
closeOnClickOutside={false}
|
||||||
|
width={250}
|
||||||
|
trapFocus
|
||||||
|
middlewares={{ inline: true }}
|
||||||
|
{...rest}
|
||||||
|
onClose={() => onClose?.()}
|
||||||
|
opened={opened ?? false}
|
||||||
|
>
|
||||||
|
<Popover.Target>{target}</Popover.Target>
|
||||||
|
<Popover.Dropdown>{children}</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Former>
|
||||||
|
);
|
||||||
|
};
|
||||||
40
src/Former/stories/apiFormData.tsx
Normal file
40
src/Former/stories/apiFormData.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { TextInput } from '@mantine/core';
|
||||||
|
import { Former } from '../Former';
|
||||||
|
import { useUncontrolled } from '@mantine/hooks';
|
||||||
|
|
||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
|
export const ApiFormData = (props: {
|
||||||
|
values?: Record<string, unknown>;
|
||||||
|
onChange?: (values: Record<string, unknown>) => void;
|
||||||
|
primeData?: Record<string, unknown>;
|
||||||
|
}) => {
|
||||||
|
const [values, setValues] = useUncontrolled<Record<string, unknown>>({
|
||||||
|
value: props.values,
|
||||||
|
defaultValue: { url: '', authToken: '', ...props.primeData },
|
||||||
|
finalValue: { url: '', authToken: '', ...props.primeData },
|
||||||
|
onChange: props.onChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Former
|
||||||
|
request="update"
|
||||||
|
uniqueKeyField="id"
|
||||||
|
disableHTMlForm
|
||||||
|
primeData={props.primeData}
|
||||||
|
values={values}
|
||||||
|
onChange={setValues}
|
||||||
|
layout={{ saveButtonTitle: 'Save URL Parameters' }}
|
||||||
|
id="api-form-data"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="url"
|
||||||
|
render={({ field }) => <TextInput type="url" label="URL" {...field} />}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="authToken"
|
||||||
|
render={({ field }) => <TextInput type="password" label="Auth Token" {...field} />}
|
||||||
|
/>
|
||||||
|
</Former>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,14 +2,38 @@ import { Button, Drawer, Group, Paper, Select, Stack, Switch } from '@mantine/co
|
|||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
import type { 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 { FormerDialog, FormerModel } from '../FormerWrappers';
|
||||||
|
|
||||||
|
const StubAPI = (): FormerAPICallType => (mode, request, value) => {
|
||||||
|
console.log('API Call', mode, request, value);
|
||||||
|
if (mode === 'read') {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ a: 'Another Value', test: 'Loaded Value' });
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(value || {});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const FormTest = () => {
|
export const FormTest = () => {
|
||||||
const [request, setRequest] = useState<null | string>('insert');
|
const [request, setRequest] = useState<null | string>('insert');
|
||||||
const [wrapped, setWrapped] = useState(false);
|
const [wrapped, setWrapped] = useState(false);
|
||||||
const [disableHTML, setDisableHTML] = useState(false);
|
const [disableHTML, setDisableHTML] = useState(false);
|
||||||
|
const [apiOptions, setApiOptions] = useState({
|
||||||
|
url: '',
|
||||||
|
authToken: '',
|
||||||
|
type: '',
|
||||||
|
});
|
||||||
const [layout, setLayout] = useState({
|
const [layout, setLayout] = useState({
|
||||||
buttonOnTop: false,
|
buttonOnTop: false,
|
||||||
title: 'Custom Former Title',
|
title: 'Custom Former Title',
|
||||||
@@ -17,8 +41,8 @@ export const FormTest = () => {
|
|||||||
} as FormerProps['layout']);
|
} as FormerProps['layout']);
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [formData, setFormData] = useState({ a: 99 });
|
const [formData, setFormData] = useState({ a: 99, rid_usernote: 3047 });
|
||||||
console.log('formData', formData);
|
//console.log('formData render', formData);
|
||||||
|
|
||||||
const ref = useRef<FormerRef>(null);
|
const ref = useRef<FormerRef>(null);
|
||||||
return (
|
return (
|
||||||
@@ -44,6 +68,13 @@ export const FormTest = () => {
|
|||||||
label="Button On Top"
|
label="Button On Top"
|
||||||
onChange={(event) => setLayout({ ...layout, buttonOnTop: event.currentTarget.checked })}
|
onChange={(event) => setLayout({ ...layout, buttonOnTop: event.currentTarget.checked })}
|
||||||
/>
|
/>
|
||||||
|
<Switch
|
||||||
|
checked={apiOptions.type === 'api'}
|
||||||
|
label="Use API"
|
||||||
|
onChange={(event) =>
|
||||||
|
setApiOptions({ ...apiOptions, type: event.currentTarget.checked ? 'api' : '' })
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Button onClick={() => setOpen(true)}>Open Former Drawer</Button>
|
<Button onClick={() => setOpen(true)}>Open Former Drawer</Button>
|
||||||
<Group>
|
<Group>
|
||||||
@@ -65,25 +96,21 @@ export const FormTest = () => {
|
|||||||
Test Show/Hide
|
Test Show/Hide
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
<FormerModel opened={open} onClose={() => setOpen(false)} former={{ request: 'insert' }}>
|
||||||
|
<div>Test</div>
|
||||||
|
</FormerModel>
|
||||||
<Former
|
<Former
|
||||||
//wrapper={(children, getState) => <div>{children}</div>}
|
//wrapper={(children, getState) => <div>{children}</div>}
|
||||||
//opened={true}
|
//opened={true}
|
||||||
apiKeyField="a"
|
uniqueKeyField="rid_usernote"
|
||||||
onAPICall={(mode, request, value) => {
|
onAPICall={
|
||||||
console.log('API Call', mode, request, value);
|
apiOptions.type === 'api'
|
||||||
if (mode === 'read') {
|
? FormerRestHeadSpecAPI({
|
||||||
return new Promise((resolve) => {
|
authToken: apiOptions.authToken,
|
||||||
setTimeout(() => {
|
url: apiOptions.url,
|
||||||
resolve({ a: 'Another Value', test: 'Loaded Value' });
|
})
|
||||||
}, 1000);
|
: StubAPI()
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve(value || {});
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
disableHTMlForm={disableHTML}
|
disableHTMlForm={disableHTML}
|
||||||
onChange={setFormData}
|
onChange={setFormData}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
@@ -94,29 +121,28 @@ export const FormTest = () => {
|
|||||||
useFormProps={{ criteriaMode: 'all', shouldUseNativeValidation: false }}
|
useFormProps={{ criteriaMode: 'all', shouldUseNativeValidation: false }}
|
||||||
values={formData}
|
values={formData}
|
||||||
layout={layout}
|
layout={layout}
|
||||||
wrapper={
|
// wrapper={
|
||||||
wrapped
|
// wrapped
|
||||||
? (children, opened, onClose, _onOpen, getState) => {
|
// ? (children, opened, onClose, _onOpen, getState) => {
|
||||||
const values = getState('values');
|
// const values = getState('values');
|
||||||
return (
|
// return (
|
||||||
<Drawer
|
// <Drawer
|
||||||
h={'100%'}
|
// h={'100%'}
|
||||||
onClose={() => onClose?.()}
|
// onClose={() => onClose?.()}
|
||||||
opened={opened ?? false}
|
// opened={opened ?? false}
|
||||||
title={`Drawer Former - Current A Value: ${values?.a}`}
|
// title={`Drawer Former - Current A Value: ${values?.a}`}
|
||||||
w={'50%'}
|
// w={'50%'}
|
||||||
|
// >
|
||||||
|
// <Paper h="100%" shadow="sm" w="100%" withBorder>
|
||||||
|
// {children}
|
||||||
|
// </Paper>
|
||||||
|
// </Drawer>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// : undefined
|
||||||
|
// }
|
||||||
>
|
>
|
||||||
<Paper h="100%" shadow="sm" w="100%" withBorder>
|
<Stack pb={'400px'}>
|
||||||
{children}
|
|
||||||
<Button>Test Save</Button>
|
|
||||||
</Paper>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Stack h="1200px">
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<Controller
|
<Controller
|
||||||
name="test"
|
name="test"
|
||||||
@@ -127,6 +153,11 @@ export const FormTest = () => {
|
|||||||
render={({ field }) => <input type="text" {...field} placeholder="B" />}
|
render={({ field }) => <input type="text" {...field} placeholder="B" />}
|
||||||
rules={{ required: 'Field is required' }}
|
rules={{ required: 'Field is required' }}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
name="note"
|
||||||
|
render={({ field }) => <input type="text" {...field} placeholder="note" />}
|
||||||
|
rules={{ required: 'Field is required' }}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
{!disableHTML && (
|
{!disableHTML && (
|
||||||
<Stack>
|
<Stack>
|
||||||
@@ -136,6 +167,14 @@ export const FormTest = () => {
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Former>
|
</Former>
|
||||||
|
{apiOptions.type === 'api' && (
|
||||||
|
<ApiFormData
|
||||||
|
values={apiOptions}
|
||||||
|
onChange={(values) => {
|
||||||
|
setApiOptions({ ...apiOptions, ...values });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- [x] Wrapper must receive button areas etc. Better scroll areas.
|
- [x] Wrapper must receive button areas etc. Better scroll areas.
|
||||||
- [ ] Predefined wrappers (Model,Dialog,notification,popover)
|
- [x] Predefined wrappers (Model,Dialog,notification,popover)
|
||||||
- [ ] Headerspec API
|
- [x] Headerspec API
|
||||||
- [ ] Relspec API
|
- [x] Relspec API
|
||||||
- [ ] SocketSpec API
|
- [ ] SocketSpec API
|
||||||
- [x] Layout Tool
|
- [x] Layout Tool
|
||||||
- [x] Header Section
|
- [x] Header Section
|
||||||
|
|||||||
Reference in New Issue
Block a user