182 lines
5.7 KiB
TypeScript
182 lines
5.7 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { IoIosTrash, IoIosCheckmark, IoIosClose } from 'react-icons/io';
|
|
import { useLanguage } from '../../../../contexts/LanguageContext';
|
|
import { useFileOperations, useUserFiles } from '../../../../hooks/useFiles';
|
|
import styles from '../ActionButton.module.css';
|
|
|
|
export interface DeleteActionButtonProps<T = any> {
|
|
row: T;
|
|
disabled?: boolean;
|
|
loading?: boolean;
|
|
className?: string;
|
|
title?: string;
|
|
confirmTitle?: string;
|
|
cancelTitle?: string;
|
|
containerRef?: React.RefObject<HTMLDivElement | null>;
|
|
onSuccess?: (row: T) => void;
|
|
onError?: (row: T, error: string) => void;
|
|
hookData?: any; // Contains all hook data including operations and refetch
|
|
// Field mappings
|
|
idField?: string; // Field name for the unique identifier
|
|
operationName?: string; // Name of the delete operation in hookData
|
|
loadingStateName?: string; // Name of the loading state in hookData
|
|
}
|
|
|
|
export function DeleteActionButton<T = any>({
|
|
row,
|
|
disabled = false,
|
|
loading = false,
|
|
className = '',
|
|
title,
|
|
confirmTitle,
|
|
cancelTitle,
|
|
containerRef,
|
|
onSuccess,
|
|
onError,
|
|
hookData,
|
|
idField = 'id',
|
|
operationName = 'handleDelete',
|
|
loadingStateName = 'deletingFiles'
|
|
}: DeleteActionButtonProps<T>) {
|
|
const { t } = useLanguage();
|
|
const [isConfirming, setIsConfirming] = useState(false);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
|
|
// Use hook data if available, otherwise fall back to direct hook calls
|
|
const handleDelete = hookData?.[operationName];
|
|
const removeOptimistically = hookData?.removeFileOptimistically || hookData?.removeOptimistically;
|
|
const refetch = hookData?.refetch;
|
|
const loadingState = hookData?.[loadingStateName];
|
|
|
|
// Fallback to direct hook calls if hookData not provided
|
|
const { handleFileDelete: fallbackHandleDelete } = useFileOperations();
|
|
const { removeFileOptimistically: fallbackRemoveFileOptimistically, refetch: fallbackRefetch } = useUserFiles();
|
|
|
|
const finalHandleDelete = handleDelete || fallbackHandleDelete;
|
|
const finalRemoveOptimistically = removeOptimistically || fallbackRemoveFileOptimistically;
|
|
const finalRefetch = refetch || fallbackRefetch;
|
|
|
|
// Handle clicks outside delete confirmation buttons
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (isConfirming && containerRef?.current) {
|
|
if (!containerRef.current.contains(event.target as Node)) {
|
|
setIsConfirming(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (isConfirming) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}
|
|
}, [isConfirming, containerRef]);
|
|
|
|
const handleDeleteClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
if (!disabled && !loading && !isDeleting) {
|
|
setIsConfirming(true);
|
|
}
|
|
};
|
|
|
|
const handleConfirmDelete = async (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
setIsDeleting(true);
|
|
|
|
try {
|
|
// Get ID from row using configurable field name
|
|
const itemId = (row as any)[idField];
|
|
if (!itemId) {
|
|
throw new Error(`${idField} not found`);
|
|
}
|
|
|
|
// Immediately remove from UI for instant feedback
|
|
if (finalRemoveOptimistically) {
|
|
finalRemoveOptimistically(itemId);
|
|
}
|
|
|
|
// Call the delete API
|
|
const success = await finalHandleDelete(itemId);
|
|
|
|
if (success) {
|
|
// Refetch to ensure UI is properly updated
|
|
if (finalRefetch) {
|
|
await finalRefetch();
|
|
}
|
|
onSuccess?.(row);
|
|
} else {
|
|
// Refetch to restore the file in case of failure
|
|
if (finalRefetch) {
|
|
await finalRefetch();
|
|
}
|
|
onError?.(row, 'Delete failed');
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Delete failed:', error);
|
|
onError?.(row, error.message || 'Delete failed');
|
|
// Refetch to restore the file in case of failure
|
|
if (finalRefetch) {
|
|
await finalRefetch();
|
|
}
|
|
} finally {
|
|
setIsDeleting(false);
|
|
setIsConfirming(false);
|
|
}
|
|
};
|
|
|
|
const handleCancelDelete = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
setIsConfirming(false);
|
|
};
|
|
|
|
const buttonTitle = title || t('files.action.delete', 'Delete');
|
|
const confirmButtonTitle = confirmTitle || t('formgen.delete.confirm', 'Confirm delete');
|
|
const cancelButtonTitle = cancelTitle || t('formgen.delete.cancel', 'Cancel delete');
|
|
|
|
// Use loading state from hookData if available
|
|
const isDeletingFromHook = loadingState?.has((row as any)[idField]) || false;
|
|
|
|
if (isConfirming) {
|
|
return (
|
|
<div className={styles.deleteConfirmButtons}>
|
|
<button
|
|
onClick={handleConfirmDelete}
|
|
className={`${styles.actionButton} ${styles.confirmButton}`}
|
|
title={confirmButtonTitle}
|
|
disabled={isDeleting}
|
|
>
|
|
<span className={styles.actionIcon}>
|
|
<IoIosCheckmark />
|
|
</span>
|
|
</button>
|
|
<button
|
|
onClick={handleCancelDelete}
|
|
className={`${styles.actionButton} ${styles.cancelButton}`}
|
|
title={cancelButtonTitle}
|
|
disabled={isDeleting}
|
|
>
|
|
<span className={styles.actionIcon}>
|
|
<IoIosClose />
|
|
</span>
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<button
|
|
onClick={handleDeleteClick}
|
|
className={`${styles.actionButton} ${styles.delete} ${loading || isDeleting || isDeletingFromHook ? styles.loading : ''} ${className}`}
|
|
title={buttonTitle}
|
|
disabled={disabled || loading || isDeleting || isDeletingFromHook}
|
|
>
|
|
<span className={styles.actionIcon}>
|
|
<IoIosTrash />
|
|
</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export default DeleteActionButton;
|