Compare commits

..

4 Commits

Author SHA1 Message Date
a748a39d2f RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.40

[skip ci]
2026-02-08 00:12:17 +02:00
8928432fe0 docs(changeset): feat(Former): add keep open functionality and update onClose behavior 2026-02-08 00:12:14 +02:00
6ff395e9be RELEASING: Releasing 1 package(s)
Releases:
  @warkypublic/oranguru@0.0.39

[skip ci]
2026-02-08 00:05:14 +02:00
00e5a70aef docs(changeset): feat(Former): enhance state management with additional callbacks and state retrieval 2026-02-08 00:05:05 +02:00
6 changed files with 108 additions and 38 deletions

View File

@@ -1,5 +1,17 @@
# @warkypublic/zustandsyncstore # @warkypublic/zustandsyncstore
## 0.0.40
### Patch Changes
- 8928432: feat(Former): add keep open functionality and update onClose behavior
## 0.0.39
### Patch Changes
- 00e5a70: feat(Former): enhance state management with additional callbacks and state retrieval
## 0.0.38 ## 0.0.38
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "@warkypublic/oranguru", "name": "@warkypublic/oranguru",
"author": "Warky Devs", "author": "Warky Devs",
"version": "0.0.38", "version": "0.0.40",
"type": "module", "type": "module",
"types": "./dist/lib.d.ts", "types": "./dist/lib.d.ts",
"main": "./dist/lib.cjs.js", "main": "./dist/lib.cjs.js",

View File

@@ -2,13 +2,16 @@ import { newUUID } from '@warkypublic/artemis-kit';
import { createSyncStore } from '@warkypublic/zustandsyncstore'; import { createSyncStore } from '@warkypublic/zustandsyncstore';
import { produce } from 'immer'; import { produce } from 'immer';
import type { FormerProps, FormerState } from './Former.types'; import type { FormerProps, FormerState, FormStateAndProps } from './Former.types';
const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore< const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
FormerState<any> & Partial<FormerProps<any>>, FormerState<any> & Partial<FormerProps<any>>,
FormerProps<any> FormerProps<any>
>( >(
(set, get) => ({ (set, get) => ({
getAllState: () => {
return get() as FormStateAndProps<any>;
},
getState: (key) => { getState: (key) => {
const current = get(); const current = get();
return current?.[key]; return current?.[key];
@@ -26,17 +29,19 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
keyValue keyValue
); );
if (get().afterGet) { if (get().afterGet) {
data = await get().afterGet!({ ...data }); data = await get().afterGet!({ ...data }, get());
} }
set({ loading: false, values: data }); set({ loading: false, values: data });
get().onChange?.(data); get().onChange?.(data, get());
} }
if (reset && get().getFormMethods) { if (reset && get().getFormMethods) {
const formMethods = get().getFormMethods!(); const formMethods = get().getFormMethods!();
formMethods.reset(); formMethods.reset();
} }
} catch (e) { } catch (e) {
set({ error: (e as Error)?.message ?? e, loading: false }); const errorMessage = (e as Error)?.message ?? e;
set({ error: errorMessage, loading: false });
get().onError?.(errorMessage, get());
} }
set({ loading: false }); set({ loading: false });
}, },
@@ -66,7 +71,7 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
let data = formMethods.getValues(); let data = formMethods.getValues();
if (get().beforeSave) { if (get().beforeSave) {
const newData = await get().beforeSave!(data); const newData = await get().beforeSave!(data, get());
data = newData; data = newData;
} }
@@ -76,7 +81,9 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
data = newdata; data = newdata;
}, },
(errors) => { (errors) => {
set({ error: errors.root?.message || 'Validation errors', loading: false }); const errorMessage = errors.root?.message || 'Validation errors';
set({ error: errorMessage, loading: false });
get().onError?.(errorMessage, get());
exit = true; exit = true;
} }
); );
@@ -107,29 +114,49 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
data, data,
keyValue keyValue
); );
const newData = { ...data, ...savedData }; //Merge what we had. In case the API doesn't return all fields, we don't want to lose them
if (get().afterSave) { if (get().afterSave) {
await get().afterSave!(savedData); await get().afterSave!(newData, get());
} }
set({ loading: false, values: savedData });
get().onChange?.(savedData); if (keepOpen) {
formMethods.reset(savedData); //reset with saved data to clear dirty state const keyName = get()?.uniqueKeyField || 'id';
if (!keepOpen) { const clearedData = { ...newData };
get().onClose?.(savedData); delete clearedData[keyName];
set({ loading: false, values: clearedData });
get().onChange?.(clearedData, get());
formMethods.reset(clearedData);
return newData;
} }
return savedData;
set({ loading: false, values: newData });
get().onChange?.(newData, get());
formMethods.reset(newData); //reset with saved data to clear dirty state
get().onClose?.(newData);
return newData;
}
if (keepOpen) {
const keyName = get()?.uniqueKeyField || 'id';
const clearedData = { ...data };
delete clearedData[keyName];
set({ loading: false, values: clearedData });
formMethods.reset(clearedData);
get().onChange?.(clearedData, get());
return data;
} }
set({ loading: false, values: data }); set({ loading: false, values: data });
formMethods.reset(data); //reset with saved data to clear dirty state formMethods.reset(data); //reset with saved data to clear dirty state
get().onChange?.(data); get().onChange?.(data, get());
if (!keepOpen) { get().onClose?.(data);
get().onClose?.(data);
}
return data; return data;
} }
} catch (e) { } catch (e) {
set({ error: (e as Error)?.message ?? e, loading: false }); const errorMessage = (e as Error)?.message ?? e;
set({ error: errorMessage, loading: false });
get().onError?.(errorMessage, get());
} }
return undefined; return undefined;
@@ -181,20 +208,20 @@ const { Provider: FormerProvider, useStore: useFormerStore } = createSyncStore<
return { return {
id: !id ? newUUID() : id, id: !id ? newUUID() : id,
onClose: () => { onClose: (data?: any) => {
const dirty = useStoreApi.getState().dirty; const dirty = useStoreApi.getState().dirty;
const setState = useStoreApi.getState().setState; const setState = useStoreApi.getState().setState;
if (dirty) { if (dirty) {
if (confirm('You have unsaved changes. Are you sure you want to close?')) { if (confirm('You have unsaved changes. Are you sure you want to close?')) {
if (onClose) { if (onClose) {
onClose(); onClose(data);
} else { } else {
setState('opened', false); setState('opened', false);
} }
} }
} else { } else {
if (onClose) { if (onClose) {
onClose(); onClose(data);
} else { } else {
setState('opened', false); setState('opened', false);
} }

View File

@@ -12,6 +12,7 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
ref: any ref: any
) { ) {
const { const {
getAllState,
getState, getState,
onChange, onChange,
onClose, onClose,
@@ -26,6 +27,7 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
values, values,
wrapper, wrapper,
} = useFormerStore((state) => ({ } = useFormerStore((state) => ({
getAllState: state.getAllState,
getState: state.getState, getState: state.getState,
onChange: state.onChange, onChange: state.onChange,
onClose: state.onClose, onClose: state.onClose,
@@ -54,7 +56,7 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
() => ({ () => ({
close: async () => { close: async () => {
//console.log('close called'); //console.log('close called');
onClose?.(); onClose?.(getState('values'));
setState('opened', false); setState('opened', false);
}, },
getValue: () => { getValue: () => {
@@ -67,7 +69,7 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
return await save(); return await save();
}, },
setValue: (value: T) => { setValue: (value: T) => {
onChange?.(value); onChange?.(value, getAllState());
}, },
show: async () => { show: async () => {
//console.log('show called'); //console.log('show called');
@@ -78,7 +80,7 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
return await validate(); return await validate();
}, },
}), }),
[getState, onChange, validate, save, reset, setState, onClose, onOpen] [getState, getAllState, onChange, validate, save, reset, setState, onClose, onOpen]
); );
useEffect(() => { useEffect(() => {
@@ -97,7 +99,19 @@ const FormerInner = forwardRef<FormerRef<any>, Partial<FormerProps<any>> & Props
return ( return (
<FormProvider {...formMethods}> <FormProvider {...formMethods}>
{typeof wrapper === 'function' ? ( {typeof wrapper === 'function' ? (
wrapper(<FormerLayout>{props.children}</FormerLayout>, opened ??false, onClose ?? (() => {setState('opened', false)}), onOpen ?? (() => {setState('opened', true)}), getState) wrapper(
<FormerLayout>{props.children}</FormerLayout>,
opened ?? false,
onClose ??
(() => {
setState('opened', false);
}),
onOpen ??
(() => {
setState('opened', true);
}),
getState
)
) : ( ) : (
<FormerLayout>{props.children || null}</FormerLayout> <FormerLayout>{props.children || null}</FormerLayout>
)} )}

View File

@@ -15,15 +15,15 @@ export type FormerAPICallType<T extends FieldValues = any> = (
) => Promise<T>; ) => Promise<T>;
export interface FormerProps<T extends FieldValues = any> { export interface FormerProps<T extends FieldValues = any> {
afterGet?: (data: T) => Promise<T> | void; afterGet?: (data: T, state: Partial<FormStateAndProps<T>>) => Promise<T> | void;
afterSave?: (data: T) => Promise<void> | void; afterSave?: (data: T, state: Partial<FormStateAndProps<T>>) => Promise<void> | void;
beforeSave?: (data: T) => Promise<T> | T; beforeSave?: (data: T, state: Partial<FormStateAndProps<T>>) => Promise<T> | T;
dirty?: boolean; dirty?: boolean;
disableHTMlForm?: boolean; disableHTMlForm?: boolean;
id?: string; id?: string;
keepOpen?: boolean; keepOpen?: boolean;
layout?: { layout?: {
buttonArea?: "bottom" | "none" | "top"; buttonArea?: 'bottom' | 'none' | 'top';
buttonAreaGroupProps?: GroupProps; buttonAreaGroupProps?: GroupProps;
closeButtonProps?: ButtonProps; closeButtonProps?: ButtonProps;
closeButtonTitle?: React.ReactNode; closeButtonTitle?: React.ReactNode;
@@ -31,14 +31,15 @@ export interface FormerProps<T extends FieldValues = any> {
renderTop?: FormerSectionRender<T>; renderTop?: FormerSectionRender<T>;
saveButtonProps?: ButtonProps; saveButtonProps?: ButtonProps;
saveButtonTitle?: React.ReactNode; saveButtonTitle?: React.ReactNode;
showKeepOpenSwitch?: boolean;
title?: string; title?: string;
}; };
onAPICall?: FormerAPICallType<T>; onAPICall?: FormerAPICallType<T>;
onCancel?: () => void; onCancel?: () => void;
onChange?: (value: T) => void; onChange?: (value: T, state: Partial<FormStateAndProps<T>>) => void;
onClose?: (data?: T) => void; onClose?: (data?: T | undefined) => void;
onConfirmDelete?: (values?: T) => Promise<boolean>; onConfirmDelete?: (values?: T) => Promise<boolean>;
onError?: (error: Error | string, state: Partial<FormStateAndProps<T>>) => void;
onOpen?: (data?: T) => void; onOpen?: (data?: T) => void;
opened?: boolean; opened?: boolean;
primeData?: T; primeData?: T;
@@ -62,15 +63,16 @@ export interface FormerRef<T extends FieldValues = any> {
export type FormerSectionRender<T extends FieldValues = any> = ( export type FormerSectionRender<T extends FieldValues = any> = (
children: React.ReactNode, children: React.ReactNode,
opened: boolean , opened: boolean,
onClose: ((data?: T) => void), onClose: (data?: T) => void,
onOpen: ((data?: T) => void) , onOpen: (data?: T) => void,
getState: FormerState<T>['getState'] getState: FormerState<T>['getState']
) => React.ReactNode; ) => React.ReactNode;
export interface FormerState<T extends FieldValues = any> { export interface FormerState<T extends FieldValues = any> {
deleteConfirmed?: boolean; deleteConfirmed?: boolean;
error?: string; error?: string;
getAllState: () => FormStateAndProps<T>;
getFormMethods?: () => UseFormReturn<any, any>; getFormMethods?: () => UseFormReturn<any, any>;
getState: <K extends keyof FormStateAndProps<T>>(key: K) => FormStateAndProps<T>[K]; getState: <K extends keyof FormStateAndProps<T>>(key: K) => FormStateAndProps<T>[K];
load: (reset?: boolean) => Promise<void>; load: (reset?: boolean) => Promise<void>;

View File

@@ -1,4 +1,4 @@
import { Button, Group, Tooltip } from '@mantine/core'; import { Button, Group, Switch, Tooltip } from '@mantine/core';
import { IconDeviceFloppy, IconX } from '@tabler/icons-react'; import { IconDeviceFloppy, IconX } from '@tabler/icons-react';
import { useFormerStore } from './Former.store'; import { useFormerStore } from './Former.store';
@@ -9,21 +9,29 @@ export const FormerButtonArea = () => {
closeButtonProps, closeButtonProps,
closeButtonTitle, closeButtonTitle,
dirty, dirty,
getState,
keepOpen,
onClose, onClose,
request, request,
save, save,
saveButtonProps, saveButtonProps,
saveButtonTitle, saveButtonTitle,
setState,
showKeepOpenSwitch,
} = useFormerStore((state) => ({ } = useFormerStore((state) => ({
buttonAreaGroupProps: state.layout?.buttonAreaGroupProps, buttonAreaGroupProps: state.layout?.buttonAreaGroupProps,
closeButtonProps: state.layout?.closeButtonProps, closeButtonProps: state.layout?.closeButtonProps,
closeButtonTitle: state.layout?.closeButtonTitle, closeButtonTitle: state.layout?.closeButtonTitle,
dirty: state.dirty, dirty: state.dirty,
getState: state.getState,
keepOpen: state.keepOpen,
onClose: state.onClose, onClose: state.onClose,
request: state.request, request: state.request,
save: state.save, save: state.save,
saveButtonProps: state.layout?.saveButtonProps, saveButtonProps: state.layout?.saveButtonProps,
saveButtonTitle: state.layout?.saveButtonTitle, saveButtonTitle: state.layout?.saveButtonTitle,
setState: state.setState,
showKeepOpenSwitch: state.layout?.showKeepOpenSwitch,
})); }));
const disabledSave = const disabledSave =
@@ -47,12 +55,19 @@ export const FormerButtonArea = () => {
size="sm" size="sm"
{...closeButtonProps} {...closeButtonProps}
onClick={() => { onClick={() => {
onClose(); onClose(getState('values'));
}} }}
> >
{closeButtonTitle || 'Close'} {closeButtonTitle || 'Close'}
</Button> </Button>
)} )}
{showKeepOpenSwitch && (
<Switch
checked={keepOpen}
label="Keep Open"
onChange={(event) => setState('keepOpen', event.currentTarget.checked)}
/>
)}
<Tooltip <Tooltip
label={ label={
disabledSave ? ( disabledSave ? (