diff --git a/pkg/service/scripts.go b/pkg/service/scripts.go index e1f35de..47da522 100644 --- a/pkg/service/scripts.go +++ b/pkg/service/scripts.go @@ -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), diff --git a/pkg/service_internal/scripts.go b/pkg/service_internal/scripts.go index 6e19c98..3de5264 100644 --- a/pkg/service_internal/scripts.go +++ b/pkg/service_internal/scripts.go @@ -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), diff --git a/validation/src/validator.rs b/validation/src/validator.rs index e5ef087..7912b99 100644 --- a/validation/src/validator.rs +++ b/validation/src/validator.rs @@ -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), ScriptNotYetReviewed(Option), @@ -90,6 +96,24 @@ impl From 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, diff --git a/web/src/app/_components/comments/CommentsTabPanel.tsx b/web/src/app/_components/comments/CommentsTabPanel.tsx index 7318b35..d8d9ace 100644 --- a/web/src/app/_components/comments/CommentsTabPanel.tsx +++ b/web/src/app/_components/comments/CommentsTabPanel.tsx @@ -38,21 +38,29 @@ export default function CommentsTabPanel({ {activeTab === 0 && ( <> - {commentEvents.map((event, index) => ( - - ))} + {commentEvents.length > 0 ? ( + commentEvents.map((event, index) => ( + + )) + ) : ( + + No Comments + + )} - + {userId !== null && ( + + )} )} diff --git a/web/src/app/_components/review/ReviewButtons.tsx b/web/src/app/_components/review/ReviewButtons.tsx index 9f59138..17c0c3c 100644 --- a/web/src/app/_components/review/ReviewButtons.tsx +++ b/web/src/app/_components/review/ReviewButtons.tsx @@ -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 = ({ 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 = ({ }); } - 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" diff --git a/web/src/app/_components/review/ReviewItemHeader.tsx b/web/src/app/_components/review/ReviewItemHeader.tsx index 50ce78f..f04cf6c 100644 --- a/web/src/app/_components/review/ReviewItemHeader.tsx +++ b/web/src/app/_components/review/ReviewItemHeader.tsx @@ -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 ( <> {displayName} - + + {isProcessing && ( + + {[0, 1, 2].map((i) => ( + + ))} + + )} + + diff --git a/web/src/app/hooks/useReviewData.ts b/web/src/app/hooks/useReviewData.ts new file mode 100644 index 0000000..1e1895b --- /dev/null +++ b/web/src/app/hooks/useReviewData.ts @@ -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; +} + +export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReviewDataResult { + const [data, setData] = useState(null); + const [auditEvents, setAuditEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [user, setUser] = useState(null); + const [roles, setRoles] = useState(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 + }; +} \ No newline at end of file diff --git a/web/src/app/mapfixes/[mapfixId]/page.tsx b/web/src/app/mapfixes/[mapfixId]/page.tsx index 4af5e69..548fa76 100644 --- a/web/src/app/mapfixes/[mapfixId]/page.tsx +++ b/web/src/app/mapfixes/[mapfixId]/page.tsx @@ -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(null); - const [auditEvents, setAuditEvents] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); const [newComment, setNewComment] = useState(""); - const [user, setUser] = useState(null); - const [roles, setRoles] = useState(RolesConstants.Empty); const [showBeforeImage, setShowBeforeImage] = useState(false); const [snackbar, setSnackbar] = useState({ 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"); } }; diff --git a/web/src/app/submissions/[submissionId]/page.tsx b/web/src/app/submissions/[submissionId]/page.tsx index ec202a3..b967c44 100644 --- a/web/src/app/submissions/[submissionId]/page.tsx +++ b/web/src/app/submissions/[submissionId]/page.tsx @@ -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(null); - const [auditEvents, setAuditEvents] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); const [newComment, setNewComment] = useState(""); - const [user, setUser] = useState(null); - const [roles, setRoles] = useState(RolesConstants.Empty); const [snackbar, setSnackbar] = useState({ 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"); } }; diff --git a/web/src/app/thumbnails/asset/[assetId]/route.tsx b/web/src/app/thumbnails/asset/[assetId]/route.tsx index 7c02e75..338de9b 100644 --- a/web/src/app/thumbnails/asset/[assetId]/route.tsx +++ b/web/src/app/thumbnails/asset/[assetId]/route.tsx @@ -1,24 +1,12 @@ import { NextRequest, NextResponse } from 'next/server'; import { errorImageResponse } from '@/app/lib/errorImageResponse'; -const cache = new Map(); -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 { 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}`, + }) } } \ No newline at end of file diff --git a/web/src/app/thumbnails/user/[userId]/route.tsx b/web/src/app/thumbnails/user/[userId]/route.tsx index 47bef3c..1e08966 100644 --- a/web/src/app/thumbnails/user/[userId]/route.tsx +++ b/web/src/app/thumbnails/user/[userId]/route.tsx @@ -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 } ); } -} +} \ No newline at end of file diff --git a/web/src/app/ts/Status.ts b/web/src/app/ts/Status.ts new file mode 100644 index 0000000..869b72a --- /dev/null +++ b/web/src/app/ts/Status.ts @@ -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); +}; \ No newline at end of file