@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.
- package/lib/components/sidebar-v2/NavbarV2.js +1 -1
- package/lib/components/sidebar-v2/SidebarV2Primitives.js +1 -1
- package/lib/context/MultiFormEngineDrawerProvider.js +9 -3
- package/lib/dashboard-visuals/utils/formatUtil.d.ts +2 -0
- package/lib/dashboard-visuals/utils/formatUtil.js +2 -2
- package/lib/pagamio-drawer/components/BaseDrawer.d.ts +2 -0
- package/lib/pagamio-drawer/components/BaseDrawer.js +2 -2
- package/lib/pagamio-drawer/components/DrawerContent.d.ts +2 -0
- package/lib/pagamio-drawer/components/DrawerContent.js +16 -2
- package/lib/pagamio-drawer/index.js +18 -2
- package/package.json +1 -1
|
@@ -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
|
|
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-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "0.8.276",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
7
|
"provenance": false
|