198 lines
6.4 KiB
TypeScript
198 lines
6.4 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { IoIosTrash, IoIosCheckmark, IoIosClose } from 'react-icons/io';
|
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
|
import styles from '../ActionButton.module.css';
|
|
|
|
export interface DeleteActionButtonProps<T = any> {
|
|
row: T;
|
|
disabled?: boolean | { disabled: boolean; message?: string };
|
|
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; // REQUIRED: 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 = 'deletingItems'
|
|
}: DeleteActionButtonProps<T>) {
|
|
const { t } = useLanguage();
|
|
const [isConfirming, setIsConfirming] = useState(false);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
|
|
// Extract disabled state and tooltip message
|
|
const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false;
|
|
const disabledMessage = typeof disabled === 'object' ? disabled?.message : undefined;
|
|
|
|
// Validate that hookData is provided with required operations
|
|
if (!hookData) {
|
|
throw new Error('DeleteActionButton requires hookData to be provided');
|
|
}
|
|
|
|
// Extract operations from hookData
|
|
const handleDelete = hookData[operationName];
|
|
const removeOptimistically = hookData.removeOptimistically || hookData.removeFileOptimistically;
|
|
const refetch = hookData.refetch;
|
|
const loadingState = hookData[loadingStateName];
|
|
|
|
|
|
// Validate required operations exist
|
|
if (!handleDelete) {
|
|
throw new Error(`DeleteActionButton requires hookData.${operationName} to be defined`);
|
|
}
|
|
if (!refetch) {
|
|
throw new Error('DeleteActionButton requires hookData.refetch to be defined');
|
|
}
|
|
|
|
// Reset confirmation state when row changes (e.g., when a previous row is deleted)
|
|
useEffect(() => {
|
|
setIsConfirming(false);
|
|
}, [(row as any)[idField]]);
|
|
|
|
// 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 (!isDisabled && !loading && !isDeleting) {
|
|
setIsConfirming(true);
|
|
}
|
|
};
|
|
|
|
const handleConfirmDelete = async (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
|
|
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 and reset state
|
|
if (removeOptimistically) {
|
|
removeOptimistically(itemId);
|
|
}
|
|
|
|
// Reset confirmation state immediately so it doesn't carry over to next row
|
|
setIsConfirming(false);
|
|
setIsDeleting(true);
|
|
|
|
// Call the delete API in the background
|
|
const success = await handleDelete(itemId);
|
|
|
|
if (success) {
|
|
// Always refetch after a successful delete. The server has actually
|
|
// removed the row, so fresh data won't bring it back — and this is
|
|
// what re-syncs pagination.totalItems (and clears any optimistic
|
|
// hidden-row state maintained by FormGeneratorTable).
|
|
refetch();
|
|
onSuccess?.(row);
|
|
} else {
|
|
// Refetch to restore the item in case of failure
|
|
await refetch();
|
|
onError?.(row, t('Löschen fehlgeschlagen'));
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Delete failed:', error);
|
|
onError?.(row, error.message || t('Löschen fehlgeschlagen'));
|
|
// Refetch to restore the item in case of failure
|
|
await refetch();
|
|
} finally {
|
|
setIsDeleting(false);
|
|
}
|
|
};
|
|
|
|
const handleCancelDelete = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
setIsConfirming(false);
|
|
};
|
|
|
|
const buttonTitle = title || t('Löschen');
|
|
const confirmButtonTitle = confirmTitle || t('Sind Sie sicher, dass Sie die {count} ausgewählten Elemente löschen möchten?');
|
|
const cancelButtonTitle = cancelTitle || t('Abbrechen');
|
|
|
|
// Check if ANY deletion is in progress (not just this specific item)
|
|
const isAnyDeletionInProgress = loadingState && loadingState.size > 0;
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
// Determine the final button title (tooltip)
|
|
const finalTitle = isDisabled && disabledMessage ? disabledMessage : buttonTitle;
|
|
|
|
return (
|
|
<button
|
|
onClick={handleDeleteClick}
|
|
className={`${styles.actionButton} ${styles.delete} ${loading || isDeleting || isAnyDeletionInProgress ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${className}`}
|
|
title={finalTitle}
|
|
disabled={isDisabled || loading || isDeleting || isAnyDeletionInProgress}
|
|
>
|
|
<span className={styles.actionIcon}>
|
|
<IoIosTrash />
|
|
</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export default DeleteActionButton;
|