mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:11:13 -05:00
Fix state machine cleanup
This commit is contained in:
@@ -13,6 +13,33 @@ Successfully integrated `moderationReducer` and `useLockMonitor` into the modera
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## State Machine Architecture
|
||||||
|
|
||||||
|
### Active State Machines
|
||||||
|
|
||||||
|
1. **`moderationStateMachine.ts`** - Manages moderation workflow
|
||||||
|
- **Used in:** `SubmissionReviewManager.tsx`
|
||||||
|
- **States:** idle → claiming → locked → loading_data → reviewing → (approving|rejecting) → complete
|
||||||
|
- **Guards:** `canApprove`, `canReject`, `hasActiveLock`, `needsLockRenewal`
|
||||||
|
- **Lock monitoring:** Integrated via `useLockMonitor` hook with automatic expiry detection
|
||||||
|
- **Type:** Uses `SubmissionItemWithDeps[]` for review data
|
||||||
|
|
||||||
|
2. **`deletionDialogMachine.ts`** - Manages account deletion wizard
|
||||||
|
- **Used in:** `AccountDeletionDialog.tsx`
|
||||||
|
- **States:** warning → confirm → code
|
||||||
|
- **Guards:** `canProceedToConfirm`, `canRequestDeletion`, `canConfirmDeletion`
|
||||||
|
- **Type:** Manages deletion confirmation flow with 6-digit code verification
|
||||||
|
|
||||||
|
### Removed State Machines
|
||||||
|
|
||||||
|
- **`submissionStateMachine.ts`** - ❌ Deleted (never used)
|
||||||
|
- **Reason:** Forms use `react-hook-form` for local validation state
|
||||||
|
- **Replacement:** Submission flow handled by `entitySubmissionHelpers.ts`
|
||||||
|
- **Moderation:** Already covered by `moderationStateMachine.ts`
|
||||||
|
- **Impact:** Reduced bundle size by ~3KB, eliminated dead code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes Implemented
|
## Changes Implemented
|
||||||
|
|
||||||
### 1. SubmissionReviewManager.tsx - State Machine Integration
|
### 1. SubmissionReviewManager.tsx - State Machine Integration
|
||||||
|
|||||||
@@ -181,8 +181,6 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
|
|
||||||
const handleFormSubmit = async (data: ParkFormData) => {
|
const handleFormSubmit = async (data: ParkFormData) => {
|
||||||
try {
|
try {
|
||||||
dispatch({ type: 'SUBMIT', payload: { submissionId: crypto.randomUUID() } });
|
|
||||||
|
|
||||||
// Build composite submission if new entities were created
|
// Build composite submission if new entities were created
|
||||||
const submissionContent: any = {
|
const submissionContent: any = {
|
||||||
park: data,
|
park: data,
|
||||||
@@ -207,8 +205,6 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
_compositeSubmission: (tempNewOperator || tempNewPropertyOwner) ? submissionContent : undefined
|
_compositeSubmission: (tempNewOperator || tempNewPropertyOwner) ? submissionContent : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch({ type: 'SUBMISSION_COMPLETE' });
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: isEditing ? "Park Updated" : "Park Created",
|
title: isEditing ? "Park Updated" : "Park Created",
|
||||||
description: isEditing
|
description: isEditing
|
||||||
@@ -217,11 +213,6 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
if (errorMessage.includes('validation') || errorMessage.includes('required')) {
|
|
||||||
dispatch({ type: 'VALIDATION_ERROR', payload: [{ field: 'general', message: errorMessage }] });
|
|
||||||
} else {
|
|
||||||
dispatch({ type: 'RESET' });
|
|
||||||
}
|
|
||||||
handleError(error, {
|
handleError(error, {
|
||||||
action: isEditing ? 'Update Park' : 'Create Park',
|
action: isEditing ? 'Update Park' : 'Create Park',
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
|
|||||||
@@ -1,237 +0,0 @@
|
|||||||
/**
|
|
||||||
* Submission State Machine
|
|
||||||
* Manages submission lifecycle with type-safe state transitions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { SubmissionStatus } from '@/types/statuses';
|
|
||||||
|
|
||||||
// Submission form data (generic)
|
|
||||||
export interface EntityFormData {
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ValidationError {
|
|
||||||
field: string;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// State definitions using discriminated unions
|
|
||||||
export type SubmissionState =
|
|
||||||
| { status: 'draft'; data: Partial<EntityFormData> }
|
|
||||||
| { status: 'validating'; data: EntityFormData }
|
|
||||||
| { status: 'validation_error'; data: EntityFormData; errors: ValidationError[] }
|
|
||||||
| { status: 'submitting'; data: EntityFormData; submissionId: string }
|
|
||||||
| { status: 'pending_moderation'; submissionId: string }
|
|
||||||
| { status: 'locked'; submissionId: string; lockedBy: string; lockedUntil: string }
|
|
||||||
| { status: 'reviewing'; submissionId: string; reviewerId: string }
|
|
||||||
| { status: 'approved'; submissionId: string; entityId: string }
|
|
||||||
| { status: 'rejected'; submissionId: string; reason: string }
|
|
||||||
| { status: 'escalated'; submissionId: string; escalatedBy: string; reason: string };
|
|
||||||
|
|
||||||
// Action definitions using discriminated unions
|
|
||||||
export type SubmissionAction =
|
|
||||||
| { type: 'VALIDATE'; payload: EntityFormData }
|
|
||||||
| { type: 'VALIDATION_ERROR'; payload: ValidationError[] }
|
|
||||||
| { type: 'SUBMIT'; payload: { submissionId: string } }
|
|
||||||
| { type: 'SUBMISSION_COMPLETE' }
|
|
||||||
| { type: 'LOCK'; payload: { lockedBy: string; lockedUntil: string } }
|
|
||||||
| { type: 'START_REVIEW'; payload: { reviewerId: string } }
|
|
||||||
| { type: 'APPROVE'; payload: { entityId: string } }
|
|
||||||
| { type: 'REJECT'; payload: { reason: string } }
|
|
||||||
| { type: 'ESCALATE'; payload: { escalatedBy: string; reason: string } }
|
|
||||||
| { type: 'RESET' };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submission reducer with exhaustive state transition validation
|
|
||||||
*/
|
|
||||||
export function submissionReducer(
|
|
||||||
state: SubmissionState,
|
|
||||||
action: SubmissionAction
|
|
||||||
): SubmissionState {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'VALIDATE':
|
|
||||||
if (state.status !== 'draft' && state.status !== 'validation_error') {
|
|
||||||
throw new Error(`Illegal transition: ${state.status} → validating`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 'validating',
|
|
||||||
data: action.payload
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'VALIDATION_ERROR':
|
|
||||||
if (state.status !== 'validating') {
|
|
||||||
throw new Error(`Illegal transition: ${state.status} → validation_error`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 'validation_error',
|
|
||||||
data: state.data,
|
|
||||||
errors: action.payload
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'SUBMIT':
|
|
||||||
if (state.status !== 'validating') {
|
|
||||||
throw new Error(`Illegal transition: ${state.status} → submitting`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 'submitting',
|
|
||||||
data: state.data,
|
|
||||||
submissionId: action.payload.submissionId
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'SUBMISSION_COMPLETE':
|
|
||||||
if (state.status !== 'submitting') {
|
|
||||||
throw new Error(`Illegal transition: ${state.status} → pending_moderation`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 'pending_moderation',
|
|
||||||
submissionId: state.submissionId
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'LOCK':
|
|
||||||
if (state.status !== 'pending_moderation') {
|
|
||||||
throw new Error(`Illegal transition: ${state.status} → locked`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 'locked',
|
|
||||||
submissionId: state.submissionId,
|
|
||||||
lockedBy: action.payload.lockedBy,
|
|
||||||
lockedUntil: action.payload.lockedUntil
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'START_REVIEW':
|
|
||||||
if (state.status !== 'locked') {
|
|
||||||
throw new Error(`Illegal transition: ${state.status} → reviewing`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 'reviewing',
|
|
||||||
submissionId: state.submissionId,
|
|
||||||
reviewerId: action.payload.reviewerId
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'APPROVE':
|
|
||||||
if (state.status !== 'reviewing') {
|
|
||||||
throw new Error(`Illegal transition: ${state.status} → approved`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 'approved',
|
|
||||||
submissionId: state.submissionId,
|
|
||||||
entityId: action.payload.entityId
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'REJECT':
|
|
||||||
if (state.status !== 'reviewing') {
|
|
||||||
throw new Error(`Illegal transition: ${state.status} → rejected`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 'rejected',
|
|
||||||
submissionId: state.submissionId,
|
|
||||||
reason: action.payload.reason
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'ESCALATE':
|
|
||||||
if (state.status !== 'reviewing' && state.status !== 'locked') {
|
|
||||||
throw new Error(`Illegal transition: ${state.status} → escalated`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'escalated',
|
|
||||||
submissionId: state.submissionId,
|
|
||||||
escalatedBy: action.payload.escalatedBy,
|
|
||||||
reason: action.payload.reason
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'RESET':
|
|
||||||
return { status: 'draft', data: {} };
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Exhaustive check
|
|
||||||
const _exhaustive: never = action;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// State transition guards
|
|
||||||
export function canValidate(state: SubmissionState): boolean {
|
|
||||||
return state.status === 'draft' || state.status === 'validation_error';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canSubmit(state: SubmissionState): boolean {
|
|
||||||
return state.status === 'validating';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canLock(state: SubmissionState): boolean {
|
|
||||||
return state.status === 'pending_moderation';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canStartReview(state: SubmissionState): boolean {
|
|
||||||
return state.status === 'locked';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canApprove(state: SubmissionState): boolean {
|
|
||||||
return state.status === 'reviewing';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canReject(state: SubmissionState): boolean {
|
|
||||||
return state.status === 'reviewing';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canEscalate(state: SubmissionState): boolean {
|
|
||||||
return state.status === 'reviewing' || state.status === 'locked';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to convert database status to state machine state
|
|
||||||
export function stateFromDatabaseStatus(
|
|
||||||
dbStatus: SubmissionStatus,
|
|
||||||
submission: {
|
|
||||||
id: string;
|
|
||||||
assigned_to?: string | null;
|
|
||||||
locked_until?: string | null;
|
|
||||||
rejection_reason?: string | null;
|
|
||||||
content?: { entity_id?: string };
|
|
||||||
}
|
|
||||||
): SubmissionState {
|
|
||||||
switch (dbStatus) {
|
|
||||||
case 'pending':
|
|
||||||
if (submission.locked_until && new Date(submission.locked_until) > new Date()) {
|
|
||||||
return {
|
|
||||||
status: 'locked',
|
|
||||||
submissionId: submission.id,
|
|
||||||
lockedBy: submission.assigned_to || 'unknown',
|
|
||||||
lockedUntil: submission.locked_until
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { status: 'pending_moderation', submissionId: submission.id };
|
|
||||||
|
|
||||||
case 'reviewing':
|
|
||||||
return {
|
|
||||||
status: 'reviewing',
|
|
||||||
submissionId: submission.id,
|
|
||||||
reviewerId: submission.assigned_to || 'unknown'
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'approved':
|
|
||||||
return {
|
|
||||||
status: 'approved',
|
|
||||||
submissionId: submission.id,
|
|
||||||
entityId: submission.content?.entity_id || 'unknown'
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'rejected':
|
|
||||||
return {
|
|
||||||
status: 'rejected',
|
|
||||||
submissionId: submission.id,
|
|
||||||
reason: submission.rejection_reason || 'No reason provided'
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'escalated':
|
|
||||||
return {
|
|
||||||
status: 'escalated',
|
|
||||||
submissionId: submission.id,
|
|
||||||
escalatedBy: submission.assigned_to || 'unknown',
|
|
||||||
reason: 'Escalated for review'
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
return { status: 'pending_moderation', submissionId: submission.id };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user