@pagamio/frontend-commons-lib 0.8.274 → 0.8.276

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,7 +40,7 @@ import { useSidebar } from './SidebarV2Context';
40
40
  const SidebarV2ToggleButton = React.memo(({ isOpen, onToggle, className, iconClassName }) => {
41
41
  const { tLib } = useLibTranslations();
42
42
  return (_jsxs(Button, { type: "button", variant: "ghost", onClick: onToggle, className: className ??
43
- 'mr-3 cursor-pointer rounded p-2 text-foreground/70 hover:bg-accent hover:text-foreground', "aria-label": tLib('sidebar.toggle', 'Toggle sidebar'), children: [_jsx("span", { className: "sr-only", children: tLib('sidebar.toggle', 'Toggle sidebar') }), isOpen ? (_jsx(IconX, { className: iconClassName ?? 'h-6 w-6' })) : (_jsx(IconMenu2, { className: iconClassName ?? 'h-6 w-6' }))] }));
43
+ 'mr-3 cursor-pointer rounded p-2 text-foreground hover:bg-accent hover:text-foreground', "aria-label": tLib('sidebar.toggle', 'Toggle sidebar'), children: [_jsx("span", { className: "sr-only", children: tLib('sidebar.toggle', 'Toggle sidebar') }), isOpen ? (_jsx(IconX, { className: iconClassName ?? 'h-6 w-6' })) : (_jsx(IconMenu2, { className: iconClassName ?? 'h-6 w-6' }))] }));
44
44
  });
45
45
  SidebarV2ToggleButton.displayName = 'SidebarV2ToggleButton';
46
46
  /**
@@ -140,7 +140,7 @@ SidebarSeparator.displayName = 'SidebarSeparator';
140
140
  const SidebarTrigger = forwardRef(({ className, showOnMobile = true, ...props }, ref) => {
141
141
  const { open, toggleSidebar, isMobile, openMobile } = useSidebarV2();
142
142
  const isOpen = isMobile ? openMobile : open;
143
- return (_jsx(Button, { ref: ref, type: "button", size: "icon", variant: "ghost", onClick: toggleSidebar, "aria-label": isOpen ? 'Close sidebar' : 'Open sidebar', className: cn('inline-flex items-center justify-center rounded-md p-2', 'text-muted-foreground hover:bg-accent hover:text-accent-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', 'transition-colors', !showOnMobile && 'hidden md:inline-flex', className), ...props, children: isOpen ? (isMobile ? (_jsx(IconX, { className: "h-5 w-5" })) : (_jsx(IconLayoutSidebarLeftCollapse, { className: "h-5 w-5" }))) : (_jsx(IconLayoutSidebarLeftExpand, { className: "h-5 w-5" })) }));
143
+ return (_jsx(Button, { ref: ref, type: "button", size: "icon", variant: "ghost", onClick: toggleSidebar, "aria-label": isOpen ? 'Close sidebar' : 'Open sidebar', className: cn('inline-flex items-center justify-center rounded-md p-2', 'text-accent-foreground hover:bg-accent hover:text-accent-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', 'transition-colors', !showOnMobile && 'hidden md:inline-flex', className), ...props, children: isOpen ? (isMobile ? (_jsx(IconX, { className: "h-5 w-5" })) : (_jsx(IconLayoutSidebarLeftCollapse, { className: "h-5 w-5" }))) : (_jsx(IconLayoutSidebarLeftExpand, { className: "h-5 w-5" })) }));
144
144
  });
145
145
  SidebarTrigger.displayName = 'SidebarTrigger';
146
146
  /**
@@ -12,9 +12,18 @@ const MultiFormEngineDrawerProviderContext = createContext(undefined);
12
12
  export const MultiFormEngineDrawerProvider = ({ pathname, children }) => {
13
13
  const [drawerStates, setDrawerStates] = useState(new Map());
14
14
  const formDataRef = useRef(new Map());
15
+ const prevPathnameRef = useRef(pathname);
15
16
  useEffect(() => {
16
17
  setupInputRegistry();
17
18
  }, []);
19
+ // Clear drawers synchronously during render when pathname changes
20
+ // (useEffect fires too late, causing the new page to render with compressed layout)
21
+ if (prevPathnameRef.current !== pathname) {
22
+ prevPathnameRef.current = pathname;
23
+ if (drawerStates.size > 0) {
24
+ setDrawerStates(new Map());
25
+ }
26
+ }
18
27
  const handleOpenDrawer = useCallback((key) => {
19
28
  setDrawerStates((prev) => new Map(prev).set(key, true));
20
29
  }, []);
@@ -51,9 +60,6 @@ export const MultiFormEngineDrawerProvider = ({ pathname, children }) => {
51
60
  const hasFormData = useCallback((key) => {
52
61
  return formDataRef.current.has(key);
53
62
  }, []);
54
- useEffect(() => {
55
- setDrawerStates(new Map());
56
- }, [pathname]);
57
63
  const contextValue = useMemo(() => ({
58
64
  isOpen,
59
65
  isAnyDrawerOpen,
@@ -16,6 +16,8 @@ interface DateFormatOptions {
16
16
  locale?: string;
17
17
  fallback?: string;
18
18
  includeTime?: boolean;
19
+ /** When true, includes seconds in the time portion. Defaults to false. */
20
+ includeSeconds?: boolean;
19
21
  }
20
22
  interface DateRangeFormatOptions extends DateFormatOptions {
21
23
  separator?: string;
@@ -211,7 +211,7 @@ export const formatValue = (value, format, options) => {
211
211
  * @returns Formatted date string or fallback value
212
212
  */
213
213
  export const formatDate = (date, options) => {
214
- const { locale = 'en-US', fallback = 'N/A', includeTime = false } = options || {};
214
+ const { locale = 'en-US', fallback = 'N/A', includeTime = false, includeSeconds = false } = options || {};
215
215
  if (!date)
216
216
  return fallback;
217
217
  try {
@@ -227,7 +227,7 @@ export const formatDate = (date, options) => {
227
227
  day: '2-digit',
228
228
  hour: '2-digit',
229
229
  minute: '2-digit',
230
- second: '2-digit',
230
+ ...(includeSeconds && { second: '2-digit' }),
231
231
  });
232
232
  }
233
233
  return dateObj.toLocaleDateString(locale, {
@@ -4,6 +4,8 @@ interface BaseDrawerProps {
4
4
  isOpen: boolean;
5
5
  marginTop?: string;
6
6
  onClose: () => void;
7
+ /** When provided, the X button calls this instead of onClose, allowing dirty-check interception. */
8
+ onCloseAttempt?: () => void;
7
9
  children: React.ReactNode;
8
10
  }
9
11
  declare const BaseDrawer: React.FC<BaseDrawerProps>;
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Drawer } from 'flowbite-react';
3
3
  import Button from '../../components/ui/Button';
4
- const BaseDrawer = ({ title, isOpen, marginTop = '0px', onClose, children }) => {
4
+ const BaseDrawer = ({ title, isOpen, marginTop = '0px', onClose, onCloseAttempt, children, }) => {
5
5
  return (_jsxs(Drawer, { open: isOpen, onClose: onClose, backdrop: false, position: "right", className: "flex flex-col px-0 bg-card border-l-2 border-border w-full md:w-[420px]", style: {
6
6
  marginTop,
7
7
  height: `calc(100vh - ${marginTop})`,
8
8
  paddingTop: '0px',
9
- }, children: [_jsxs("div", { className: "sticky top-0 flex items-center h-12 border-b border-border", style: { zIndex: 100 }, children: [_jsx("div", { className: "flex items-center justify-start pl-6 h-full bg-muted py-5", style: { width: '88%' }, children: _jsx("h2", { className: "text-lg font-semibold text-foreground truncate", children: title }) }), _jsx(Button, { type: "button", variant: "ghost", className: "hover:text-foreground/70 hover:bg-accent bg-card", onClick: onClose, style: { width: '12%', height: '3rem' }, children: "x" })] }), children] }));
9
+ }, children: [_jsxs("div", { className: "sticky top-0 flex items-center h-12 border-b border-border", style: { zIndex: 100 }, children: [_jsx("div", { className: "flex items-center justify-start pl-6 h-full bg-muted py-5", style: { width: '88%' }, children: _jsx("h2", { className: "text-lg font-semibold text-foreground truncate", children: title }) }), _jsx(Button, { type: "button", variant: "ghost", className: "hover:text-foreground/70 hover:bg-accent bg-card", onClick: onCloseAttempt ?? onClose, style: { width: '12%', height: '3rem' }, children: "x" })] }), children] }));
10
10
  };
11
11
  export default BaseDrawer;
@@ -22,6 +22,8 @@ interface DrawerContentProps {
22
22
  processingDependencyRef?: React.MutableRefObject<boolean>;
23
23
  pendingUpdatesRef?: React.MutableRefObject<Map<string, any>>;
24
24
  persistenceKey?: string;
25
+ /** Ref updated every render with current dirty state — read by FormEngineDrawer for X-button check. */
26
+ isDirtyRef?: React.MutableRefObject<boolean>;
25
27
  }
26
28
  declare const DrawerContent: React.FC<DrawerContentProps>;
27
29
  export default DrawerContent;
@@ -5,7 +5,7 @@ import { Button, NotificationModal } from '../../components';
5
5
  import { useToast } from '../../context';
6
6
  import { FieldWrapper } from '../../form-engine';
7
7
  import { useFormPersistence } from '../../form-engine/hooks/useFormPersistence';
8
- const DrawerContent = ({ fields, onSubmit, isOpen, initialValues, submitButtonText = 'Submit', cancelButtonText = 'Cancel', showForm = true, showDrawerButtons = true, handleCloseDrawer, onFieldUpdate, onFieldChange, children, updateFieldOptions, updateFieldLabel, setFieldHidden, addField, updateFieldValidation, processInitialFieldUpdates, processingDependencyRef, pendingUpdatesRef, persistenceKey, }) => {
8
+ const DrawerContent = ({ fields, onSubmit, isOpen, initialValues, submitButtonText = 'Submit', cancelButtonText = 'Cancel', showForm = true, showDrawerButtons = true, handleCloseDrawer, onFieldUpdate, onFieldChange, children, updateFieldOptions, updateFieldLabel, setFieldHidden, addField, updateFieldValidation, processInitialFieldUpdates, processingDependencyRef, pendingUpdatesRef, persistenceKey, isDirtyRef, }) => {
9
9
  const { saveFormData, restoreFormData, clearPersistedData, hasPersistedData } = useFormPersistence(persistenceKey);
10
10
  const { addToast } = useToast();
11
11
  // Determine initial values: initialValues take precedence over persisted data
@@ -29,13 +29,23 @@ const DrawerContent = ({ fields, onSubmit, isOpen, initialValues, submitButtonTe
29
29
  const [showCancelConfirmModal, setShowCancelConfirmModal] = useState(false);
30
30
  const fieldChangeRef = useRef(new Set());
31
31
  const initialLoadPerformedRef = useRef(false);
32
+ // Capture initial values at the moment the drawer opens — used as baseline for dirty detection.
33
+ // We intentionally do NOT use the live `initialValues` prop, because some consumers update it
34
+ // on every field change (e.g. trackedFormData pattern) which would make dirty detection always return false.
35
+ const mountInitialValuesRef = useRef(initialValues || {});
36
+ const wasOpenRef = useRef(!!isOpen);
37
+ if (isOpen && !wasOpenRef.current) {
38
+ // Drawer just transitioned from closed → open; snapshot current initialValues as the baseline.
39
+ mountInitialValuesRef.current = initialValues || {};
40
+ }
41
+ wasOpenRef.current = !!isOpen;
32
42
  const password = watch('password');
33
43
  const allFields = watch();
34
44
  // Function to check if there are unsaved changes
35
45
  const checkUnsavedChanges = () => {
36
46
  if (!allFields || Object.keys(allFields).length === 0)
37
47
  return false;
38
- const originalInitialValues = initialValues || {};
48
+ const originalInitialValues = mountInitialValuesRef.current;
39
49
  return Object.keys(allFields).some((key) => {
40
50
  const currentValue = allFields[key];
41
51
  const originalValue = originalInitialValues[key];
@@ -134,6 +144,10 @@ const DrawerContent = ({ fields, onSubmit, isOpen, initialValues, submitButtonTe
134
144
  const handleContinueEditing = () => {
135
145
  setShowCancelConfirmModal(false);
136
146
  };
147
+ // Keep isDirtyRef in sync so FormEngineDrawer can check before X-button close.
148
+ if (isDirtyRef) {
149
+ isDirtyRef.current = checkUnsavedChanges();
150
+ }
137
151
  // Helper function to process dependency validations
138
152
  const processDependencyValidation = (field) => {
139
153
  if (!field.validation?.dependency) {
@@ -1,12 +1,16 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ import { Button, NotificationModal } from '../components';
3
4
  import BaseDrawer from './components/BaseDrawer';
4
5
  import DrawerContent from './components/DrawerContent';
5
6
  const FormEngineDrawer = ({ title, cancelButtonText = 'Cancel', submitButtonText = 'Submit', showForm = true, showDrawerButtons = true, isOpen, fields: initialFields, initialValues, children, marginTop = '0px', onClose, onSubmit, onFieldUpdate, onFieldChange, persistenceKey, }) => {
6
7
  const [fields, setFields] = useState(initialFields);
8
+ const [showDiscardConfirm, setShowDiscardConfirm] = useState(false);
7
9
  const initializedRef = useRef(false);
8
10
  const processingDependencyRef = useRef(false);
9
11
  const pendingUpdatesRef = useRef(new Map());
12
+ // Updated by DrawerContent on every render with current dirty state.
13
+ const isDirtyRef = useRef(false);
10
14
  // Update fields when initialFields changes while preserving values
11
15
  useEffect(() => {
12
16
  setFields((prevFields) => {
@@ -130,6 +134,18 @@ const FormEngineDrawer = ({ title, cancelButtonText = 'Cancel', submitButtonText
130
134
  processingDependencyRef.current = false;
131
135
  }
132
136
  };
133
- return (_jsx(BaseDrawer, { title: title, isOpen: isOpen, marginTop: marginTop, onClose: onClose, children: _jsx(DrawerContent, { isOpen: isOpen, fields: fields, onSubmit: onSubmit, handleCloseDrawer: onClose, initialValues: initialValues, showForm: showForm, showDrawerButtons: showDrawerButtons, cancelButtonText: cancelButtonText, submitButtonText: submitButtonText, onFieldUpdate: onFieldUpdate, onFieldChange: onFieldChange, updateFieldOptions: updateFieldOptions, updateFieldLabel: updateFieldLabel, setFieldHidden: setFieldHidden, addField: addField, updateFieldValidation: updateFieldValidation, processInitialFieldUpdates: processInitialFieldUpdates, processingDependencyRef: processingDependencyRef, pendingUpdatesRef: pendingUpdatesRef, persistenceKey: persistenceKey, children: children }, persistenceKey || 'drawer-content') }));
137
+ const handleCloseAttempt = () => {
138
+ if (isDirtyRef.current) {
139
+ setShowDiscardConfirm(true);
140
+ }
141
+ else {
142
+ onClose();
143
+ }
144
+ };
145
+ const handleConfirmDiscard = () => {
146
+ setShowDiscardConfirm(false);
147
+ onClose();
148
+ };
149
+ return (_jsxs(_Fragment, { children: [_jsx(BaseDrawer, { title: title, isOpen: isOpen, marginTop: marginTop, onClose: onClose, onCloseAttempt: handleCloseAttempt, children: _jsx(DrawerContent, { isOpen: isOpen, fields: fields, onSubmit: onSubmit, handleCloseDrawer: onClose, initialValues: initialValues, showForm: showForm, showDrawerButtons: showDrawerButtons, cancelButtonText: cancelButtonText, submitButtonText: submitButtonText, onFieldUpdate: onFieldUpdate, onFieldChange: onFieldChange, updateFieldOptions: updateFieldOptions, updateFieldLabel: updateFieldLabel, setFieldHidden: setFieldHidden, addField: addField, updateFieldValidation: updateFieldValidation, processInitialFieldUpdates: processInitialFieldUpdates, processingDependencyRef: processingDependencyRef, pendingUpdatesRef: pendingUpdatesRef, persistenceKey: persistenceKey, isDirtyRef: isDirtyRef, children: children }, persistenceKey || 'drawer-content') }), _jsx(NotificationModal, { show: showDiscardConfirm, onClose: () => setShowDiscardConfirm(false), title: "Unsaved Changes", message: "You have unsaved changes. Are you sure you want to close?", variant: "warning", showConfirmButton: false, showCancelButton: false, children: _jsxs("div", { className: "flex justify-center gap-3", children: [_jsx(Button, { variant: "primary", onClick: () => setShowDiscardConfirm(false), children: "Keep Editing" }), _jsx(Button, { variant: "outline", onClick: handleConfirmDiscard, children: "Discard Changes" })] }) })] }));
134
150
  };
135
151
  export default FormEngineDrawer;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pagamio/frontend-commons-lib",
3
3
  "description": "Pagamio library for Frontend reusable components like the form engine and table container",
4
- "version": "0.8.274",
4
+ "version": "0.8.276",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "provenance": false