Add workflow timeline #300

Merged
itzaname merged 1 commits from feature/timeline into staging 2025-12-27 08:04:02 +00:00
2 changed files with 285 additions and 22 deletions

View File

@@ -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 (
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4 }}>
<ReviewItemHeader
displayName={item.DisplayName}
assetId={isMapfix ? item.TargetAssetID : undefined}
statusId={item.StatusID}
creator={item.Creator}
submitterId={item.Submitter}
/>
<>
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4 }}>
<ReviewItemHeader
displayName={item.DisplayName}
assetId={isMapfix ? item.TargetAssetID : undefined}
statusId={item.StatusID}
creator={item.Creator}
submitterId={item.Submitter}
/>
{/* Item Details */}
<Grid container spacing={2} sx={{ mt: 2 }}>
{/* Item Details */}
<Grid container spacing={2} sx={{ mt: 2 }}>
{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({
})}
</Grid>
{/* Description Section */}
{isMapfix && item.Description && (
<div style={{ marginTop: 24 }}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
Description
</Typography>
<Typography variant="body1">
{item.Description}
</Typography>
</div>
)}
</Paper>
{/* Description Section */}
{isMapfix && item.Description && (
<div style={{ marginTop: 24 }}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
Description
</Typography>
<Typography variant="body1">
{item.Description}
</Typography>
</div>
)}
</Paper>
{/* Workflow Progress Indicator */}
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4, display: { xs: 'none', md: 'block' } }}>
<WorkflowStepper
currentStatus={item.StatusID}
type={isMapfix ? 'mapfix' : 'submission'}
/>
</Paper>
</>
);
}

View File

@@ -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[] = [
Review

100% code duplication of submissionWorkflow

100% code duplication of submissionWorkflow
{
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 <CancelIcon className={className} sx={{ ...iconStyle, color: 'error.main' }} />;
}
if (completed) {
return <CheckCircleIcon className={className} sx={{ ...iconStyle, color: 'success.main' }} />;
}
if (active && isChangesRequested) {
return <WarningIcon className={className} sx={{ ...iconStyle, color: 'warning.main' }} />;
}
if (active) {
return <PendingIcon className={className} sx={{ ...iconStyle, color: 'primary.main', animation: `${pulse} 2s ease-in-out infinite` }} />;
}
return (
<Box
className={className}
sx={{
width: 24,
height: 24,
borderRadius: '50%',
border: 2,
borderColor: 'grey.400',
backgroundColor: 'background.paper',
transition: 'all 0.4s ease-in-out',
}}
/>
);
};
const WorkflowStepper: React.FC<WorkflowStepperProps> = ({ 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;
Review

I'm an idiot for making the 'Release' typo in the enum

I'm an idiot for making the 'Release' typo in the enum
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 (
<Box sx={{ width: '100%' }}>
<Stepper activeStep={-1} alternativeLabel connector={<CustomConnector />}>
{workflow.map((step) => (
<Step key={step.label} completed={false}>
<StepLabel
StepIconComponent={(props) => <CustomStepIcon {...props} isRejected={true} />}
error={true}
>
<Box sx={{ fontSize: '0.875rem', fontWeight: 500 }}>
{step.label}
</Box>
<Box sx={{ fontSize: '0.75rem', color: 'error.main', mt: 0.5 }}>
Rejected
</Box>
</StepLabel>
</Step>
))}
</Stepper>
</Box>
);
}
return (
<Box sx={{ width: '100%' }}>
<Stepper activeStep={activeStep} alternativeLabel connector={<CustomConnector />}>
{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 key={step.label} completed={isCompleted}>
<StepLabel
StepIconComponent={(props) => <CustomStepIcon {...props} isChangesRequested={stepIncludesCurrentStatus && isChangesRequested} />}
>
<Box sx={{
fontSize: '0.875rem',
fontWeight: isReleased ? 500 : (isActive ? 600 : 500),
transition: 'all 0.4s ease-in-out'
}}>
{step.label}
</Box>
{step.description && (
<Box
sx={{
fontSize: '0.75rem',
color: isReleased ? 'text.secondary' : (stepIncludesCurrentStatus && isChangesRequested ? 'warning.main' : (isActive ? 'primary.main' : 'text.secondary')),
mt: 0.5,
transition: 'color 0.4s ease-in-out'
}}
>
{step.description}
</Box>
)}
</StepLabel>
</Step>
);
})}
</Stepper>
</Box>
);
};
export default WorkflowStepper;