Validate Asset Version + Website QoL + Script Names Fix #193
@@ -84,6 +84,7 @@ func (svc *Service) ListScripts(ctx context.Context, params api.ListScriptsParam
|
||||
for _, item := range items {
|
||||
resp = append(resp, api.Script{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Hash: model.HashFormat(uint64(item.Hash)),
|
||||
Source: item.Source,
|
||||
ResourceType: int32(item.ResourceType),
|
||||
|
||||
@@ -71,6 +71,7 @@ func (svc *Service) ListScripts(ctx context.Context, params api.ListScriptsParam
|
||||
for _, item := range items {
|
||||
resp = append(resp, api.Script{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Hash: model.HashFormat(uint64(item.Hash)),
|
||||
Source: item.Source,
|
||||
ResourceType: int32(item.ResourceType),
|
||||
|
||||
@@ -34,6 +34,12 @@ fn hash_source(source:&str)->String{
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ModelInfoDownload(rbx_asset::cloud::GetError),
|
||||
CreatorTypeMustBeUser,
|
||||
RevisionMismatch{
|
||||
current:u64,
|
||||
submitted:u64,
|
||||
},
|
||||
ScriptFlaggedIllegalKeyword(String),
|
||||
ScriptBlocked(Option<submissions_api::types::ScriptID>),
|
||||
ScriptNotYetReviewed(Option<submissions_api::types::ScriptID>),
|
||||
@@ -90,6 +96,24 @@ impl From<crate::nats_types::ValidateSubmissionRequest> for ValidateRequest{
|
||||
|
||||
impl crate::message_handler::MessageHandler{
|
||||
pub async fn validate_inner(&self,validate_info:ValidateRequest)->Result<(),Error>{
|
||||
// discover asset creator and latest version
|
||||
let info=self.cloud_context.get_asset_info(
|
||||
rbx_asset::cloud::GetAssetLatestRequest{asset_id:validate_info.ModelID}
|
||||
).await.map_err(Error::ModelInfoDownload)?;
|
||||
|
||||
// reject models created by a group
|
||||
let rbx_asset::cloud::Creator::userId(_user_id)=info.creationContext.creator else{
|
||||
return Err(Error::CreatorTypeMustBeUser);
|
||||
};
|
||||
|
||||
// Has the map been updated since it was submitted?
|
||||
if info.revisionId!=validate_info.ModelVersion{
|
||||
return Err(Error::RevisionMismatch{
|
||||
current:info.revisionId,
|
||||
submitted:validate_info.ModelVersion,
|
||||
});
|
||||
}
|
||||
|
||||
// download the map model
|
||||
let maybe_gzip=download_asset_version(&self.cloud_context,rbx_asset::cloud::GetAssetVersionRequest{
|
||||
asset_id:validate_info.ModelID,
|
||||
|
||||
@@ -38,21 +38,29 @@ export default function CommentsTabPanel({
|
||||
{activeTab === 0 && (
|
||||
<>
|
||||
<Stack spacing={2} sx={{ mb: 3 }}>
|
||||
{commentEvents.map((event, index) => (
|
||||
<CommentItem
|
||||
key={index}
|
||||
event={event}
|
||||
validatorUser={validatorUser}
|
||||
/>
|
||||
))}
|
||||
{commentEvents.length > 0 ? (
|
||||
commentEvents.map((event, index) => (
|
||||
<CommentItem
|
||||
key={index}
|
||||
event={event}
|
||||
validatorUser={validatorUser}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Box sx={{ textAlign: 'center', py: 2, color: 'text.secondary' }}>
|
||||
No Comments
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<CommentInput
|
||||
newComment={newComment}
|
||||
setNewComment={setNewComment}
|
||||
handleCommentSubmit={handleCommentSubmit}
|
||||
userId={userId}
|
||||
/>
|
||||
{userId !== null && (
|
||||
<CommentInput
|
||||
newComment={newComment}
|
||||
setNewComment={setNewComment}
|
||||
handleCommentSubmit={handleCommentSubmit}
|
||||
userId={userId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -2,7 +2,8 @@ import React from 'react';
|
||||
import { Button, Stack } from '@mui/material';
|
||||
import {MapfixInfo } from "@/app/ts/Mapfix";
|
||||
import {hasRole, Roles, RolesConstants} from "@/app/ts/Roles";
|
||||
import {SubmissionInfo, SubmissionStatus} from "@/app/ts/Submission";
|
||||
import {SubmissionInfo} from "@/app/ts/Submission";
|
||||
import {Status, StatusMatches} from "@/app/ts/Status";
|
||||
|
||||
interface ReviewAction {
|
||||
name: string,
|
||||
@@ -51,38 +52,18 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
||||
const is_submitter = userId === item.Submitter;
|
||||
const status = item.StatusID;
|
||||
|
||||
// Helper function to check status regardless of which enum type it is
|
||||
const statusMatches = (statusValues: number[]) => {
|
||||
return statusValues.includes(status);
|
||||
};
|
||||
|
||||
// Create status constants that work with both types
|
||||
const Status = {
|
||||
UnderConstruction: SubmissionStatus.UnderConstruction,
|
||||
ChangesRequested: SubmissionStatus.ChangesRequested,
|
||||
Submitting: SubmissionStatus.Submitting,
|
||||
Submitted: SubmissionStatus.Submitted,
|
||||
AcceptedUnvalidated: SubmissionStatus.AcceptedUnvalidated,
|
||||
Validating: SubmissionStatus.Validating,
|
||||
Validated: SubmissionStatus.Validated,
|
||||
Uploading: SubmissionStatus.Uploading,
|
||||
Uploaded: SubmissionStatus.Uploaded,
|
||||
Rejected: SubmissionStatus.Rejected,
|
||||
Release: SubmissionStatus.Released
|
||||
};
|
||||
|
||||
const reviewRole = type === "submission" ? RolesConstants.SubmissionReview : RolesConstants.MapfixReview;
|
||||
const uploadRole = type === "submission" ? RolesConstants.SubmissionUpload : RolesConstants.MapfixUpload;
|
||||
|
||||
if (is_submitter) {
|
||||
if (statusMatches([Status.UnderConstruction, Status.ChangesRequested])) {
|
||||
if (StatusMatches(status, [Status.UnderConstruction, Status.ChangesRequested])) {
|
||||
buttons.push({
|
||||
action: ReviewActions.Submit,
|
||||
color: "primary"
|
||||
});
|
||||
}
|
||||
|
||||
if (statusMatches([Status.Submitted, Status.ChangesRequested])) {
|
||||
if (StatusMatches(status, [Status.Submitted, Status.ChangesRequested])) {
|
||||
buttons.push({
|
||||
action: ReviewActions.Revoke,
|
||||
color: "error"
|
||||
@@ -119,7 +100,7 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (statusMatches([Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) {
|
||||
if (StatusMatches(status, [Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) {
|
||||
buttons.push({
|
||||
action: ReviewActions.RequestChanges,
|
||||
color: "warning"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {Typography, Box, Avatar} from "@mui/material";
|
||||
import {Typography, Box, Avatar, keyframes} from "@mui/material";
|
||||
import { StatusChip } from "@/app/_components/statusChip";
|
||||
import { SubmissionStatus } from "@/app/ts/Submission";
|
||||
import { MapfixStatus } from "@/app/ts/Mapfix";
|
||||
import {Status, StatusMatches} from "@/app/ts/Status";
|
||||
|
||||
type StatusIdType = SubmissionStatus | MapfixStatus;
|
||||
|
||||
@@ -13,13 +14,45 @@ interface ReviewItemHeaderProps {
|
||||
}
|
||||
|
||||
export const ReviewItemHeader = ({ displayName, statusId, creator, submitterId }: ReviewItemHeaderProps) => {
|
||||
const isProcessing = StatusMatches(statusId, [Status.Validating, Status.Uploading, Status.Submitting]);
|
||||
|
||||
const pulse = keyframes`
|
||||
0%, 100% { opacity: 0.2; transform: scale(0.8); }
|
||||
50% { opacity: 1; transform: scale(1); }
|
||||
`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
{displayName}
|
||||
</Typography>
|
||||
<StatusChip status={statusId as number} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{isProcessing && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
mr: 0.5,
|
||||
height: 24
|
||||
}}>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<Box
|
||||
key={i}
|
||||
sx={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'primary.main',
|
||||
animation: `${pulse} 1.4s ease-in-out infinite`,
|
||||
animationDelay: `${i * 0.2}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<StatusChip status={statusId as number} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
|
||||
122
web/src/app/hooks/useReviewData.ts
Normal file
122
web/src/app/hooks/useReviewData.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import {useState, useEffect, useCallback} from "react";
|
||||
import {Roles, RolesConstants} from "@/app/ts/Roles";
|
||||
import {AuditEvent} from "@/app/ts/AuditEvent";
|
||||
import {Status, StatusMatches} from "@/app/ts/Status";
|
||||
import {MapfixInfo} from "@/app/ts/Mapfix";
|
||||
import {SubmissionInfo} from "@/app/ts/Submission";
|
||||
|
||||
type ReviewItemType = "submissions" | "mapfixes";
|
||||
type ReviewData = MapfixInfo | SubmissionInfo;
|
||||
|
||||
interface UseReviewDataProps {
|
||||
itemType: ReviewItemType;
|
||||
itemId: string | number;
|
||||
}
|
||||
|
||||
interface UseReviewDataResult {
|
||||
data: ReviewData | null;
|
||||
auditEvents: AuditEvent[];
|
||||
roles: Roles;
|
||||
user: number | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refreshData: (skipLoadingState?: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReviewDataResult {
|
||||
const [data, setData] = useState<ReviewData | null>(null);
|
||||
const [auditEvents, setAuditEvents] = useState<AuditEvent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [user, setUser] = useState<number | null>(null);
|
||||
const [roles, setRoles] = useState<Roles>(RolesConstants.Empty);
|
||||
|
||||
const fetchData = useCallback(async (skipLoadingState = false) => {
|
||||
try {
|
||||
if (!skipLoadingState) {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const [reviewData, auditData] = await Promise.all([
|
||||
fetch(`/api/${itemType}/${itemId}`).then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${itemType.slice(0, -1)}: ${res.status}`);
|
||||
return res.json();
|
||||
}),
|
||||
fetch(`/api/${itemType}/${itemId}/audit-events?Page=1&Limit=100`).then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch audit events: ${res.status}`);
|
||||
return res.json();
|
||||
})
|
||||
]);
|
||||
|
||||
setData(reviewData);
|
||||
setAuditEvents(auditData);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching critical ${itemType} data:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
const rolesResponse = await fetch("/api/session/roles");
|
||||
if (rolesResponse.ok) {
|
||||
const rolesData = await rolesResponse.json();
|
||||
setRoles(rolesData.Roles);
|
||||
} else {
|
||||
console.warn(`Failed to fetch roles: ${rolesResponse.status}`);
|
||||
setRoles(RolesConstants.Empty);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error fetching roles data:", error);
|
||||
setRoles(RolesConstants.Empty);
|
||||
}
|
||||
|
||||
try {
|
||||
const userResponse = await fetch("/api/session/user");
|
||||
if (userResponse.ok) {
|
||||
const userData = await userResponse.json();
|
||||
setUser(userData.UserID);
|
||||
} else {
|
||||
console.warn(`Failed to fetch user: ${userResponse.status}`);
|
||||
setUser(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error fetching user data:", error);
|
||||
setUser(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching review data:", error);
|
||||
setError(error instanceof Error ? error.message : `Failed to load ${itemType.slice(0, -1)} details`);
|
||||
} finally {
|
||||
if (!skipLoadingState) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [itemId, itemType]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (StatusMatches(data.StatusID, [Status.Uploading, Status.Submitting, Status.Validating])) {
|
||||
const intervalId = setInterval(() => {
|
||||
fetchData(true);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}
|
||||
}
|
||||
}, [data, itemType, fetchData]);
|
||||
|
||||
return {
|
||||
data,
|
||||
auditEvents,
|
||||
roles,
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
refreshData: fetchData
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import {AuditEvent} from "@/app/ts/AuditEvent";
|
||||
import { Roles, RolesConstants } from "@/app/ts/Roles";
|
||||
import Webpage from "@/app/_components/webpage";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import {useState, useEffect, useCallback} from "react";
|
||||
import {useState} from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
// MUI Components
|
||||
@@ -25,10 +23,9 @@ import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
||||
import CommentsAndAuditSection from "@/app/_components/comments/CommentsAndAuditSection";
|
||||
import {ReviewItem} from "@/app/_components/review/ReviewItem";
|
||||
import {ErrorDisplay} from "@/app/_components/ErrorDisplay";
|
||||
import {MapfixInfo} from "@/app/ts/Mapfix";
|
||||
import ReviewButtons from "@/app/_components/review/ReviewButtons";
|
||||
|
||||
// Review action definitions
|
||||
import {useReviewData} from "@/app/hooks/useReviewData";
|
||||
import {MapfixInfo} from "@/app/ts/Mapfix";
|
||||
|
||||
interface SnackbarState {
|
||||
open: boolean;
|
||||
@@ -39,14 +36,7 @@ interface SnackbarState {
|
||||
export default function MapfixDetailsPage() {
|
||||
const { mapfixId } = useParams<{ mapfixId: string }>();
|
||||
const router = useRouter();
|
||||
|
||||
const [mapfix, setMapfix] = useState<MapfixInfo | null>(null);
|
||||
const [auditEvents, setAuditEvents] = useState<AuditEvent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [newComment, setNewComment] = useState("");
|
||||
const [user, setUser] = useState<number|null>(null);
|
||||
const [roles, setRoles] = useState<Roles>(RolesConstants.Empty);
|
||||
const [showBeforeImage, setShowBeforeImage] = useState(false);
|
||||
const [snackbar, setSnackbar] = useState<SnackbarState>({
|
||||
open: false,
|
||||
@@ -70,50 +60,19 @@ export default function MapfixDetailsPage() {
|
||||
|
||||
const validatorUser = 9223372036854776000;
|
||||
|
||||
const fetchData = useCallback(async (skipLoadingState = false) => {
|
||||
try {
|
||||
if (!skipLoadingState) {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
const [mapfixData, auditData, rolesData, userData] = await Promise.all([
|
||||
fetch(`/api/mapfixes/${mapfixId}`).then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch mapfix: ${res.status}`);
|
||||
return res.json();
|
||||
}),
|
||||
fetch(`/api/mapfixes/${mapfixId}/audit-events?Page=1&Limit=100`).then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch audit events: ${res.status}`);
|
||||
return res.json();
|
||||
}),
|
||||
fetch("/api/session/roles").then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch roles: ${res.status}`);
|
||||
return res.json();
|
||||
}),
|
||||
fetch("/api/session/user").then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch user: ${res.status}`);
|
||||
return res.json();
|
||||
})
|
||||
]);
|
||||
|
||||
setMapfix(mapfixData);
|
||||
setAuditEvents(auditData);
|
||||
setRoles(rolesData.Roles);
|
||||
setUser(userData.UserID);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
setError(error instanceof Error ? error.message : "Failed to load mapfix details");
|
||||
} finally {
|
||||
if (!skipLoadingState) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [mapfixId]);
|
||||
|
||||
// Fetch mapfix data and audit events
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
const {
|
||||
data: mapfixData,
|
||||
auditEvents,
|
||||
roles,
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
refreshData
|
||||
} = useReviewData({
|
||||
itemType: 'mapfixes',
|
||||
itemId: mapfixId
|
||||
});
|
||||
const mapfix = mapfixData as MapfixInfo;
|
||||
|
||||
// Handle review button actions
|
||||
async function handleReviewAction(action: string, mapfixId: number) {
|
||||
@@ -134,14 +93,14 @@ export default function MapfixDetailsPage() {
|
||||
showSnackbar(`Successfully completed action: ${action}`, "success");
|
||||
|
||||
// Reload data instead of refreshing the page
|
||||
fetchData(true);
|
||||
await refreshData(true);
|
||||
} catch (error) {
|
||||
console.error("Error updating mapfix status:", error);
|
||||
showSnackbar(error instanceof Error ? error.message : "Failed to update mapfix", 'error');
|
||||
|
||||
|
||||
// Reload data instead of refreshing the page
|
||||
fetchData(true);
|
||||
await refreshData(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,15 +131,10 @@ export default function MapfixDetailsPage() {
|
||||
// Clear comment input
|
||||
setNewComment("");
|
||||
|
||||
// Refresh audit events to show the new comment
|
||||
const auditData = await fetch(`/api/mapfixes/${mapfixId}/audit-events?Page=1&Limit=100`);
|
||||
if (auditData.ok) {
|
||||
const updatedAuditEvents = await auditData.json();
|
||||
setAuditEvents(updatedAuditEvents);
|
||||
}
|
||||
await refreshData(true);
|
||||
} catch (error) {
|
||||
console.error("Error submitting comment:", error);
|
||||
setError(error instanceof Error ? error.message : "Failed to submit comment");
|
||||
showSnackbar(error instanceof Error ? error.message : "Failed to submit comment", "error");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { SubmissionInfo } from "@/app/ts/Submission";
|
||||
import {AuditEvent} from "@/app/ts/AuditEvent";
|
||||
import { Roles, RolesConstants } from "@/app/ts/Roles";
|
||||
import Webpage from "@/app/_components/webpage";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import {useState, useEffect, useCallback} from "react";
|
||||
import {useState} from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
// MUI Components
|
||||
@@ -27,6 +23,8 @@ import CommentsAndAuditSection from "@/app/_components/comments/CommentsAndAudit
|
||||
import {ReviewItem} from "@/app/_components/review/ReviewItem";
|
||||
import {ErrorDisplay} from "@/app/_components/ErrorDisplay";
|
||||
import ReviewButtons from "@/app/_components/review/ReviewButtons";
|
||||
import {useReviewData} from "@/app/hooks/useReviewData";
|
||||
import {SubmissionInfo} from "@/app/ts/Submission";
|
||||
|
||||
interface SnackbarState {
|
||||
open: boolean;
|
||||
@@ -37,14 +35,7 @@ interface SnackbarState {
|
||||
export default function SubmissionDetailsPage() {
|
||||
const { submissionId } = useParams<{ submissionId: string }>();
|
||||
const router = useRouter();
|
||||
|
||||
const [submission, setSubmission] = useState<SubmissionInfo | null>(null);
|
||||
const [auditEvents, setAuditEvents] = useState<AuditEvent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [newComment, setNewComment] = useState("");
|
||||
const [user, setUser] = useState<number|null>(null);
|
||||
const [roles, setRoles] = useState<Roles>(RolesConstants.Empty);
|
||||
const [snackbar, setSnackbar] = useState<SnackbarState>({
|
||||
open: false,
|
||||
message: null,
|
||||
@@ -68,50 +59,19 @@ export default function SubmissionDetailsPage() {
|
||||
|
||||
const validatorUser = 9223372036854776000;
|
||||
|
||||
const fetchData = useCallback(async (skipLoadingState = false) => {
|
||||
try {
|
||||
if (!skipLoadingState) {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
const [submissionData, auditData, rolesData, userData] = await Promise.all([
|
||||
fetch(`/api/submissions/${submissionId}`).then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch submission: ${res.status}`);
|
||||
return res.json();
|
||||
}),
|
||||
fetch(`/api/submissions/${submissionId}/audit-events?Page=1&Limit=100`).then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch audit events: ${res.status}`);
|
||||
return res.json();
|
||||
}),
|
||||
fetch("/api/session/roles").then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch roles: ${res.status}`);
|
||||
return res.json();
|
||||
}),
|
||||
fetch("/api/session/user").then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch user: ${res.status}`);
|
||||
return res.json();
|
||||
})
|
||||
]);
|
||||
|
||||
setSubmission(submissionData);
|
||||
setAuditEvents(auditData);
|
||||
setRoles(rolesData.Roles);
|
||||
setUser(userData.UserID);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
setError(error instanceof Error ? error.message : "Failed to load submission details");
|
||||
} finally {
|
||||
if (!skipLoadingState) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [submissionId]);
|
||||
|
||||
// Fetch submission data and audit events
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
const {
|
||||
data: submissionData,
|
||||
auditEvents,
|
||||
roles,
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
refreshData
|
||||
} = useReviewData({
|
||||
itemType: 'submissions',
|
||||
itemId: submissionId
|
||||
});
|
||||
const submission = submissionData as SubmissionInfo;
|
||||
|
||||
// Handle review button actions
|
||||
async function handleReviewAction(action: string, submissionId: number) {
|
||||
@@ -132,14 +92,14 @@ export default function SubmissionDetailsPage() {
|
||||
showSnackbar(`Successfully completed action: ${action}`, "success");
|
||||
|
||||
// Reload data instead of refreshing the page
|
||||
fetchData(true);
|
||||
await refreshData(true);
|
||||
} catch (error) {
|
||||
console.error("Error updating submission status:", error);
|
||||
showSnackbar(error instanceof Error ? error.message : "Failed to update submission", 'error');
|
||||
|
||||
|
||||
// Reload data instead of refreshing the page
|
||||
fetchData(true);
|
||||
await refreshData(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,15 +130,10 @@ export default function SubmissionDetailsPage() {
|
||||
// Clear comment input
|
||||
setNewComment("");
|
||||
|
||||
// Refresh audit events to show the new comment
|
||||
const auditData = await fetch(`/api/submissions/${submissionId}/audit-events?Page=1&Limit=100`);
|
||||
if (auditData.ok) {
|
||||
const updatedAuditEvents = await auditData.json();
|
||||
setAuditEvents(updatedAuditEvents);
|
||||
}
|
||||
await refreshData(true);
|
||||
} catch (error) {
|
||||
console.error("Error submitting comment:", error);
|
||||
setError(error instanceof Error ? error.message : "Failed to submit comment");
|
||||
showSnackbar(error instanceof Error ? error.message : "Failed to submit comment", "error");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { errorImageResponse } from '@/app/lib/errorImageResponse';
|
||||
|
||||
const cache = new Map<number, { buffer: Buffer; expires: number }>();
|
||||
const CACHE_TTL = 15 * 60 * 1000;
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of cache.entries()) {
|
||||
if (value.expires <= now) {
|
||||
cache.delete(key);
|
||||
}
|
||||
}
|
||||
}, 60 * 5 * 1000);
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ assetId: number }> }
|
||||
): Promise<NextResponse> {
|
||||
const { assetId } = await context.params;
|
||||
|
||||
|
||||
if (!assetId) {
|
||||
return errorImageResponse(400, {
|
||||
message: "Missing asset ID",
|
||||
@@ -39,19 +27,6 @@ export async function GET(
|
||||
}
|
||||
} catch { }
|
||||
|
||||
const now = Date.now();
|
||||
const cached = cache.get(finalAssetId);
|
||||
|
||||
if (cached && cached.expires > now) {
|
||||
return new NextResponse(cached.buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': cached.buffer.length.toString(),
|
||||
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://thumbnails.roblox.com/v1/assets?format=png&size=512x512&assetIds=${finalAssetId}`
|
||||
@@ -70,26 +45,12 @@ export async function GET(
|
||||
})
|
||||
}
|
||||
|
||||
const imageResponse = await fetch(imageUrl);
|
||||
if (!imageResponse.ok) {
|
||||
throw new Error(`Failed to fetch the image [${imageResponse.status}]`)
|
||||
}
|
||||
// Redirect to the actual image URL instead of proxying
|
||||
return NextResponse.redirect(imageUrl);
|
||||
|
||||
const arrayBuffer = await imageResponse.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
cache.set(finalAssetId, { buffer, expires: now + CACHE_TTL });
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': buffer.length.toString(),
|
||||
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
return errorImageResponse(500, {
|
||||
message: `Failed to fetch or process thumbnail: ${err}`,
|
||||
})
|
||||
message: `Failed to fetch thumbnail URL: ${err}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -32,24 +32,13 @@ export async function GET(
|
||||
);
|
||||
}
|
||||
|
||||
const imageResponse = await fetch(imageUrl);
|
||||
if (!imageResponse.ok) {
|
||||
throw new Error('Failed to fetch the image');
|
||||
}
|
||||
// Redirect to the image URL instead of proxying
|
||||
return NextResponse.redirect(imageUrl);
|
||||
|
||||
const arrayBuffer = await imageResponse.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': buffer.length.toString(),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch or process avatar headshot' },
|
||||
{ error: 'Failed to fetch avatar headshot URL' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
web/src/app/ts/Status.ts
Normal file
19
web/src/app/ts/Status.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {SubmissionStatus} from "@/app/ts/Submission";
|
||||
|
||||
export const Status = {
|
||||
UnderConstruction: SubmissionStatus.UnderConstruction,
|
||||
ChangesRequested: SubmissionStatus.ChangesRequested,
|
||||
Submitting: SubmissionStatus.Submitting,
|
||||
Submitted: SubmissionStatus.Submitted,
|
||||
AcceptedUnvalidated: SubmissionStatus.AcceptedUnvalidated,
|
||||
Validating: SubmissionStatus.Validating,
|
||||
Validated: SubmissionStatus.Validated,
|
||||
Uploading: SubmissionStatus.Uploading,
|
||||
Uploaded: SubmissionStatus.Uploaded,
|
||||
Rejected: SubmissionStatus.Rejected,
|
||||
Release: SubmissionStatus.Released
|
||||
};
|
||||
|
||||
export const StatusMatches = (status: number, statusValues: number[]) => {
|
||||
return statusValues.includes(status);
|
||||
};
|
||||
Reference in New Issue
Block a user