Group buttons and add confirmation dialogues (#310)
Reviewer: <img width="409" alt="image.png" src="attachments/a090c61e-a2d8-4685-ae64-547851d1ee84"> Submitter: <img width="404" alt="image.png" src="attachments/9205a438-1f1f-4af4-b9a0-6a8d56580afa"> <img width="411" alt="image.png" src="attachments/7ae8115b-3376-4306-b9b9-acc12226abb3"> Admin: <img width="392" alt="image.png" src="attachments/07a182d1-5375-4195-bfda-c14f09469cbe"> <img width="388" alt="image.png" src="attachments/ce82017d-5c1d-4a93-9247-9b5608f9030e"> Confirmation Dialogue: <img width="545" alt="image.png" src="attachments/1efff8be-1d41-429e-8c6e-3d36b7dad128"> Example where both groups show up: <img width="404" alt="image.png" src="attachments/b0ca4be2-7c58-4c0c-9a5f-dcd89e23b08f"> Reviewed-on: #310 Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me> Co-authored-by: itzaname <me@sliving.io> Co-committed-by: itzaname <me@sliving.io>
This commit was merged in pull request #310.
This commit is contained in:
@@ -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<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user