119 lines
3.3 KiB
TypeScript
119 lines
3.3 KiB
TypeScript
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||
|
|
import { useTranslation } from 'react-i18next';
|
||
|
|
import { Undo2, X } from 'lucide-react';
|
||
|
|
import { Button } from '@/components/ui/Button';
|
||
|
|
import { useUndoLastAction } from '@/api/entityHistory';
|
||
|
|
|
||
|
|
interface UndoToastProps {
|
||
|
|
message: string;
|
||
|
|
onUndo: () => void;
|
||
|
|
onDismiss: () => void;
|
||
|
|
isUndoing: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Undo toast shown after delete actions.
|
||
|
|
* Auto-dismisses after 5 seconds and is manually dismissable.
|
||
|
|
*/
|
||
|
|
export function UndoToast({ message, onUndo, onDismiss, isUndoing }: UndoToastProps) {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className="fixed bottom-4 right-4 z-[100] flex items-center gap-3 p-4 rounded-lg shadow-lg border border-secondary-200 bg-white max-w-sm"
|
||
|
|
role="alert"
|
||
|
|
aria-live="polite"
|
||
|
|
data-testid="undo-toast"
|
||
|
|
>
|
||
|
|
<p className="flex-1 text-sm font-medium text-secondary-900">{message}</p>
|
||
|
|
<Button
|
||
|
|
variant="primary"
|
||
|
|
size="sm"
|
||
|
|
onClick={onUndo}
|
||
|
|
isLoading={isUndoing}
|
||
|
|
icon={<Undo2 className="w-4 h-4" />}
|
||
|
|
>
|
||
|
|
{t('undoToast.undo', 'Undo')}
|
||
|
|
</Button>
|
||
|
|
<button
|
||
|
|
onClick={onDismiss}
|
||
|
|
className="flex-shrink-0 text-secondary-400 hover:text-secondary-700 min-h-touch min-w-touch flex items-center justify-center rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
||
|
|
aria-label={t('common.dismiss', 'Schließen')}
|
||
|
|
>
|
||
|
|
<X className="h-4 w-4" aria-hidden="true" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
interface UndoToastState {
|
||
|
|
message: string;
|
||
|
|
entityType: string;
|
||
|
|
entityId: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Hook that shows an undo toast after a delete action and provides the undo function.
|
||
|
|
*
|
||
|
|
* Returns:
|
||
|
|
* - `showUndoToast(message, entityType, entityId)`: trigger the toast
|
||
|
|
* - `undoToast`: the JSX element to render (render it once in the page)
|
||
|
|
*/
|
||
|
|
export function useUndoToast() {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
const [state, setState] = useState<UndoToastState | null>(null);
|
||
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||
|
|
const undoMutation = useUndoLastAction();
|
||
|
|
|
||
|
|
const clearTimer = useCallback(() => {
|
||
|
|
if (timerRef.current) {
|
||
|
|
clearTimeout(timerRef.current);
|
||
|
|
timerRef.current = null;
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const dismiss = useCallback(() => {
|
||
|
|
clearTimer();
|
||
|
|
setState(null);
|
||
|
|
}, [clearTimer]);
|
||
|
|
|
||
|
|
const showUndoToast = useCallback(
|
||
|
|
(message: string, entityType: string, entityId: string) => {
|
||
|
|
clearTimer();
|
||
|
|
setState({ message, entityType, entityId });
|
||
|
|
timerRef.current = setTimeout(() => {
|
||
|
|
setState(null);
|
||
|
|
timerRef.current = null;
|
||
|
|
}, 5000);
|
||
|
|
},
|
||
|
|
[clearTimer]
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleUndo = useCallback(async () => {
|
||
|
|
if (!state) return;
|
||
|
|
try {
|
||
|
|
await undoMutation.mutateAsync({
|
||
|
|
entityType: state.entityType,
|
||
|
|
entityId: state.entityId,
|
||
|
|
});
|
||
|
|
dismiss();
|
||
|
|
} catch {
|
||
|
|
// Keep the toast visible so the user can retry; the mutation error is surfaced elsewhere.
|
||
|
|
}
|
||
|
|
}, [state, undoMutation, dismiss]);
|
||
|
|
|
||
|
|
// Cleanup timer on unmount
|
||
|
|
useEffect(() => clearTimer, [clearTimer]);
|
||
|
|
|
||
|
|
const undoToast = state ? (
|
||
|
|
<UndoToast
|
||
|
|
message={state.message}
|
||
|
|
onUndo={handleUndo}
|
||
|
|
onDismiss={dismiss}
|
||
|
|
isUndoing={undoMutation.isPending}
|
||
|
|
/>
|
||
|
|
) : null;
|
||
|
|
|
||
|
|
return { showUndoToast, undoToast };
|
||
|
|
}
|