From e41d34dd3dac668567fec00562dd610308412fe5 Mon Sep 17 00:00:00 2001 From: itzaname Date: Sun, 28 Dec 2025 00:34:58 +0000 Subject: [PATCH] Group buttons and add confirmation dialogues (#310) Reviewer: image.png Submitter: image.png image.png Admin: image.png image.png Confirmation Dialogue: image.png Example where both groups show up: image.png Reviewed-on: https://git.itzana.me/StrafesNET/maps-service/pulls/310 Reviewed-by: Rhys Lloyd Co-authored-by: itzaname Co-committed-by: itzaname --- .../app/_components/review/ReviewButtons.tsx | 345 ++++++++++++++---- 1 file changed, 271 insertions(+), 74 deletions(-) diff --git a/web/src/app/_components/review/ReviewButtons.tsx b/web/src/app/_components/review/ReviewButtons.tsx index 2af8273..40c91b8 100644 --- a/web/src/app/_components/review/ReviewButtons.tsx +++ b/web/src/app/_components/review/ReviewButtons.tsx @@ -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.", + 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.", + 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 = ({ @@ -42,16 +127,46 @@ const ReviewButtons: React.FC = ({ 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 = ({ 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 ( + + + {title} + + + {actions.map((button, index) => ( + + ))} + + + ); }; return ( - - {getVisibleButtons().map((button, index) => ( - - ))} - + <> + + + + + + + {/* Confirmation Dialog */} + + + {confirmDialog.action?.confirmTitle || confirmDialog.action?.name} + + + + {confirmDialog.action?.confirmMessage || "Are you sure you want to proceed?"} + + + + + + + + ); };