Deploy workflow timeline #302
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
252
web/src/app/_components/review/WorkflowStepper.tsx
Normal file
252
web/src/app/_components/review/WorkflowStepper.tsx
Normal 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: 'Scripts approved'
|
||||
},
|
||||
{
|
||||
label: 'Uploaded',
|
||||
statuses: [Status.Uploading, Status.Uploaded],
|
||||
description: 'Published to Roblox group'
|
||||
},
|
||||
{
|
||||
label: 'Released',
|
||||
statuses: [Status.Releasing, Status.Release],
|
||||
description: 'Live in-game'
|
||||
}
|
||||
];
|
||||
|
||||
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: 'Scripts approved'
|
||||
},
|
||||
{
|
||||
label: 'Uploaded',
|
||||
statuses: [Status.Uploading, Status.Uploaded],
|
||||
description: 'Published to Roblox group'
|
||||
},
|
||||
{
|
||||
label: 'Released',
|
||||
statuses: [Status.Releasing, Status.Release],
|
||||
description: 'Live in-game'
|
||||
}
|
||||
];
|
||||
|
||||
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;
|
||||
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;
|
||||
Reference in New Issue
Block a user