diff --git a/src/components/moderation/SubmissionReviewManager.tsx b/src/components/moderation/SubmissionReviewManager.tsx index e03986f0..8f621c73 100644 --- a/src/components/moderation/SubmissionReviewManager.tsx +++ b/src/components/moderation/SubmissionReviewManager.tsx @@ -77,7 +77,7 @@ export function SubmissionReviewManager({ const Container = isMobile ? Sheet : Dialog; // Lock monitoring integration - useLockMonitor(state, dispatch, submissionId); + const { extendLock } = useLockMonitor(state, dispatch, submissionId); // Auto-claim on mount useEffect(() => { @@ -96,10 +96,10 @@ export function SubmissionReviewManager({ // Load data dispatch({ type: 'LOAD_DATA' }); - await loadSubmissionItems(); + const loadedItems = await loadSubmissionItems(); - // Transition to reviewing state with loaded data (empty array as items are tracked separately) - dispatch({ type: 'DATA_LOADED', payload: { reviewData: [] } }); + // Transition to reviewing state with actual loaded data + dispatch({ type: 'DATA_LOADED', payload: { reviewData: loadedItems || [] } }); } catch (error: unknown) { dispatch({ type: 'ERROR', payload: { error: getErrorMessage(error) } }); handleError(error, { action: 'Claim Submission', userId: user?.id }); @@ -139,6 +139,8 @@ export function SubmissionReviewManager({ .filter(item => item.status === 'pending') .map(item => item.id); setSelectedItemIds(new Set(pendingIds)); + + return itemsWithDeps; } catch (error: unknown) { throw error; // Let handleClaimSubmission handle the error } diff --git a/src/lib/deletionDialogMachine.ts b/src/lib/deletionDialogMachine.ts index c4a9d2b8..15ed89f8 100644 --- a/src/lib/deletionDialogMachine.ts +++ b/src/lib/deletionDialogMachine.ts @@ -66,6 +66,8 @@ export function deletionDialogReducer( return initialState; default: + // Exhaustive check + const _exhaustive: never = action; return state; } } diff --git a/src/lib/moderation/lockMonitor.ts b/src/lib/moderation/lockMonitor.ts index f2e37e45..89c422d1 100644 --- a/src/lib/moderation/lockMonitor.ts +++ b/src/lib/moderation/lockMonitor.ts @@ -19,12 +19,13 @@ import { logger } from '../logger'; * @param state - Current moderation state * @param dispatch - State machine dispatch function * @param itemId - ID of the locked item (optional, for manual extension) + * @returns Extension function to manually extend lock */ export function useLockMonitor( state: ModerationState, dispatch: React.Dispatch, itemId?: string -) { +): { extendLock: () => Promise } { useEffect(() => { if (!hasActiveLock(state)) { return; @@ -54,6 +55,14 @@ export function useLockMonitor( return () => clearInterval(checkInterval); }, [state, dispatch, itemId]); + + const extendLock = async () => { + if (itemId) { + await handleExtendLock(itemId, dispatch); + } + }; + + return { extendLock }; } /** @@ -62,7 +71,7 @@ export function useLockMonitor( * @param submissionId - Submission ID * @param dispatch - State machine dispatch function */ -async function handleExtendLock( +export async function handleExtendLock( submissionId: string, dispatch: React.Dispatch ) { diff --git a/src/lib/moderationStateMachine.ts b/src/lib/moderationStateMachine.ts index f7f124d3..edd3fcf9 100644 --- a/src/lib/moderationStateMachine.ts +++ b/src/lib/moderationStateMachine.ts @@ -3,11 +3,7 @@ * Manages moderation workflow with type-safe state transitions and lock coordination */ -// Generic review data interface for moderation -export interface ModerationReviewData { - id: string; - [key: string]: unknown; -} +import type { SubmissionItemWithDeps } from './submissionItemsService'; // State definitions using discriminated unions export type ModerationState = @@ -15,7 +11,7 @@ export type ModerationState = | { status: 'claiming'; itemId: string } | { status: 'locked'; itemId: string; lockExpires: string } | { status: 'loading_data'; itemId: string; lockExpires: string } - | { status: 'reviewing'; itemId: string; lockExpires: string; reviewData: ModerationReviewData[] } + | { status: 'reviewing'; itemId: string; lockExpires: string; reviewData: SubmissionItemWithDeps[] } | { status: 'approving'; itemId: string } | { status: 'rejecting'; itemId: string } | { status: 'complete'; itemId: string; result: 'approved' | 'rejected' } @@ -28,7 +24,7 @@ export type ModerationAction = | { type: 'LOCK_ACQUIRED'; payload: { lockExpires: string } } | { type: 'LOCK_EXPIRED' } | { type: 'LOAD_DATA' } - | { type: 'DATA_LOADED'; payload: { reviewData: ModerationReviewData[] } } + | { type: 'DATA_LOADED'; payload: { reviewData: SubmissionItemWithDeps[] } } | { type: 'START_APPROVAL' } | { type: 'START_REJECTION' } | { type: 'COMPLETE'; payload: { result: 'approved' | 'rejected' } } @@ -54,9 +50,14 @@ export function moderationReducer( if (state.status !== 'claiming') { throw new Error(`Illegal transition: ${state.status} → locked`); } + // Validate lock expiry date + const lockDate = new Date(action.payload.lockExpires); + if (isNaN(lockDate.getTime())) { + throw new Error('Invalid lock expiry date'); + } return { status: 'locked', - itemId: (state as Extract).itemId, + itemId: state.itemId, lockExpires: action.payload.lockExpires }; @@ -67,7 +68,7 @@ export function moderationReducer( } return { status: 'lock_expired', - itemId: (state as Extract).itemId + itemId: state.itemId }; case 'LOAD_DATA': @@ -76,8 +77,8 @@ export function moderationReducer( } return { status: 'loading_data', - itemId: (state as Extract).itemId, - lockExpires: (state as Extract).lockExpires + itemId: state.itemId, + lockExpires: state.lockExpires }; case 'DATA_LOADED': @@ -86,8 +87,8 @@ export function moderationReducer( } return { status: 'reviewing', - itemId: (state as Extract).itemId, - lockExpires: (state as Extract).lockExpires, + itemId: state.itemId, + lockExpires: state.lockExpires, reviewData: action.payload.reviewData }; @@ -97,7 +98,7 @@ export function moderationReducer( } return { status: 'approving', - itemId: (state as Extract).itemId + itemId: state.itemId }; case 'START_REJECTION': @@ -106,7 +107,7 @@ export function moderationReducer( } return { status: 'rejecting', - itemId: (state as Extract).itemId + itemId: state.itemId }; case 'COMPLETE': @@ -115,7 +116,7 @@ export function moderationReducer( } return { status: 'complete', - itemId: (state as Extract).itemId, + itemId: state.itemId, result: action.payload.result }; @@ -127,7 +128,7 @@ export function moderationReducer( } return { status: 'error', - itemId: (state as Extract).itemId, + itemId: state.itemId, error: action.payload.error }; @@ -183,7 +184,7 @@ export function hasActiveLock(state: ModerationState): boolean { return false; } - const lockExpires = new Date((state as Extract).lockExpires); + const lockExpires = new Date(state.lockExpires); return lockExpires > new Date(); } @@ -196,7 +197,7 @@ export function needsLockRenewal(state: ModerationState): boolean { return false; } - const lockExpires = new Date((state as Extract).lockExpires); + const lockExpires = new Date(state.lockExpires); const now = new Date(); const timeUntilExpiry = lockExpires.getTime() - now.getTime(); diff --git a/src/lib/submissionStateMachine.ts b/src/lib/submissionStateMachine.ts index 2ae25597..c0135580 100644 --- a/src/lib/submissionStateMachine.ts +++ b/src/lib/submissionStateMachine.ts @@ -30,7 +30,6 @@ export type SubmissionState = // Action definitions using discriminated unions export type SubmissionAction = - | { type: 'UPDATE_DRAFT'; payload: Partial } | { type: 'VALIDATE'; payload: EntityFormData } | { type: 'VALIDATION_ERROR'; payload: ValidationError[] } | { type: 'SUBMIT'; payload: { submissionId: string } } @@ -50,16 +49,6 @@ export function submissionReducer( action: SubmissionAction ): SubmissionState { switch (action.type) { - case 'UPDATE_DRAFT': - if (state.status !== 'draft') { - console.warn(`Cannot update draft in state: ${state.status}`); - return state; - } - return { - status: 'draft', - data: { ...state.data, ...action.payload } - }; - case 'VALIDATE': if (state.status !== 'draft' && state.status !== 'validation_error') { throw new Error(`Illegal transition: ${state.status} → validating`); @@ -165,10 +154,6 @@ export function submissionReducer( } // State transition guards -export function canUpdateDraft(state: SubmissionState): boolean { - return state.status === 'draft'; -} - export function canValidate(state: SubmissionState): boolean { return state.status === 'draft' || state.status === 'validation_error'; }