diff --git a/web/src/app/_components/review/ReviewItem.tsx b/web/src/app/_components/review/ReviewItem.tsx index ecb2e83..da446f0 100644 --- a/web/src/app/_components/review/ReviewItem.tsx +++ b/web/src/app/_components/review/ReviewItem.tsx @@ -1,6 +1,7 @@ import { Paper, Grid, Typography } from "@mui/material"; import { ReviewItemHeader } from "./ReviewItemHeader"; import { CopyableField } from "@/app/_components/review/CopyableField"; +import WorkflowStepper from "./WorkflowStepper"; import { SubmissionInfo } from "@/app/ts/Submission"; import { MapfixInfo } from "@/app/ts/Mapfix"; @@ -46,17 +47,18 @@ export function ReviewItem({ } return ( - - + <> + + - {/* Item Details */} - + {/* Item Details */} + {fields.map((field) => { const fieldValue = (item as never)[field.key]; const displayValue = fieldValue === 0 || fieldValue == null ? 'N/A' : fieldValue; @@ -76,17 +78,26 @@ export function ReviewItem({ })} - {/* Description Section */} - {isMapfix && item.Description && ( -
- - Description - - - {item.Description} - -
- )} -
+ {/* Description Section */} + {isMapfix && item.Description && ( +
+ + Description + + + {item.Description} + +
+ )} +
+ + {/* Workflow Progress Indicator */} + + + + ); } diff --git a/web/src/app/_components/review/WorkflowStepper.tsx b/web/src/app/_components/review/WorkflowStepper.tsx new file mode 100644 index 0000000..e9eef19 --- /dev/null +++ b/web/src/app/_components/review/WorkflowStepper.tsx @@ -0,0 +1,252 @@ +import React from 'react'; +import { Stepper, Step, StepLabel, Box, StepConnector, stepConnectorClasses, StepIconProps, styled, keyframes } from '@mui/material'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import CancelIcon from '@mui/icons-material/Cancel'; +import PendingIcon from '@mui/icons-material/Pending'; +import WarningIcon from '@mui/icons-material/Warning'; +import { Status } from '@/app/ts/Status'; + +const pulse = keyframes` + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +`; + +interface WorkflowStepperProps { + currentStatus: number; + type: 'submission' | 'mapfix'; +} + +// Define the workflow steps +interface WorkflowStep { + label: string; + statuses: number[]; + description?: string; +} + +// Transitional states that show as "in progress" +const transitionalStates = [ + Status.Submitting, + Status.Validating, + Status.Uploading, + Status.Releasing +]; + +const submissionWorkflow: WorkflowStep[] = [ + { + label: 'Draft', + statuses: [Status.UnderConstruction, Status.ChangesRequested], + description: 'Creating or revising' + }, + { + label: 'Submitted', + statuses: [Status.Submitting, Status.Submitted], + description: 'Awaiting review' + }, + { + label: 'Accepted', + statuses: [Status.AcceptedUnvalidated], + description: 'Script review pending' + }, + { + label: 'Validated', + statuses: [Status.Validating, Status.Validated], + description: 'Automated validation' + }, + { + label: 'Uploaded', + statuses: [Status.Uploading, Status.Uploaded], + description: 'Published to Roblox group' + }, + { + label: 'Released', + statuses: [Status.Releasing, Status.Release], + description: 'Live in production' + } +]; + +const mapfixWorkflow: WorkflowStep[] = [ + { + label: 'Draft', + statuses: [Status.UnderConstruction, Status.ChangesRequested], + description: 'Creating or revising' + }, + { + label: 'Submitted', + statuses: [Status.Submitting, Status.Submitted], + description: 'Awaiting review' + }, + { + label: 'Accepted', + statuses: [Status.AcceptedUnvalidated], + description: 'Script review pending' + }, + { + label: 'Validated', + statuses: [Status.Validating, Status.Validated], + description: 'Automated validation' + }, + { + label: 'Uploaded', + statuses: [Status.Uploading, Status.Uploaded], + description: 'Published to Roblox group' + }, + { + label: 'Released', + statuses: [Status.Releasing, Status.Release], + description: 'Live in production' + } +]; + +const CustomConnector = styled(StepConnector)(({ theme }) => ({ + [`&.${stepConnectorClasses.alternativeLabel}`]: { + top: 10, + left: 'calc(-50% + 16px)', + right: 'calc(50% + 16px)', + }, + [`&.${stepConnectorClasses.active}`]: { + [`& .${stepConnectorClasses.line}`]: { + borderColor: theme.palette.primary.main, + }, + }, + [`&.${stepConnectorClasses.completed}`]: { + [`& .${stepConnectorClasses.line}`]: { + borderColor: theme.palette.success.main, + }, + }, + [`& .${stepConnectorClasses.line}`]: { + borderColor: theme.palette.mode === 'dark' ? theme.palette.grey[800] : '#eaeaf0', + borderTopWidth: 3, + borderRadius: 1, + transition: 'border-color 0.4s ease-in-out', + }, +})); + +const CustomStepIcon = (props: StepIconProps & { isRejected?: boolean; isChangesRequested?: boolean }) => { + const { active, completed, className, isRejected, isChangesRequested } = props; + + const iconStyle = { + transition: 'color 0.4s ease-in-out, opacity 0.3s ease-in-out, transform 0.3s ease-in-out', + }; + + if (isRejected) { + return ; + } + + if (completed) { + return ; + } + + if (active && isChangesRequested) { + return ; + } + + if (active) { + return ; + } + + return ( + + ); +}; + +const WorkflowStepper: React.FC = ({ currentStatus, type }) => { + const workflow = type === 'mapfix' ? mapfixWorkflow : submissionWorkflow; + + // Check if rejected or released + const isRejected = currentStatus === Status.Rejected; + const isReleased = currentStatus === Status.Release || currentStatus === Status.Releasing; + const isChangesRequested = currentStatus === Status.ChangesRequested; + + // Find the active step + const activeStep = workflow.findIndex(step => + step.statuses.includes(currentStatus) + ); + + // If rejected, show all steps as incomplete with error state + if (isRejected) { + return ( + + }> + {workflow.map((step) => ( + + } + error={true} + > + + {step.label} + + + Rejected + + + + ))} + + + ); + } + + return ( + + }> + {workflow.map((step, index) => { + const stepIncludesCurrentStatus = index === activeStep; + const isTransitional = transitionalStates.includes(currentStatus); + + // Show as active if in a transitional state OR if changes requested + const isActive = stepIncludesCurrentStatus && (isTransitional || isChangesRequested); + + const isCompleted = isReleased + ? true + : index < activeStep || (stepIncludesCurrentStatus && !isTransitional && !isChangesRequested); + + return ( + + } + > + + {step.label} + + {step.description && ( + + {step.description} + + )} + + + ); + })} + + + ); +}; + +export default WorkflowStepper;