Group buttons and add confirmation dialogues #310

Merged
itzaname merged 5 commits from feature/review-buttons into staging 2025-12-28 00:34:58 +00:00

View File

@@ -1,13 +1,16 @@
import React from 'react';
import { Button, Stack } from '@mui/material';
import React, { useState } from 'react';
import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, Typography, Box } from '@mui/material';
import {MapfixInfo } from "@/app/ts/Mapfix";
import {hasRole, Roles, RolesConstants} from "@/app/ts/Roles";
import {SubmissionInfo} from "@/app/ts/Submission";
import {Status, StatusMatches} from "@/app/ts/Status";
interface ReviewAction {
name: string,
action: string,
name: string;
action: string;
confirmTitle?: string;
confirmMessage?: string;
requiresConfirmation: boolean;
}
interface ReviewButtonsProps {
@@ -19,20 +22,102 @@ interface ReviewButtonsProps {
}
const ReviewActions = {
Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction,
AdminSubmit: {name:"Admin Submit",action:"trigger-submit"} as ReviewAction,
SubmitUnchecked: {name:"Submit Unchecked", action:"trigger-submit-unchecked"} as ReviewAction,
ResetSubmitting: {name:"Reset Submitting",action:"reset-submitting"} as ReviewAction,
Revoke: {name:"Revoke",action:"revoke"} as ReviewAction,
Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction,
Reject: {name:"Reject",action:"reject"} as ReviewAction,
Validate: {name:"Validate",action:"retry-validate"} as ReviewAction,
ResetValidating: {name:"Reset Validating",action:"reset-validating"} as ReviewAction,
RequestChanges: {name:"Request Changes",action:"request-changes"} as ReviewAction,
Upload: {name:"Upload",action:"trigger-upload"} as ReviewAction,
ResetUploading: {name:"Reset Uploading",action:"reset-uploading"} as ReviewAction,
Release: {name:"Release",action:"trigger-release"} as ReviewAction,
ResetReleasing: {name:"Reset Releasing",action:"reset-releasing"} as ReviewAction,
Submit: {
name: "Submit for Review",
action: "trigger-submit",
confirmTitle: "Submit for Review",
confirmMessage: "Are you ready to submit this for review? The model version is locked in once submitted, but you can revoke it later if needed.",
itzaname marked this conversation as resolved Outdated

Are you ready to submit this for review? The model version is locked in once submitted, but you can revoke it and click submit again if needed.

Are you ready to submit this for review? The model version is locked in once submitted, but you can revoke it and click submit again if needed.
requiresConfirmation: true
} as ReviewAction,
AdminSubmit: {
name: "Submit on Behalf of User",
action: "trigger-submit",
confirmTitle: "Admin Submit",
confirmMessage: "This will submit the work as if the original user did it. Continue?",
requiresConfirmation: true
} as ReviewAction,
SubmitUnchecked: {
name: "Approve Without Validation",
action: "trigger-submit-unchecked",
confirmTitle: "Skip Validation",
confirmMessage: "This will approve without running validation checks. Only use this if you're certain the work is correct.",
requiresConfirmation: true
} as ReviewAction,
ResetSubmitting: {
name: "Reset Submit Process",
action: "reset-submitting",
confirmTitle: "Reset Submit",
confirmMessage: "This will force-cancel the submission process and return to 'Under Construction' status. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
requiresConfirmation: true
} as ReviewAction,
Revoke: {
name: "Revoke",
action: "revoke",
confirmTitle: "Revoke",
confirmMessage: "This will withdraw from review and return to 'Under Construction' status.",
requiresConfirmation: true
} as ReviewAction,
Accept: {
name: "Accept & Validate",
action: "trigger-validate",
confirmTitle: "Accept",
confirmMessage: "This will accept and trigger validation. The work will proceed to the next stage.",
requiresConfirmation: true
} as ReviewAction,
Reject: {
name: "Reject",
action: "reject",
confirmTitle: "Reject",
confirmMessage: "This will permanently reject. The user will need to create a new one. Are you sure?",
requiresConfirmation: true
} as ReviewAction,
Validate: {
name: "Run Validation",
action: "retry-validate",
requiresConfirmation: false
} as ReviewAction,
ResetValidating: {
name: "Reset Validation Process",
action: "reset-validating",
confirmTitle: "Reset Validation",
confirmMessage: "This will force-abort the validation process so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
requiresConfirmation: true
} as ReviewAction,
RequestChanges: {
name: "Request Changes",
action: "request-changes",
confirmTitle: "Request Changes",
confirmMessage: "Request that the submitter make changes. Make sure you've explained which changes are requested in a comment.",
itzaname marked this conversation as resolved Outdated

This wording is strange.
Suggestion:

Request that the submitter make changes.  Make sure you've explained which changes are requested in a comment.
This wording is strange. Suggestion: ``` Request that the submitter make changes. Make sure you've explained which changes are requested in a comment. ```
requiresConfirmation: true
} as ReviewAction,
Upload: {
name: "Upload to Roblox",
action: "trigger-upload",
confirmTitle: "Upload to Roblox Group",
confirmMessage: "This will upload the validated work to the Roblox group. Continue?",
requiresConfirmation: true
} as ReviewAction,
ResetUploading: {
name: "Reset Upload Process",
action: "reset-uploading",
confirmTitle: "Reset Upload",
confirmMessage: "This will force-abort the upload to Roblox so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
requiresConfirmation: true
} as ReviewAction,
Release: {
name: "Release to Game",
action: "trigger-release",
confirmTitle: "Release to Game",
confirmMessage: "This will make the work available in game. This is the final step!",
requiresConfirmation: true
} as ReviewAction,
ResetReleasing: {
name: "Reset Release Process",
action: "reset-releasing",
confirmTitle: "Reset Release",
confirmMessage: "This will force-abort the release to the game so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
requiresConfirmation: true
} as ReviewAction,
}
const ReviewButtons: React.FC<ReviewButtonsProps> = ({
@@ -42,16 +127,46 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
roles,
type,
}) => {
const getVisibleButtons = () => {
if (!item || userId === null) return [];
const [confirmDialog, setConfirmDialog] = useState<{
open: boolean;
action: ReviewAction | null;
}>({ open: false, action: null });
const handleButtonClick = (action: ReviewAction) => {
if (action.requiresConfirmation) {
setConfirmDialog({ open: true, action });
} else {
onClick(action.action, item.ID);
}
};
const handleConfirm = () => {
if (confirmDialog.action) {
onClick(confirmDialog.action.action, item.ID);
}
setConfirmDialog({ open: false, action: null });
};
const handleCancel = () => {
setConfirmDialog({ open: false, action: null });
};
const getVisibleButtons = () => {
if (!item || userId === null) return { primary: [], secondary: [], submitter: [], reviewer: [], admin: [] };
// Define a type for the button
type ReviewButton = {
action: ReviewAction;
color: "primary" | "error" | "success" | "info" | "warning";
variant?: "contained" | "outlined";
isPrimary?: boolean;
};
const buttons: ReviewButton[] = [];
const primaryButtons: ReviewButton[] = [];
const secondaryButtons: ReviewButton[] = [];
const submitterButtons: ReviewButton[] = [];
const reviewerButtons: ReviewButton[] = [];
const adminButtons: ReviewButton[] = [];
const is_submitter = userId === item.Submitter;
const status = item.StatusID;
@@ -59,133 +174,215 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
const uploadRole = type === "submission" ? RolesConstants.SubmissionUpload : RolesConstants.MapfixUpload;
const releaseRole = type === "submission" ? RolesConstants.SubmissionRelease : RolesConstants.MapfixRelease;
// Submitter actions
if (is_submitter) {
if (StatusMatches(status, [Status.UnderConstruction, Status.ChangesRequested])) {
buttons.push({
submitterButtons.push({
action: ReviewActions.Submit,
color: "primary"
color: "success"
});
}
if (StatusMatches(status, [Status.Submitted, Status.ChangesRequested])) {
buttons.push({
submitterButtons.push({
action: ReviewActions.Revoke,
color: "error"
color: "warning",
variant: "outlined"
});
}
if (status === Status.Submitting) {
buttons.push({
adminButtons.push({
action: ReviewActions.ResetSubmitting,
color: "warning"
color: "error",
variant: "outlined"
});
}
}
// Buttons for review role
// Reviewer actions
if (hasRole(roles, reviewRole)) {
if (status === Status.Submitted && !is_submitter) {
buttons.push(
{
action: ReviewActions.Accept,
color: "success"
},
{
action: ReviewActions.Reject,
color: "error"
}
);
reviewerButtons.push({
action: ReviewActions.Accept,
color: "success"
});
reviewerButtons.push({
action: ReviewActions.Reject,
color: "error",
variant: "outlined"
});
}
if (status === Status.AcceptedUnvalidated) {
buttons.push({
reviewerButtons.push({
action: ReviewActions.Validate,
color: "info"
color: "primary"
});
}
if (status === Status.Validating) {
buttons.push({
adminButtons.push({
action: ReviewActions.ResetValidating,
color: "warning"
color: "error",
variant: "outlined"
});
}
if (StatusMatches(status, [Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) {
buttons.push({
reviewerButtons.push({
action: ReviewActions.RequestChanges,
color: "warning"
color: "warning",
variant: "outlined"
});
}
if (status === Status.ChangesRequested) {
buttons.push({
adminButtons.push({
action: ReviewActions.SubmitUnchecked,
color: "warning"
color: "warning",
variant: "outlined"
});
// button only exists for submissions
// submitter has normal submit button
if (type === "submission" && !is_submitter) {
buttons.push({
adminButtons.push({
action: ReviewActions.AdminSubmit,
color: "primary"
color: "info",
variant: "outlined"
});
}
}
}
// Buttons for upload role
// Upload role actions
if (hasRole(roles, uploadRole)) {
if (status === Status.Validated) {
buttons.push({
reviewerButtons.push({
action: ReviewActions.Upload,
color: "success"
});
}
if (status === Status.Uploading) {
buttons.push({
adminButtons.push({
action: ReviewActions.ResetUploading,
color: "warning"
color: "error",
variant: "outlined"
});
}
}
// Buttons for release role
// Release role actions
if (hasRole(roles, releaseRole)) {
// submissions do not have a release button
if (type === "mapfix" && status === Status.Uploaded) {
buttons.push({
reviewerButtons.push({
action: ReviewActions.Release,
color: "success"
});
}
if (status === Status.Releasing) {
buttons.push({
adminButtons.push({
action: ReviewActions.ResetReleasing,
color: "warning"
color: "error",
variant: "outlined"
});
}
}
return buttons;
return {
primary: primaryButtons,
secondary: secondaryButtons,
submitter: submitterButtons,
reviewer: reviewerButtons,
admin: adminButtons
};
};
const buttons = getVisibleButtons();
const hasAnyButtons = buttons.submitter.length > 0 || buttons.reviewer.length > 0 || buttons.admin.length > 0;
if (!hasAnyButtons) return null;
const ActionCard = ({ title, actions, isFirst = false }: { title: string; actions: any[]; isFirst?: boolean }) => {
if (actions.length === 0) return null;
return (
<Box sx={{ mt: isFirst ? 0 : 3 }}>
<Typography
variant="caption"
fontWeight={600}
color="text.secondary"
sx={{
textTransform: 'uppercase',
letterSpacing: '0.5px',
mb: 1.5,
display: 'block'
}}
>
{title}
</Typography>
<Stack spacing={1}>
{actions.map((button, index) => (
<Button
key={index}
variant="contained"
color={button.color}
fullWidth
size="large"
onClick={() => handleButtonClick(button.action)}
sx={{
textTransform: 'none',
fontSize: '1rem',
fontWeight: 600,
py: 1.5
}}
>
{button.action.name}
</Button>
))}
</Stack>
</Box>
);
};
return (
<Stack spacing={2} sx={{ mb: 3 }}>
{getVisibleButtons().map((button, index) => (
<Button
key={index}
variant="contained"
color={button.color}
fullWidth
onClick={() => onClick(button.action.action, item.ID)}
>
{button.action.name}
</Button>
))}
</Stack>
<>
<Box sx={{ mb: 3 }}>
<ActionCard title="Your Actions" actions={buttons.submitter} isFirst={true} />
<ActionCard title="Review Actions" actions={buttons.reviewer} isFirst={buttons.submitter.length === 0} />
<ActionCard title="Admin Actions" actions={buttons.admin} isFirst={buttons.submitter.length === 0 && buttons.reviewer.length === 0} />
</Box>
{/* Confirmation Dialog */}
<Dialog
open={confirmDialog.open}
onClose={handleCancel}
maxWidth="xs"
fullWidth
>
<DialogTitle sx={{ pb: 1 }}>
{confirmDialog.action?.confirmTitle || confirmDialog.action?.name}
</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary">
{confirmDialog.action?.confirmMessage || "Are you sure you want to proceed?"}
</Typography>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={handleCancel} color="inherit">
Cancel
</Button>
<Button
onClick={handleConfirm}
variant="contained"
color="primary"
autoFocus
>
Confirm
</Button>
</DialogActions>
</Dialog>
</>
);
};