frontend_nyla/src/components/FormGenerator/ActionButtons/DeleteActionButton/DeleteActionButton.tsx
2026-04-17 15:05:34 +02:00

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;