mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 07:51:13 -05:00
Integrate transaction resilience hook
Integrate the `useTransactionResilience` hook into `SubmissionReviewManager.tsx` to add timeout detection, auto-release functionality, and idempotency key management to moderation actions. The `handleApprove` and `handleReject` functions have been updated to use the `executeTransaction` wrapper for these operations.
This commit is contained in:
@@ -6,6 +6,7 @@ import { handleError, getErrorMessage } from '@/lib/errorHandler';
|
|||||||
import { invokeWithTracking } from '@/lib/edgeFunctionTracking';
|
import { invokeWithTracking } from '@/lib/edgeFunctionTracking';
|
||||||
import { moderationReducer, canApprove, canReject, hasActiveLock } from '@/lib/moderationStateMachine';
|
import { moderationReducer, canApprove, canReject, hasActiveLock } from '@/lib/moderationStateMachine';
|
||||||
import { useLockMonitor } from '@/lib/moderation/lockMonitor';
|
import { useLockMonitor } from '@/lib/moderation/lockMonitor';
|
||||||
|
import { useTransactionResilience } from '@/hooks/useTransactionResilience';
|
||||||
import {
|
import {
|
||||||
fetchSubmissionItems,
|
fetchSubmissionItems,
|
||||||
buildDependencyTree,
|
buildDependencyTree,
|
||||||
@@ -92,6 +93,15 @@ export function SubmissionReviewManager({
|
|||||||
// Lock monitoring integration
|
// Lock monitoring integration
|
||||||
const { extendLock } = useLockMonitor(state, dispatch, submissionId);
|
const { extendLock } = useLockMonitor(state, dispatch, submissionId);
|
||||||
|
|
||||||
|
// Transaction resilience (timeout detection & auto-release)
|
||||||
|
const { executeTransaction } = useTransactionResilience({
|
||||||
|
submissionId,
|
||||||
|
timeoutMs: 30000, // 30s timeout
|
||||||
|
autoReleaseOnUnload: true,
|
||||||
|
autoReleaseOnInactivity: true,
|
||||||
|
inactivityMinutes: 10,
|
||||||
|
});
|
||||||
|
|
||||||
// Moderation actions
|
// Moderation actions
|
||||||
const { escalateSubmission } = useModerationActions({
|
const { escalateSubmission } = useModerationActions({
|
||||||
user,
|
user,
|
||||||
@@ -230,6 +240,7 @@ export function SubmissionReviewManager({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedItems = items.filter(item => selectedItemIds.has(item.id));
|
const selectedItems = items.filter(item => selectedItemIds.has(item.id));
|
||||||
|
const selectedIds = Array.from(selectedItemIds);
|
||||||
|
|
||||||
// Transition: reviewing → approving
|
// Transition: reviewing → approving
|
||||||
dispatch({ type: 'START_APPROVAL' });
|
dispatch({ type: 'START_APPROVAL' });
|
||||||
@@ -258,6 +269,7 @@ export function SubmissionReviewManager({
|
|||||||
id: item.id
|
id: item.id
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
setValidationResults(validationResultsMap);
|
setValidationResults(validationResultsMap);
|
||||||
|
|
||||||
@@ -324,64 +336,73 @@ export function SubmissionReviewManager({
|
|||||||
return; // Ask for confirmation
|
return; // Ask for confirmation
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed with approval
|
// Proceed with approval - wrapped with transaction resilience
|
||||||
const { supabase } = await import('@/integrations/supabase/client');
|
await executeTransaction(
|
||||||
|
'approval',
|
||||||
// Call the edge function for backend processing
|
selectedIds,
|
||||||
const { data, error, requestId } = await invokeWithTracking(
|
async (idempotencyKey) => {
|
||||||
'process-selective-approval',
|
const { supabase } = await import('@/integrations/supabase/client');
|
||||||
{
|
|
||||||
itemIds: Array.from(selectedItemIds),
|
// Call the edge function for backend processing
|
||||||
submissionId
|
const { data, error, requestId } = await invokeWithTracking(
|
||||||
},
|
'process-selective-approval',
|
||||||
user?.id
|
{
|
||||||
|
itemIds: selectedIds,
|
||||||
|
submissionId,
|
||||||
|
idempotencyKey, // Pass idempotency key to edge function
|
||||||
|
},
|
||||||
|
user?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message || 'Failed to process approval');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data?.success) {
|
||||||
|
throw new Error(data?.error || 'Approval processing failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transition: approving → complete
|
||||||
|
dispatch({ type: 'COMPLETE', payload: { result: 'approved' } });
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Items Approved',
|
||||||
|
description: `Successfully approved ${selectedIds.length} item(s)${requestId ? ` (Request: ${requestId.substring(0, 8)})` : ''}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ApprovalResult { success: boolean; item_id: string; error?: string }
|
||||||
|
const successCount = data.results.filter((r: ApprovalResult) => r.success).length;
|
||||||
|
const failCount = data.results.filter((r: ApprovalResult) => !r.success).length;
|
||||||
|
|
||||||
|
const allFailed = failCount > 0 && successCount === 0;
|
||||||
|
const someFailed = failCount > 0 && successCount > 0;
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: allFailed ? 'Approval Failed' : someFailed ? 'Partial Approval' : 'Approval Complete',
|
||||||
|
description: failCount > 0
|
||||||
|
? `Approved ${successCount} item(s), ${failCount} failed`
|
||||||
|
: `Successfully approved ${successCount} item(s)`,
|
||||||
|
variant: allFailed ? 'destructive' : someFailed ? 'default' : 'default',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset warning confirmation state after approval
|
||||||
|
setUserConfirmedWarnings(false);
|
||||||
|
|
||||||
|
// If ALL items failed, don't close dialog - show errors
|
||||||
|
if (allFailed) {
|
||||||
|
dispatch({ type: 'ERROR', payload: { error: 'All items failed' } });
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset warning confirmation state after approval
|
||||||
|
setUserConfirmedWarnings(false);
|
||||||
|
|
||||||
|
onComplete();
|
||||||
|
onOpenChange(false);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw new Error(error.message || 'Failed to process approval');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data?.success) {
|
|
||||||
throw new Error(data?.error || 'Approval processing failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transition: approving → complete
|
|
||||||
dispatch({ type: 'COMPLETE', payload: { result: 'approved' } });
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Items Approved',
|
|
||||||
description: `Successfully approved ${selectedItemIds.size} item(s)${requestId ? ` (Request: ${requestId.substring(0, 8)})` : ''}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
interface ApprovalResult { success: boolean; item_id: string; error?: string }
|
|
||||||
const successCount = data.results.filter((r: ApprovalResult) => r.success).length;
|
|
||||||
const failCount = data.results.filter((r: ApprovalResult) => !r.success).length;
|
|
||||||
|
|
||||||
const allFailed = failCount > 0 && successCount === 0;
|
|
||||||
const someFailed = failCount > 0 && successCount > 0;
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: allFailed ? 'Approval Failed' : someFailed ? 'Partial Approval' : 'Approval Complete',
|
|
||||||
description: failCount > 0
|
|
||||||
? `Approved ${successCount} item(s), ${failCount} failed`
|
|
||||||
: `Successfully approved ${successCount} item(s)`,
|
|
||||||
variant: allFailed ? 'destructive' : someFailed ? 'default' : 'default',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset warning confirmation state after approval
|
|
||||||
setUserConfirmedWarnings(false);
|
|
||||||
|
|
||||||
// If ALL items failed, don't close dialog - show errors
|
|
||||||
if (allFailed) {
|
|
||||||
dispatch({ type: 'ERROR', payload: { error: 'All items failed' } });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset warning confirmation state after approval
|
|
||||||
setUserConfirmedWarnings(false);
|
|
||||||
|
|
||||||
onComplete();
|
|
||||||
onOpenChange(false);
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
dispatch({ type: 'ERROR', payload: { error: getErrorMessage(error) } });
|
dispatch({ type: 'ERROR', payload: { error: getErrorMessage(error) } });
|
||||||
handleError(error, {
|
handleError(error, {
|
||||||
@@ -438,23 +459,34 @@ export function SubmissionReviewManager({
|
|||||||
|
|
||||||
if (!user?.id) return;
|
if (!user?.id) return;
|
||||||
|
|
||||||
|
const selectedItems = items.filter(item => selectedItemIds.has(item.id));
|
||||||
|
const selectedIds = selectedItems.map(item => item.id);
|
||||||
|
|
||||||
// Transition: reviewing → rejecting
|
// Transition: reviewing → rejecting
|
||||||
dispatch({ type: 'START_REJECTION' });
|
dispatch({ type: 'START_REJECTION' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const selectedItems = items.filter(item => selectedItemIds.has(item.id));
|
// Wrap rejection with transaction resilience
|
||||||
await rejectSubmissionItems(selectedItems, reason, user.id, cascade);
|
await executeTransaction(
|
||||||
|
'rejection',
|
||||||
// Transition: rejecting → complete
|
selectedIds,
|
||||||
dispatch({ type: 'COMPLETE', payload: { result: 'rejected' } });
|
async (idempotencyKey) => {
|
||||||
|
await rejectSubmissionItems(selectedItems, reason, user.id, cascade);
|
||||||
toast({
|
|
||||||
title: 'Items Rejected',
|
// Transition: rejecting → complete
|
||||||
description: `Successfully rejected ${selectedItems.length} item${selectedItems.length !== 1 ? 's' : ''}`,
|
dispatch({ type: 'COMPLETE', payload: { result: 'rejected' } });
|
||||||
});
|
|
||||||
|
toast({
|
||||||
|
title: 'Items Rejected',
|
||||||
|
description: `Successfully rejected ${selectedItems.length} item${selectedItems.length !== 1 ? 's' : ''}`,
|
||||||
|
});
|
||||||
|
|
||||||
onComplete();
|
onComplete();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
dispatch({ type: 'ERROR', payload: { error: getErrorMessage(error) } });
|
dispatch({ type: 'ERROR', payload: { error: getErrorMessage(error) } });
|
||||||
handleError(error, {
|
handleError(error, {
|
||||||
|
|||||||
Reference in New Issue
Block a user