@pagamio/frontend-commons-lib 0.8.275 → 0.8.277

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
  /**
@@ -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,10 @@ 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>;
27
+ /** Ref updated every render with handleCancel — call from outside (e.g. X button) to show the same unsaved-changes modal. */
28
+ cancelRef?: React.MutableRefObject<() => void>;
25
29
  }
26
30
  declare const DrawerContent: React.FC<DrawerContentProps>;
27
31
  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, cancelRef, }) => {
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,13 @@ const DrawerContent = ({ fields, onSubmit, isOpen, initialValues, submitButtonTe
134
144
  const handleContinueEditing = () => {
135
145
  setShowCancelConfirmModal(false);
136
146
  };
147
+ // Keep isDirtyRef and cancelRef in sync every render.
148
+ if (isDirtyRef) {
149
+ isDirtyRef.current = checkUnsavedChanges();
150
+ }
151
+ if (cancelRef) {
152
+ cancelRef.current = handleCancel;
153
+ }
137
154
  // Helper function to process dependency validations
138
155
  const processDependencyValidation = (field) => {
139
156
  if (!field.validation?.dependency) {
@@ -7,6 +7,9 @@ const FormEngineDrawer = ({ title, cancelButtonText = 'Cancel', submitButtonText
7
7
  const initializedRef = useRef(false);
8
8
  const processingDependencyRef = useRef(false);
9
9
  const pendingUpdatesRef = useRef(new Map());
10
+ // cancelRef is updated every render by DrawerContent with its handleCancel function.
11
+ // Calling cancelRef.current() from the X button shows the same modal as the Cancel button.
12
+ const cancelRef = useRef(() => { });
10
13
  // Update fields when initialFields changes while preserving values
11
14
  useEffect(() => {
12
15
  setFields((prevFields) => {
@@ -130,6 +133,6 @@ const FormEngineDrawer = ({ title, cancelButtonText = 'Cancel', submitButtonText
130
133
  processingDependencyRef.current = false;
131
134
  }
132
135
  };
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') }));
136
+ return (_jsx(BaseDrawer, { title: title, isOpen: isOpen, marginTop: marginTop, onClose: onClose, onCloseAttempt: () => cancelRef.current(), 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, cancelRef: cancelRef, children: children }, persistenceKey || 'drawer-content') }));
134
137
  };
135
138
  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.275",
4
+ "version": "0.8.277",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "provenance": false