UNPKG

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
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;