mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 10:11:13 -05:00
Fix state machine issues
This commit is contained in:
@@ -77,7 +77,7 @@ export function SubmissionReviewManager({
|
|||||||
const Container = isMobile ? Sheet : Dialog;
|
const Container = isMobile ? Sheet : Dialog;
|
||||||
|
|
||||||
// Lock monitoring integration
|
// Lock monitoring integration
|
||||||
useLockMonitor(state, dispatch, submissionId);
|
const { extendLock } = useLockMonitor(state, dispatch, submissionId);
|
||||||
|
|
||||||
// Auto-claim on mount
|
// Auto-claim on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -96,10 +96,10 @@ export function SubmissionReviewManager({
|
|||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
dispatch({ type: '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)
|
// Transition to reviewing state with actual loaded data
|
||||||
dispatch({ type: 'DATA_LOADED', payload: { reviewData: [] } });
|
dispatch({ type: 'DATA_LOADED', payload: { reviewData: loadedItems || [] } });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
dispatch({ type: 'ERROR', payload: { error: getErrorMessage(error) } });
|
dispatch({ type: 'ERROR', payload: { error: getErrorMessage(error) } });
|
||||||
handleError(error, { action: 'Claim Submission', userId: user?.id });
|
handleError(error, { action: 'Claim Submission', userId: user?.id });
|
||||||
@@ -139,6 +139,8 @@ export function SubmissionReviewManager({
|
|||||||
.filter(item => item.status === 'pending')
|
.filter(item => item.status === 'pending')
|
||||||
.map(item => item.id);
|
.map(item => item.id);
|
||||||
setSelectedItemIds(new Set(pendingIds));
|
setSelectedItemIds(new Set(pendingIds));
|
||||||
|
|
||||||
|
return itemsWithDeps;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
throw error; // Let handleClaimSubmission handle the error
|
throw error; // Let handleClaimSubmission handle the error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ export function deletionDialogReducer(
|
|||||||
return initialState;
|
return initialState;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
// Exhaustive check
|
||||||
|
const _exhaustive: never = action;
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ import { logger } from '../logger';
|
|||||||
* @param state - Current moderation state
|
* @param state - Current moderation state
|
||||||
* @param dispatch - State machine dispatch function
|
* @param dispatch - State machine dispatch function
|
||||||
* @param itemId - ID of the locked item (optional, for manual extension)
|
* @param itemId - ID of the locked item (optional, for manual extension)
|
||||||
|
* @returns Extension function to manually extend lock
|
||||||
*/
|
*/
|
||||||
export function useLockMonitor(
|
export function useLockMonitor(
|
||||||
state: ModerationState,
|
state: ModerationState,
|
||||||
dispatch: React.Dispatch<ModerationAction>,
|
dispatch: React.Dispatch<ModerationAction>,
|
||||||
itemId?: string
|
itemId?: string
|
||||||
) {
|
): { extendLock: () => Promise<void> } {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasActiveLock(state)) {
|
if (!hasActiveLock(state)) {
|
||||||
return;
|
return;
|
||||||
@@ -54,6 +55,14 @@ export function useLockMonitor(
|
|||||||
|
|
||||||
return () => clearInterval(checkInterval);
|
return () => clearInterval(checkInterval);
|
||||||
}, [state, dispatch, itemId]);
|
}, [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 submissionId - Submission ID
|
||||||
* @param dispatch - State machine dispatch function
|
* @param dispatch - State machine dispatch function
|
||||||
*/
|
*/
|
||||||
async function handleExtendLock(
|
export async function handleExtendLock(
|
||||||
submissionId: string,
|
submissionId: string,
|
||||||
dispatch: React.Dispatch<ModerationAction>
|
dispatch: React.Dispatch<ModerationAction>
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -3,11 +3,7 @@
|
|||||||
* Manages moderation workflow with type-safe state transitions and lock coordination
|
* Manages moderation workflow with type-safe state transitions and lock coordination
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Generic review data interface for moderation
|
import type { SubmissionItemWithDeps } from './submissionItemsService';
|
||||||
export interface ModerationReviewData {
|
|
||||||
id: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// State definitions using discriminated unions
|
// State definitions using discriminated unions
|
||||||
export type ModerationState =
|
export type ModerationState =
|
||||||
@@ -15,7 +11,7 @@ export type ModerationState =
|
|||||||
| { status: 'claiming'; itemId: string }
|
| { status: 'claiming'; itemId: string }
|
||||||
| { status: 'locked'; itemId: string; lockExpires: string }
|
| { status: 'locked'; itemId: string; lockExpires: string }
|
||||||
| { status: 'loading_data'; 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: 'approving'; itemId: string }
|
||||||
| { status: 'rejecting'; itemId: string }
|
| { status: 'rejecting'; itemId: string }
|
||||||
| { status: 'complete'; itemId: string; result: 'approved' | 'rejected' }
|
| { status: 'complete'; itemId: string; result: 'approved' | 'rejected' }
|
||||||
@@ -28,7 +24,7 @@ export type ModerationAction =
|
|||||||
| { type: 'LOCK_ACQUIRED'; payload: { lockExpires: string } }
|
| { type: 'LOCK_ACQUIRED'; payload: { lockExpires: string } }
|
||||||
| { type: 'LOCK_EXPIRED' }
|
| { type: 'LOCK_EXPIRED' }
|
||||||
| { type: 'LOAD_DATA' }
|
| { type: 'LOAD_DATA' }
|
||||||
| { type: 'DATA_LOADED'; payload: { reviewData: ModerationReviewData[] } }
|
| { type: 'DATA_LOADED'; payload: { reviewData: SubmissionItemWithDeps[] } }
|
||||||
| { type: 'START_APPROVAL' }
|
| { type: 'START_APPROVAL' }
|
||||||
| { type: 'START_REJECTION' }
|
| { type: 'START_REJECTION' }
|
||||||
| { type: 'COMPLETE'; payload: { result: 'approved' | 'rejected' } }
|
| { type: 'COMPLETE'; payload: { result: 'approved' | 'rejected' } }
|
||||||
@@ -54,9 +50,14 @@ export function moderationReducer(
|
|||||||
if (state.status !== 'claiming') {
|
if (state.status !== 'claiming') {
|
||||||
throw new Error(`Illegal transition: ${state.status} → locked`);
|
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 {
|
return {
|
||||||
status: 'locked',
|
status: 'locked',
|
||||||
itemId: (state as Extract<ModerationState, { status: 'claiming' }>).itemId,
|
itemId: state.itemId,
|
||||||
lockExpires: action.payload.lockExpires
|
lockExpires: action.payload.lockExpires
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ export function moderationReducer(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
status: 'lock_expired',
|
status: 'lock_expired',
|
||||||
itemId: (state as Extract<ModerationState, { status: 'locked' | 'reviewing' | 'loading_data' }>).itemId
|
itemId: state.itemId
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'LOAD_DATA':
|
case 'LOAD_DATA':
|
||||||
@@ -76,8 +77,8 @@ export function moderationReducer(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
status: 'loading_data',
|
status: 'loading_data',
|
||||||
itemId: (state as Extract<ModerationState, { status: 'locked' }>).itemId,
|
itemId: state.itemId,
|
||||||
lockExpires: (state as Extract<ModerationState, { status: 'locked' }>).lockExpires
|
lockExpires: state.lockExpires
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'DATA_LOADED':
|
case 'DATA_LOADED':
|
||||||
@@ -86,8 +87,8 @@ export function moderationReducer(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
status: 'reviewing',
|
status: 'reviewing',
|
||||||
itemId: (state as Extract<ModerationState, { status: 'loading_data' }>).itemId,
|
itemId: state.itemId,
|
||||||
lockExpires: (state as Extract<ModerationState, { status: 'loading_data' }>).lockExpires,
|
lockExpires: state.lockExpires,
|
||||||
reviewData: action.payload.reviewData
|
reviewData: action.payload.reviewData
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ export function moderationReducer(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
status: 'approving',
|
status: 'approving',
|
||||||
itemId: (state as Extract<ModerationState, { status: 'reviewing' }>).itemId
|
itemId: state.itemId
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'START_REJECTION':
|
case 'START_REJECTION':
|
||||||
@@ -106,7 +107,7 @@ export function moderationReducer(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
status: 'rejecting',
|
status: 'rejecting',
|
||||||
itemId: (state as Extract<ModerationState, { status: 'reviewing' }>).itemId
|
itemId: state.itemId
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'COMPLETE':
|
case 'COMPLETE':
|
||||||
@@ -115,7 +116,7 @@ export function moderationReducer(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
status: 'complete',
|
status: 'complete',
|
||||||
itemId: (state as Extract<ModerationState, { status: 'approving' | 'rejecting' }>).itemId,
|
itemId: state.itemId,
|
||||||
result: action.payload.result
|
result: action.payload.result
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,7 +128,7 @@ export function moderationReducer(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
itemId: (state as Extract<ModerationState, { itemId: string }>).itemId,
|
itemId: state.itemId,
|
||||||
error: action.payload.error
|
error: action.payload.error
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -183,7 +184,7 @@ export function hasActiveLock(state: ModerationState): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lockExpires = new Date((state as Extract<ModerationState, { lockExpires: string }>).lockExpires);
|
const lockExpires = new Date(state.lockExpires);
|
||||||
return lockExpires > new Date();
|
return lockExpires > new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +197,7 @@ export function needsLockRenewal(state: ModerationState): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lockExpires = new Date((state as Extract<ModerationState, { lockExpires: string }>).lockExpires);
|
const lockExpires = new Date(state.lockExpires);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const timeUntilExpiry = lockExpires.getTime() - now.getTime();
|
const timeUntilExpiry = lockExpires.getTime() - now.getTime();
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export type SubmissionState =
|
|||||||
|
|
||||||
// Action definitions using discriminated unions
|
// Action definitions using discriminated unions
|
||||||
export type SubmissionAction =
|
export type SubmissionAction =
|
||||||
| { type: 'UPDATE_DRAFT'; payload: Partial<EntityFormData> }
|
|
||||||
| { type: 'VALIDATE'; payload: EntityFormData }
|
| { type: 'VALIDATE'; payload: EntityFormData }
|
||||||
| { type: 'VALIDATION_ERROR'; payload: ValidationError[] }
|
| { type: 'VALIDATION_ERROR'; payload: ValidationError[] }
|
||||||
| { type: 'SUBMIT'; payload: { submissionId: string } }
|
| { type: 'SUBMIT'; payload: { submissionId: string } }
|
||||||
@@ -50,16 +49,6 @@ export function submissionReducer(
|
|||||||
action: SubmissionAction
|
action: SubmissionAction
|
||||||
): SubmissionState {
|
): SubmissionState {
|
||||||
switch (action.type) {
|
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':
|
case 'VALIDATE':
|
||||||
if (state.status !== 'draft' && state.status !== 'validation_error') {
|
if (state.status !== 'draft' && state.status !== 'validation_error') {
|
||||||
throw new Error(`Illegal transition: ${state.status} → validating`);
|
throw new Error(`Illegal transition: ${state.status} → validating`);
|
||||||
@@ -165,10 +154,6 @@ export function submissionReducer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// State transition guards
|
// State transition guards
|
||||||
export function canUpdateDraft(state: SubmissionState): boolean {
|
|
||||||
return state.status === 'draft';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canValidate(state: SubmissionState): boolean {
|
export function canValidate(state: SubmissionState): boolean {
|
||||||
return state.status === 'draft' || state.status === 'validation_error';
|
return state.status === 'draft' || state.status === 'validation_error';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user