pagamio-frontend-commons-lib
Version:
Pagamio library for Frontend reusable components like the form engine and table container
238 lines (237 loc) • 12.2 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useForm } from 'react-hook-form';
import { useEffect, useRef, useState } from 'react';
import { Button, NotificationModal } from '../../components';
import { FieldWrapper } from '../../form-engine';
import { useFormPersistence } from '../../form-engine/hooks/useFormPersistence';
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, }) => {
const { saveFormData, restoreFormData, clearPersistedData, hasPersistedData } = useFormPersistence(persistenceKey);
// Determine initial values: initialValues take precedence over persisted data
// This ensures that when editing different items, the form shows the correct item's data
const getEffectiveInitialValues = () => {
// If initialValues are provided, use them
if (initialValues && Object.keys(initialValues).length > 0) {
return initialValues;
}
// Otherwise, check for persisted data
if (persistenceKey && hasPersistedData()) {
const persistedData = restoreFormData();
if (persistedData) {
return persistedData;
}
}
return initialValues || {};
};
const { control, handleSubmit, watch, setValue, clearErrors, formState: { errors, isSubmitting }, reset, getValues, } = useForm({ mode: 'onBlur', defaultValues: getEffectiveInitialValues() });
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
const [submitError, setSubmitError] = useState(null);
const [showCancelConfirmModal, setShowCancelConfirmModal] = useState(false);
const fieldChangeRef = useRef(new Set());
const initialLoadPerformedRef = useRef(false);
const password = watch('password');
const allFields = watch();
// Function to check if there are unsaved changes
const checkUnsavedChanges = () => {
if (!allFields || Object.keys(allFields).length === 0)
return false;
const originalInitialValues = initialValues || {};
return Object.keys(allFields).some((key) => {
const currentValue = allFields[key];
const originalValue = originalInitialValues[key];
// Handle different value types
if (currentValue === originalValue)
return false;
if (currentValue === '' && (originalValue === null || originalValue === undefined))
return false;
if ((currentValue === null || currentValue === undefined) && originalValue === '')
return false;
return true;
});
};
// Save form data when fields change (for persistence)
useEffect(() => {
if (persistenceKey && allFields && Object.keys(allFields).length > 0) {
// Only save if there are actual field values (not just empty object)
const hasValues = Object.values(allFields).some((value) => value !== undefined && value !== null && value !== '');
if (hasValues) {
saveFormData(allFields, true);
}
}
}, [allFields, persistenceKey, saveFormData]);
// Process initial field updates once when drawer opens
useEffect(() => {
if (isOpen && !initialLoadPerformedRef.current && processInitialFieldUpdates) {
const initializeFields = async () => {
await processInitialFieldUpdates(getValues);
initialLoadPerformedRef.current = true;
};
initializeFields();
}
// Reset initialization flag when drawer closes
if (!isOpen) {
initialLoadPerformedRef.current = false;
fieldChangeRef.current.clear();
}
}, [isOpen, processInitialFieldUpdates]);
const handleFormSubmit = async (data) => {
setSubmitError(null);
try {
const visibleFieldsData = Object.fromEntries(Object.entries(data).filter(([key]) => !fields.find((field) => field.name === key && field.isHidden)));
await onSubmit(visibleFieldsData);
// Clear persisted data on successful submission
if (persistenceKey) {
clearPersistedData();
}
// Close drawer after successful submission
handleCloseDrawer();
}
catch (error) {
setSubmitError(error instanceof Error ? error.message : 'An error occurred');
}
};
// Reset form when drawer closes
useEffect(() => {
if (!isOpen) {
reset();
// Clear persisted data when drawer closes to prevent stale data on next open
if (persistenceKey) {
clearPersistedData();
}
}
}, [isOpen, reset, persistenceKey, clearPersistedData]);
// Reset form when initialValues change
useEffect(() => {
if (isOpen && initialValues) {
reset(initialValues);
}
}, [initialValues, isOpen, reset]);
const handleCancel = () => {
if (checkUnsavedChanges()) {
setShowCancelConfirmModal(true);
}
else {
// No changes detected, just close the drawer directly
handleCloseDrawer();
}
};
const handleDiscardChanges = () => {
// Clear persisted data when explicitly discarding changes
if (persistenceKey) {
clearPersistedData();
}
setShowCancelConfirmModal(false);
handleCloseDrawer();
};
const handleSaveAndClose = () => {
// Just close the drawer without submitting - the form data is already persisted
setShowCancelConfirmModal(false);
handleCloseDrawer();
};
const handleContinueEditing = () => {
setShowCancelConfirmModal(false);
};
// Helper function to process dependency validations
const processDependencyValidation = (field) => {
if (!field.validation?.dependency) {
return field.validation;
}
const dependencies = Array.isArray(field.validation.dependency)
? field.validation.dependency
: [field.validation.dependency];
let finalValidation = { ...field.validation };
delete finalValidation.dependency;
// Apply each dependency validation if its condition is met
for (const dep of dependencies) {
const dependencyValue = watch(dep.field);
if (dep.condition(dependencyValue)) {
finalValidation = { ...finalValidation, ...dep.validationToApply };
}
}
return {
...finalValidation,
validate: field.name === 'confirmPassword'
? (value) => value === password || 'Passwords do not match'
: finalValidation.validate,
};
};
// Helper function to apply a single update result
const applyUpdateResult = (result) => {
if (!result)
return;
const { field, options, value, label, hideField, fieldInnitialData } = result;
// Apply each possible update type if needed
if (options !== undefined && updateFieldOptions) {
updateFieldOptions(field, options);
}
if (value !== undefined && setValue) {
setValue(field, value);
}
if (label !== undefined && updateFieldLabel) {
updateFieldLabel(field, label);
}
if (hideField !== undefined && setFieldHidden) {
setFieldHidden(field, hideField);
if (hideField && setValue) {
setValue(field, undefined);
}
}
if (fieldInnitialData && addField) {
addField(fieldInnitialData);
}
if (clearErrors) {
clearErrors(field);
}
};
// Helper function to process updates for a single field
const processFieldUpdates = async (fieldName, value) => {
if (!onFieldUpdate?.[fieldName])
return;
const updates = onFieldUpdate[fieldName];
for (const update of updates) {
try {
const result = await update.action(value, false);
applyUpdateResult(result);
}
catch (error) {
console.error(`Error processing update for ${fieldName}:`, error);
}
}
};
// Main function with reduced complexity
const processPendingUpdates = async () => {
// Early return if conditions aren't met
if (!pendingUpdatesRef || !processingDependencyRef || processingDependencyRef.current) {
return;
}
processingDependencyRef.current = true;
try {
const pendingUpdates = pendingUpdatesRef.current;
if (pendingUpdates.size === 0)
return;
// Process all pending updates
const updatePromises = Array.from(pendingUpdates.entries()).map(([fieldName, value]) => processFieldUpdates(fieldName, value));
await Promise.all(updatePromises);
pendingUpdatesRef.current.clear();
}
finally {
processingDependencyRef.current = false;
}
};
// Process pending updates when needed
useEffect(() => {
if (pendingUpdatesRef?.current?.size && pendingUpdatesRef?.current?.size > 0) {
processPendingUpdates().then();
}
}, [pendingUpdateCount]);
return (_jsxs(_Fragment, { children: [showForm && (_jsxs("form", { className: "grid grid-cols-12 gap-2 align-middle mb-6 border-gray-400 px-6 py-5", onSubmit: handleSubmit(handleFormSubmit), children: [fields?.map((field) => (_jsx(FieldWrapper, { field: {
...field,
validation: processDependencyValidation(field),
}, control: control, errors: errors, layout: "vertical", onFieldUpdate: onFieldUpdate, onFieldChange: onFieldChange, updateFieldOptions: updateFieldOptions, updateFieldLabel: updateFieldLabel, setFieldHidden: setFieldHidden, addField: addField, setValue: setValue, clearErrors: clearErrors, getValues: getValues, pendingUpdatesRef: pendingUpdatesRef, notifyPendingUpdate: () => setPendingUpdateCount((prev) => prev + 1), processingDependencyRef: processingDependencyRef, fieldChangeRef: fieldChangeRef }, field.name))), isSubmitting && _jsx("span", { children: "Submitting..." }), submitError && _jsx("p", { className: "text-red-500", children: submitError })] })), children, showDrawerButtons && (_jsxs("div", { className: "flex border-t bg-white", style: {
height: '50px',
position: 'fixed',
bottom: 0,
zIndex: 100,
width: '420px',
}, children: [_jsx(Button, { variant: "outline-primary", className: "w-full flex-1 h-full rounded-none", disabled: isSubmitting, onClick: handleCancel, children: cancelButtonText }), _jsx(Button, { variant: "primary", className: "flex-1 h-full rounded-none", disabled: isSubmitting, onClick: handleSubmit(handleFormSubmit), children: submitButtonText })] })), _jsx(NotificationModal, { show: showCancelConfirmModal, onClose: handleContinueEditing, title: "Unsaved Changes", message: "You have unsaved changes. What would you like to do?", variant: "warning", showConfirmButton: false, showCancelButton: false, size: "lg", children: _jsxs("div", { className: "flex gap-2 flex-wrap", children: [_jsx(Button, { variant: "primary", onClick: handleSaveAndClose, children: "Save & Close" }), _jsx(Button, { variant: "outline", onClick: handleDiscardChanges, children: "Discard Changes" }), _jsx(Button, { variant: "outline", onClick: handleContinueEditing, children: "Continue Editing" })] }) })] }));
};
export default DrawerContent;