import React, { useState, useEffect, useRef, useCallback, memo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '../utils/cn';
import { DialogMode, DialogType, BaseTableData } from '../types';
import { useFormHandling } from '../contexts/FormHandlingContext';
// Separate component for dialog content to prevent unnecessary re-renders
const DialogContent = memo(
({
children,
error,
description,
isLoading
}: {
children: React.ReactNode;
error?: string | Error | null;
description?: string;
isLoading: boolean;
}) => {
const errorMessage = error
? (typeof error === 'string' ? error : error.message || 'An error occurred')
: null;
return (
{description &&
{description}
}
{errorMessage && (
{errorMessage}
)}
{isLoading && (
)}
{children}
);
},
(prevProps, nextProps) => {
return (
prevProps.isLoading === nextProps.isLoading &&
prevProps.error === nextProps.error &&
prevProps.description === nextProps.description
);
}
);
DialogContent.displayName = 'DialogContent';
// Separate component for dialog footer to prevent unnecessary re-renders
const DialogFooter = memo(
({
dialogType,
onCancel,
onSubmit,
isSubmitting,
buttonSize = 'md',
actionsPosition = 'right',
elevatedButtons = true,
isReadOnly = false,
isFormValid = true,
disabled = false
}: {
dialogType: DialogType | null;
onCancel: () => void;
onSubmit: () => void;
isSubmitting: boolean;
buttonSize?: string;
actionsPosition?: string;
elevatedButtons?: boolean;
isReadOnly?: boolean;
isFormValid?: boolean;
disabled?: boolean;
}) => {
// Determine submit button label based on dialog type
const submitLabel = {
create: 'Tạo mới',
edit: 'Lưu thay đổi',
delete: 'Xóa',
view: '',
custom: 'Xác nhận'
}[dialogType ?? 'custom'] || 'Xác nhận';
// Determine loading label based on dialog type
const loadingLabel = {
create: 'Đang tạo...',
edit: 'Đang lưu...',
delete: 'Đang xóa...',
view: '',
custom: 'Đang xử lý...'
}[dialogType ?? "custom"] || 'Đang xử lý...';
const isSubmitButtonDisabled =
isSubmitting ||
disabled ||
(dialogType !== 'delete' && !isFormValid);
return (
{dialogType !== 'view' && !isReadOnly && (
)}
);
}
);
DialogFooter.displayName = 'DialogFooter';
// Types for the form component props
export interface OptimizedFormComponentProps {
data?: any;
onSubmit?: (data: any, type: DialogType | null) => Promise | boolean;
onClose?: () => void;
dialogType: DialogType | null;
loading?: boolean;
error?: string | Error | null;
isReadOnly?: boolean;
ref?: React.Ref;
onFormDirty?: (isDirty: boolean) => void;
skipInitialValidation?: boolean;
dialogRef?: React.RefObject;
}
// Types for the dialog props
export interface OptimizedDialogProps {
open?: boolean;
dialogType: DialogType | null;
dialogTitle?: string;
dialogDescription?: string;
data?: T | null;
formComponent?: React.ComponentType;
onClose?: () => void;
onSubmit?: (data: any, type: DialogType | null) => Promise | boolean;
loading?: boolean;
error?: string | Error | null;
width?: string | number;
closeOnClickOutside?: boolean;
closeOnEsc?: boolean;
actionsPosition?: 'left' | 'right' | 'center' | 'between';
buttonSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
elevatedButtons?: boolean;
reducedMotion?: boolean;
disableSubmitUntilDirty?: boolean;
validateOnMount?: boolean;
skipInitialValidation?: boolean;
}
// Simple dialog implementation without conditional hooks
function SimpleDialogImplementation({
open = false,
dialogType = 'custom',
dialogTitle,
dialogDescription,
data,
formComponent: FormComponent,
onClose,
onSubmit,
loading = false,
error,
width = '500px',
closeOnClickOutside = true,
closeOnEsc = true,
actionsPosition = 'right',
buttonSize = 'md',
elevatedButtons = true,
reducedMotion = false,
validateOnMount = false,
skipInitialValidation = true
}: OptimizedDialogProps) {
// ALL HOOKS ARE CALLED UNCONDITIONALLY AT THE TOP LEVEL
const [isSubmitting, setIsSubmitting] = useState(false);
const [isFormDirty, setIsFormDirty] = useState(false);
const [isFormValid, setIsFormValid] = useState(false);
const [hasInteracted, setHasInteracted] = useState(false);
const [localError, setLocalError] = useState(null);
const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);
const formRef = useRef(null);
const dialogRef = useRef(null);
const dataRef = useRef(data);
const dialogTypeRef = useRef(dialogType);
const onSubmitRef = useRef(onSubmit);
const [formTouched, setFormTouched] = useState(false);
// ALWAYS call useFormHandling - never conditionally
const formContext = useFormHandling();
// ALL useEffect hooks are called unconditionally
useEffect(() => {
dataRef.current = data;
dialogTypeRef.current = dialogType;
}, [data, dialogType]);
useEffect(() => {
onSubmitRef.current = onSubmit;
}, [onSubmit]);
// Reset states effect
useEffect(() => {
if (!open) {
setIsSubmitting(false);
setIsFormDirty(false);
setIsFormValid(false);
setFormTouched(false);
setLocalError(null);
setHasAttemptedSubmit(false);
setHasInteracted(false);
return;
}
if (dialogType === 'delete') {
setIsFormDirty(true);
setIsFormValid(true);
} else {
setIsFormDirty(false);
setIsFormValid(!validateOnMount);
setFormTouched(false);
setLocalError(null);
setHasAttemptedSubmit(false);
setHasInteracted(false);
}
}, [open, dialogType, validateOnMount]);
// Escape key effect
useEffect(() => {
const handleEscapeKey = (e: KeyboardEvent) => {
if (open && closeOnEsc && e.key === 'Escape' && !isSubmitting && !loading) {
onClose?.();
}
};
document.addEventListener('keydown', handleEscapeKey);
return () => document.removeEventListener('keydown', handleEscapeKey);
}, [closeOnEsc, open, isSubmitting, loading, onClose]);
// Form validity tracking effect
useEffect(() => {
if (dialogType === 'delete') {
setIsFormValid(true);
return;
}
if (dialogType === 'create' && !validateOnMount) {
setIsFormValid(true);
return;
}
if (formContext && dialogType && open) {
try {
const initialValidity = formContext.isFormValid(dialogType as DialogMode);
setIsFormValid(initialValidity);
} catch (error) {
console.error('Error checking form validity:', error);
setIsFormValid(true);
}
}
}, [dialogType, formContext, open, validateOnMount]);
// Form change detection effect
useEffect(() => {
if (!open || !dialogRef.current) return;
if (formContext || (formRef.current && (typeof formRef.current.isDirty === 'function'))) {
return;
}
const formElements = dialogRef.current.querySelectorAll('input, select, textarea');
const handleFormChange = () => {
setFormTouched(true);
setIsFormDirty(true);
if (!hasAttemptedSubmit) {
setLocalError(null);
}
};
formElements.forEach(element => {
element.addEventListener('input', handleFormChange);
element.addEventListener('change', handleFormChange);
});
return () => {
formElements.forEach(element => {
element.removeEventListener('input', handleFormChange);
element.removeEventListener('change', handleFormChange);
});
};
}, [open, dialogType, formContext, hasAttemptedSubmit]);
// ALL useCallback hooks are called unconditionally
const handleBackdropClick = useCallback((e: React.MouseEvent) => {
if (e.target === e.currentTarget && closeOnClickOutside && !isSubmitting && !loading) {
onClose?.();
}
}, [closeOnClickOutside, isSubmitting, loading, onClose]);
const handleFormDirty = useCallback((isDirty: boolean) => {
setIsFormDirty(isDirty);
if (isDirty) {
setHasInteracted(true);
setFormTouched(true);
}
if (formContext && dialogTypeRef.current) {
formContext.setFormDirty(isDirty, dialogTypeRef.current as DialogMode);
}
}, [formContext]);
const handleSubmit = useCallback(async () => {
const currentDialogType = dialogTypeRef.current;
const currentData = dataRef.current;
if (!currentDialogType || !onSubmitRef.current || isSubmitting || loading) {
return;
}
setHasAttemptedSubmit(true);
setHasInteracted(true);
setIsSubmitting(true);
setLocalError(null);
try {
if (currentDialogType === 'delete') {
const result = onSubmitRef.current(currentData || {}, currentDialogType);
const finalResult = result instanceof Promise ? await result : result;
setIsSubmitting(false);
// Only close dialog if submission was successful
if (finalResult === true || finalResult === undefined) {
onClose?.();
}
return;
}
if (formContext && currentDialogType) {
try {
const { isValid, data: formData, errors } = await formContext.validateAndGetFormData(currentDialogType as DialogMode);
if (!isValid || !formData) {
const errorMessage = errors && typeof errors === 'object' && '_form' in errors
? String(errors._form)
: 'Form validation failed';
setLocalError(errorMessage);
setIsSubmitting(false);
// CRITICAL FIX: Don't reset form on validation failure
return;
}
if (formData && Object.keys(formData).length === 0 && (currentDialogType as DialogMode) !== 'delete') {
setLocalError('Không có dữ liệu được nhập. Vui lòng điền vào form.');
setIsSubmitting(false);
// CRITICAL FIX: Don't reset form when no data
return;
}
const finalData = currentDialogType === 'edit' && currentData
? { ...currentData, ...formData }
: formData;
if (currentDialogType === 'edit' && currentData?.id && !finalData.id) {
finalData.id = currentData.id;
}
const result = onSubmitRef.current(finalData, currentDialogType);
const finalResult = result instanceof Promise ? await result : result;
setIsSubmitting(false);
// Only close dialog and potentially reset form if submission was successful
if (finalResult === true || finalResult === undefined) {
// Success: Close dialog (form will be reset when dialog closes)
onClose?.();
} else if (finalResult === false) {
// Failure: Keep dialog open, preserve form data, show error
console.log('Submission failed, keeping dialog open for user feedback');
setLocalError('Submission failed. Please check your input and try again.');
}
} catch (err) {
setLocalError(err instanceof Error ? err.message : "An unexpected error occurred");
setIsSubmitting(false);
// CRITICAL FIX: Don't close dialog or reset form on error
console.error('Form submission error, preserving user data:', err);
}
return;
}
// Fallback: use original data
const result = onSubmitRef.current(currentData || {}, currentDialogType);
const finalResult = result instanceof Promise ? await result : result;
setIsSubmitting(false);
// Only close dialog if submission was successful
if (finalResult === true || finalResult === undefined) {
onClose?.();
} else if (finalResult === false) {
// Keep dialog open if submission failed
console.log('Fallback submission failed, keeping dialog open for user feedback');
setLocalError('Submission failed. Please check your input and try again.');
}
} catch (err) {
setLocalError(err instanceof Error ? err.message : "Đã xảy ra lỗi khi gửi biểu mẫu");
setIsSubmitting(false);
// CRITICAL FIX: Don't close dialog on error - preserve user data
console.error('Unexpected error during submission, preserving user data:', err);
}
}, [isSubmitting, loading, onClose, formContext]);
// Don't render if not open
if (!open) {
return null;
}
// Determine dialog title based on type
const title = dialogTitle || {
create: 'Thêm mới',
edit: 'Chỉnh sửa',
view: 'Xem chi tiết',
delete: 'Xác nhận xóa',
custom: 'Thao tác'
}[dialogType ?? 'custom'] || 'Dialog';
// Animation settings based on reduced motion preference
const animationSettings = reducedMotion ? {
initial: {},
animate: {},
exit: {},
transition: { duration: 0.1 }
} : {
initial: { opacity: 0, y: 20, scale: 0.95 },
animate: { opacity: 1, y: 0, scale: 1 },
exit: { opacity: 0, y: 20, scale: 0.95 },
transition: { duration: 0.2 }
};
const isReadOnly = dialogType === 'view';
const displayError = localError || error;
const isDelete = dialogType === 'delete';
return (
e.stopPropagation()}
{...animationSettings}
>
{title}
{dialogDescription && (
{dialogDescription}
)}
{FormComponent && (
)}
);
}
// Safe wrapper that handles AnimatePresence
export function OptimizedDialog(props: OptimizedDialogProps) {
return (
{props.open && }
);
}
export default OptimizedDialog;