forked from StrafesNET/maps-service
Compare commits
1 Commits
ai
...
restricted
| Author | SHA1 | Date | |
|---|---|---|---|
|
69438a9d0c
|
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -1297,9 +1297,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rbx_asset"
|
||||
version = "0.3.4"
|
||||
version = "0.3.3"
|
||||
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
|
||||
checksum = "681587db1bd628a7a9344c12008e65e11a10159831e00dcc85c089682cfcf2fb"
|
||||
checksum = "91722b37549ded270f39556194ca03d03e08bd70674d239ec845765ed9e42b7d"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"flate2",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Stage 1: Build
|
||||
FROM registry.itzana.me/docker-proxy/golang:1.24 AS builder
|
||||
FROM docker.io/golang:1.23 AS builder
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /app
|
||||
@@ -14,7 +14,7 @@ COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o service ./cmd/maps-service/service.go
|
||||
|
||||
# Stage 2: Run
|
||||
FROM registry.itzana.me/docker-proxy/alpine:3.21
|
||||
FROM alpine
|
||||
|
||||
# Set up a non-root user for security
|
||||
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||
|
||||
@@ -38,6 +38,7 @@ type Mapfixes interface {
|
||||
IfStatusThenUpdateAndGet(ctx context.Context, id int64, statuses []model.MapfixStatus, values OptionalMap) (model.Mapfix, error)
|
||||
Delete(ctx context.Context, id int64) error
|
||||
List(ctx context.Context, filters OptionalMap, page model.Page, sort ListSort) ([]model.Mapfix, error)
|
||||
ListRestricted(ctx context.Context, filters OptionalMap, page model.Page, sort ListSort, submitter int64) ([]model.Mapfix, error)
|
||||
}
|
||||
|
||||
type Operations interface {
|
||||
@@ -56,6 +57,7 @@ type Submissions interface {
|
||||
IfStatusThenUpdateAndGet(ctx context.Context, id int64, statuses []model.SubmissionStatus, values OptionalMap) (model.Submission, error)
|
||||
Delete(ctx context.Context, id int64) error
|
||||
List(ctx context.Context, filters OptionalMap, page model.Page, sort ListSort) ([]model.Submission, error)
|
||||
ListRestricted(ctx context.Context, filters OptionalMap, page model.Page, sort ListSort, submitter int64) ([]model.Submission, error)
|
||||
}
|
||||
|
||||
type Scripts interface {
|
||||
|
||||
@@ -130,3 +130,40 @@ func (env *Mapfixes) List(ctx context.Context, filters datastore.OptionalMap, pa
|
||||
|
||||
return maps, nil
|
||||
}
|
||||
|
||||
func (env *Mapfixes) ListRestricted(ctx context.Context, filters datastore.OptionalMap, page model.Page, sort datastore.ListSort, submitter int64) ([]model.Mapfix, error) {
|
||||
var maps []model.Mapfix
|
||||
|
||||
db := env.db
|
||||
|
||||
switch sort {
|
||||
case datastore.ListSortDisabled:
|
||||
// No sort
|
||||
break
|
||||
case datastore.ListSortDisplayNameAscending:
|
||||
db=db.Order("display_name ASC")
|
||||
break
|
||||
case datastore.ListSortDisplayNameDescending:
|
||||
db=db.Order("display_name DESC")
|
||||
break
|
||||
case datastore.ListSortDateAscending:
|
||||
db=db.Order("created_at ASC")
|
||||
break
|
||||
case datastore.ListSortDateDescending:
|
||||
db=db.Order("created_at DESC")
|
||||
break
|
||||
default:
|
||||
return nil, datastore.ErrInvalidListSort
|
||||
}
|
||||
|
||||
if err := db.
|
||||
Where(filters.Map()).
|
||||
Where("status_id NOT IN ? OR submitter = ? AND status_id IN ?",PrivateSubmissions,submitter,PrivateSubmissions).
|
||||
Offset(int((page.Number - 1) * page.Size)).
|
||||
Limit(int(page.Size)).
|
||||
Find(&maps).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return maps, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ import (
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var(
|
||||
PrivateSubmissions = []model.SubmissionStatus{
|
||||
model.SubmissionStatusUnderConstruction,
|
||||
model.SubmissionStatusChangesRequested,
|
||||
}
|
||||
)
|
||||
|
||||
type Submissions struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
@@ -130,3 +137,44 @@ func (env *Submissions) List(ctx context.Context, filters datastore.OptionalMap,
|
||||
|
||||
return maps, nil
|
||||
}
|
||||
|
||||
func (env *Submissions) ListRestricted(ctx context.Context, filters datastore.OptionalMap, page model.Page, sort datastore.ListSort, submitter int64) ([]model.Submission, error) {
|
||||
var maps []model.Submission
|
||||
|
||||
db := env.db
|
||||
|
||||
switch sort {
|
||||
case datastore.ListSortDisabled:
|
||||
// No sort
|
||||
break
|
||||
case datastore.ListSortDisplayNameAscending:
|
||||
db=db.Order("display_name ASC")
|
||||
break
|
||||
case datastore.ListSortDisplayNameDescending:
|
||||
db=db.Order("display_name DESC")
|
||||
break
|
||||
case datastore.ListSortDateAscending:
|
||||
db=db.Order("created_at ASC")
|
||||
break
|
||||
case datastore.ListSortDateDescending:
|
||||
db=db.Order("created_at DESC")
|
||||
break
|
||||
default:
|
||||
return nil, datastore.ErrInvalidListSort
|
||||
}
|
||||
|
||||
if err := db.
|
||||
Where(filters.Map()).
|
||||
// In order to see submissions,
|
||||
// at least one of two criteria must be met:
|
||||
// - You are the submitter
|
||||
// - The submission is not under construction / changes requested
|
||||
Where("status_id NOT IN ? OR submitter = ? AND status_id IN ?",PrivateSubmissions,submitter,PrivateSubmissions).
|
||||
Offset(int((page.Number - 1) * page.Size)).
|
||||
Limit(int(page.Size)).
|
||||
Find(&maps).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return maps, nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
submissions-api = { path = "api", features = ["internal"], default-features = false, registry = "strafesnet" }
|
||||
async-nats = "0.40.0"
|
||||
futures = "0.3.31"
|
||||
rbx_asset = { version = "0.3.4", registry = "strafesnet" }
|
||||
rbx_asset = { version = "0.3.3", registry = "strafesnet" }
|
||||
rbx_binary = { version = "0.7.4", registry = "strafesnet"}
|
||||
rbx_dom_weak = { version = "2.9.0", registry = "strafesnet"}
|
||||
rbx_reflection_database = { version = "0.2.12", registry = "strafesnet"}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Using the `rust-musl-builder` as base image, instead of
|
||||
# the official Rust toolchain
|
||||
FROM registry.itzana.me/docker-proxy/clux/muslrust:1.86.0-stable AS chef
|
||||
FROM docker.io/clux/muslrust:stable AS chef
|
||||
USER root
|
||||
RUN cargo install cargo-chef
|
||||
WORKDIR /app
|
||||
@@ -17,7 +17,7 @@ RUN cargo chef cook --release --target x86_64-unknown-linux-musl --recipe-path r
|
||||
COPY . .
|
||||
RUN cargo build --release --target x86_64-unknown-linux-musl --bin maps-validation
|
||||
|
||||
FROM registry.itzana.me/docker-proxy/alpine:3.21 AS runtime
|
||||
FROM docker.io/alpine:latest AS runtime
|
||||
RUN addgroup -S myuser && adduser -S myuser -G myuser
|
||||
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/maps-validation /usr/local/bin/
|
||||
USER myuser
|
||||
|
||||
@@ -20,7 +20,6 @@ pub enum StartupError{
|
||||
NatsConnect(async_nats::ConnectError),
|
||||
NatsGetStream(async_nats::jetstream::context::GetStreamError),
|
||||
NatsConsumer(async_nats::jetstream::stream::ConsumerError),
|
||||
NatsConsumerUpdate(async_nats::jetstream::stream::ConsumerUpdateError),
|
||||
NatsStream(async_nats::jetstream::consumer::StreamError),
|
||||
}
|
||||
impl std::fmt::Display for StartupError{
|
||||
@@ -53,32 +52,17 @@ async fn main()->Result<(),StartupError>{
|
||||
// nats
|
||||
let nats_host=std::env::var("NATS_HOST").expect("NATS_HOST env required");
|
||||
let nats_fut=async{
|
||||
const STREAM_NAME:&str="maptest";
|
||||
const DURABLE_NAME:&str="validation";
|
||||
const FILTER_SUBJECT:&str="maptest.>";
|
||||
|
||||
let nats_config=async_nats::jetstream::consumer::pull::Config{
|
||||
name:Some(DURABLE_NAME.to_owned()),
|
||||
durable_name:Some(DURABLE_NAME.to_owned()),
|
||||
filter_subject:FILTER_SUBJECT.to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let nasty=async_nats::connect(nats_host).await.map_err(StartupError::NatsConnect)?;
|
||||
|
||||
// use nats jetstream
|
||||
let stream=async_nats::jetstream::new(nasty)
|
||||
.get_stream(STREAM_NAME).await.map_err(StartupError::NatsGetStream)?;
|
||||
|
||||
let consumer=stream.get_or_create_consumer(DURABLE_NAME,nats_config.clone()).await.map_err(StartupError::NatsConsumer)?;
|
||||
|
||||
// check if config matches expected config
|
||||
if consumer.cached_info().config.filter_subject!=FILTER_SUBJECT{
|
||||
stream.update_consumer(nats_config).await.map_err(StartupError::NatsConsumerUpdate)?;
|
||||
}
|
||||
|
||||
// only need messages
|
||||
consumer.messages().await.map_err(StartupError::NatsStream)
|
||||
async_nats::jetstream::new(nasty)
|
||||
.get_stream("maptest").await.map_err(StartupError::NatsGetStream)?
|
||||
.get_or_create_consumer("validation",async_nats::jetstream::consumer::pull::Config{
|
||||
name:Some("validation".to_owned()),
|
||||
durable_name:Some("validation".to_owned()),
|
||||
filter_subject:"maptest.>".to_owned(),
|
||||
..Default::default()
|
||||
}).await.map_err(StartupError::NatsConsumer)?
|
||||
.messages().await.map_err(StartupError::NatsStream)
|
||||
};
|
||||
|
||||
let message_handler=message_handler::MessageHandler::new(cookie_context,group_id,api);
|
||||
|
||||
@@ -32,7 +32,7 @@ fn hash_source(source:&str)->String{
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
pub enum ValidateError{
|
||||
ScriptFlaggedIllegalKeyword(String),
|
||||
ScriptBlocked(Option<submissions_api::types::ScriptID>),
|
||||
ScriptNotYetReviewed(Option<submissions_api::types::ScriptID>),
|
||||
@@ -51,12 +51,12 @@ pub enum Error{
|
||||
AssetUpload(rbx_asset::cookie::UploadError),
|
||||
AssetCreate(rbx_asset::cookie::CreateError),
|
||||
}
|
||||
impl std::fmt::Display for Error{
|
||||
impl std::fmt::Display for ValidateError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for Error{}
|
||||
impl std::error::Error for ValidateError{}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
pub struct ValidateRequest{
|
||||
@@ -88,15 +88,15 @@ impl From<crate::nats_types::ValidateSubmissionRequest> for ValidateRequest{
|
||||
}
|
||||
|
||||
impl crate::message_handler::MessageHandler{
|
||||
pub async fn validate_inner(&self,validate_info:ValidateRequest)->Result<(),Error>{
|
||||
pub async fn validate_inner(&self,validate_info:ValidateRequest)->Result<(),ValidateError>{
|
||||
// download map
|
||||
let data=self.cookie_context.get_asset(rbx_asset::cookie::GetAssetRequest{
|
||||
asset_id:validate_info.ModelID,
|
||||
version:Some(validate_info.ModelVersion),
|
||||
}).await.map_err(Error::ModelFileDownload)?;
|
||||
}).await.map_err(ValidateError::ModelFileDownload)?;
|
||||
|
||||
// decode dom (slow!)
|
||||
let mut dom=read_dom(&mut std::io::Cursor::new(data)).map_err(Error::ModelFileDecode)?;
|
||||
let mut dom=read_dom(&mut std::io::Cursor::new(data)).map_err(ValidateError::ModelFileDecode)?;
|
||||
|
||||
/* VALIDATE MAP */
|
||||
|
||||
@@ -111,7 +111,7 @@ impl crate::message_handler::MessageHandler{
|
||||
// immediately abort
|
||||
// grab path to offending script
|
||||
let path=get_partial_path(&dom,script);
|
||||
return Err(Error::ScriptFlaggedIllegalKeyword(path));
|
||||
return Err(ValidateError::ScriptFlaggedIllegalKeyword(path));
|
||||
}
|
||||
// associate a name and policy with the source code
|
||||
// policy will be fetched from the database to replace the default policy
|
||||
@@ -132,7 +132,7 @@ impl crate::message_handler::MessageHandler{
|
||||
// fetch the script policy
|
||||
let script_policy=self.api.get_script_policy_from_hash(submissions_api::types::HashRequest{
|
||||
hash:hash.as_str(),
|
||||
}).await.map_err(Error::ApiGetScriptPolicyFromHash)?;
|
||||
}).await.map_err(ValidateError::ApiGetScriptPolicyFromHash)?;
|
||||
|
||||
// write the policy to the script_map, fetching the replacement code if necessary
|
||||
if let Some(script_policy)=script_policy{
|
||||
@@ -144,7 +144,7 @@ impl crate::message_handler::MessageHandler{
|
||||
submissions_api::types::Policy::Replace=>{
|
||||
let script=self.api.get_script(submissions_api::types::GetScriptRequest{
|
||||
ScriptID:script_policy.ToScriptID,
|
||||
}).await.map_err(Error::ApiGetScript)?;
|
||||
}).await.map_err(ValidateError::ApiGetScript)?;
|
||||
Policy::Replace(script.Source)
|
||||
},
|
||||
};
|
||||
@@ -160,14 +160,14 @@ impl crate::message_handler::MessageHandler{
|
||||
Source:source.as_str(),
|
||||
ResourceType:resource_type,
|
||||
ResourceID:Some(resource_id),
|
||||
}).await.map_err(Error::ApiCreateScript)?;
|
||||
}).await.map_err(ValidateError::ApiCreateScript)?;
|
||||
|
||||
// create a None policy (pending review by yours truly)
|
||||
self.api.create_script_policy(submissions_api::types::CreateScriptPolicyRequest{
|
||||
ToScriptID:script.ScriptID,
|
||||
FromScriptID:script.ScriptID,
|
||||
Policy:submissions_api::types::Policy::None,
|
||||
}).await.map_err(Error::ApiCreateScriptPolicy)?;
|
||||
}).await.map_err(ValidateError::ApiCreateScriptPolicy)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -175,7 +175,7 @@ impl crate::message_handler::MessageHandler{
|
||||
.await?;
|
||||
|
||||
// make the replacements
|
||||
let mut modified=false;
|
||||
let mut modified=true;
|
||||
for &script_ref in &script_refs{
|
||||
if let Some(script)=dom.get_by_ref_mut(script_ref){
|
||||
if let Some(rbx_dom_weak::types::Variant::String(source))=script.properties.get_mut("Source"){
|
||||
@@ -184,8 +184,8 @@ impl crate::message_handler::MessageHandler{
|
||||
let hash=hash_source(source.as_str());
|
||||
let script=self.api.get_script_from_hash(submissions_api::types::HashRequest{
|
||||
hash:hash.as_str(),
|
||||
}).await.map_err(Error::ApiGetScriptFromHash)?;
|
||||
return Err(Error::ScriptBlocked(script.map(|s|s.ID)));
|
||||
}).await.map_err(ValidateError::ApiGetScriptFromHash)?;
|
||||
return Err(ValidateError::ScriptBlocked(script.map(|s|s.ID)));
|
||||
},
|
||||
None
|
||||
|Some(Policy::None)
|
||||
@@ -193,8 +193,8 @@ impl crate::message_handler::MessageHandler{
|
||||
let hash=hash_source(source.as_str());
|
||||
let script=self.api.get_script_from_hash(submissions_api::types::HashRequest{
|
||||
hash:hash.as_str(),
|
||||
}).await.map_err(Error::ApiGetScriptFromHash)?;
|
||||
return Err(Error::ScriptNotYetReviewed(script.map(|s|s.ID)));
|
||||
}).await.map_err(ValidateError::ApiGetScriptFromHash)?;
|
||||
return Err(ValidateError::ScriptNotYetReviewed(script.map(|s|s.ID)));
|
||||
},
|
||||
Some(Policy::Allowed)=>(),
|
||||
Some(Policy::Delete)=>{
|
||||
@@ -211,17 +211,19 @@ impl crate::message_handler::MessageHandler{
|
||||
}
|
||||
}
|
||||
|
||||
println!("[Validator] Forcing model upload! modified=true");
|
||||
|
||||
// if the model was validated, the submission must be changed to use the modified model
|
||||
let (validated_model_id,validated_model_version)=if modified{
|
||||
if modified{
|
||||
// serialize model (slow!)
|
||||
let mut data=Vec::new();
|
||||
let &[map_ref]=dom.root().children()else{
|
||||
return Err(Error::ModelFileRootMustHaveOneChild);
|
||||
return Err(ValidateError::ModelFileRootMustHaveOneChild);
|
||||
};
|
||||
rbx_binary::to_writer(&mut data,&dom,&[map_ref]).map_err(Error::ModelFileEncode)?;
|
||||
rbx_binary::to_writer(&mut data,&dom,&[map_ref]).map_err(ValidateError::ModelFileEncode)?;
|
||||
|
||||
// upload a model lol
|
||||
if let Some(model_id)=validate_info.ValidatedModelID{
|
||||
let model_id=if let Some(model_id)=validate_info.ValidatedModelID{
|
||||
// upload to existing id
|
||||
let response=self.cookie_context.upload(rbx_asset::cookie::UploadRequest{
|
||||
assetid:model_id,
|
||||
@@ -230,13 +232,13 @@ impl crate::message_handler::MessageHandler{
|
||||
ispublic:None,
|
||||
allowComments:None,
|
||||
groupId:None,
|
||||
},data).await.map_err(Error::AssetUpload)?;
|
||||
},data).await.map_err(ValidateError::AssetUpload)?;
|
||||
|
||||
(response.AssetId,response.AssetVersion)
|
||||
response.AssetId
|
||||
}else{
|
||||
// grab the map instance from the map ref
|
||||
// grab the map instance from the map re
|
||||
let Some(map_instance)=dom.get_by_ref(map_ref)else{
|
||||
return Err(Error::ModelFileChildRefIsNil);
|
||||
return Err(ValidateError::ModelFileChildRefIsNil);
|
||||
};
|
||||
// create new model
|
||||
let response=self.cookie_context.create(rbx_asset::cookie::CreateRequest{
|
||||
@@ -245,31 +247,29 @@ impl crate::message_handler::MessageHandler{
|
||||
ispublic:true,
|
||||
allowComments:true,
|
||||
groupId:None,
|
||||
},data).await.map_err(Error::AssetCreate)?;
|
||||
},data).await.map_err(ValidateError::AssetCreate)?;
|
||||
|
||||
(response.AssetId,response.AssetVersion)
|
||||
response.AssetId
|
||||
};
|
||||
|
||||
match validate_info.ResourceID{
|
||||
ResourceID::Mapfix(mapfix_id)=>{
|
||||
// update the mapfix to use the validated model
|
||||
self.api.update_mapfix_validated_model(submissions_api::types::UpdateMapfixModelRequest{
|
||||
MapfixID:mapfix_id,
|
||||
ModelID:model_id,
|
||||
ModelVersion:1, //TODO
|
||||
}).await.map_err(ValidateError::ApiUpdateMapfixModel)?;
|
||||
},
|
||||
ResourceID::Submission(submission_id)=>{
|
||||
// update the submission to use the validated model
|
||||
self.api.update_submission_validated_model(submissions_api::types::UpdateSubmissionModelRequest{
|
||||
SubmissionID:submission_id,
|
||||
ModelID:model_id,
|
||||
ModelVersion:1, //TODO
|
||||
}).await.map_err(ValidateError::ApiUpdateSubmissionModel)?;
|
||||
},
|
||||
}
|
||||
}else{
|
||||
(validate_info.ModelID,validate_info.ModelVersion)
|
||||
};
|
||||
|
||||
match validate_info.ResourceID{
|
||||
ResourceID::Mapfix(mapfix_id)=>{
|
||||
// update the mapfix to use the validated model
|
||||
self.api.update_mapfix_validated_model(submissions_api::types::UpdateMapfixModelRequest{
|
||||
MapfixID:mapfix_id,
|
||||
ModelID:validated_model_id,
|
||||
ModelVersion:validated_model_version,
|
||||
}).await.map_err(Error::ApiUpdateMapfixModel)?;
|
||||
},
|
||||
ResourceID::Submission(submission_id)=>{
|
||||
// update the submission to use the validated model
|
||||
self.api.update_submission_validated_model(submissions_api::types::UpdateSubmissionModelRequest{
|
||||
SubmissionID:submission_id,
|
||||
ModelID:validated_model_id,
|
||||
ModelVersion:validated_model_version,
|
||||
}).await.map_err(Error::ApiUpdateSubmissionModel)?;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM registry.itzana.me/docker-proxy/oven/bun:1.2.8
|
||||
FROM oven/bun:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -10,4 +10,4 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN bun install
|
||||
RUN bun run build
|
||||
ENTRYPOINT ["bun", "run", "start"]
|
||||
ENTRYPOINT ["bun", "run", "start"]
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Roles, RolesConstants } from "@/app/ts/Roles";
|
||||
import { MapfixStatus } from "@/app/ts/Mapfix";
|
||||
import { Button, ButtonOwnProps } from "@mui/material";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
type Actions = "Completed" | "Submit" | "Reject" | "Revoke"
|
||||
type ApiActions = Lowercase<Actions> | "request-changes" | "trigger-validate" | "retry-validate" | "trigger-upload" | "reset-uploading" | "reset-validating"
|
||||
type Review = Actions | "Request Changes" | "Accept" | "Validate" | "Upload" | "Reset Uploading (fix softlocked status)" | "Reset Validating (fix softlocked status)" | "Request Changes"
|
||||
type ApiActions = Lowercase<Actions> | "trigger-validate" | "retry-validate" | "trigger-upload" | "reset-uploading" | "reset-validating"
|
||||
type Review = Actions | "Accept" | "Validate" | "Upload" | "Reset Uploading (fix softlocked status)" | "Reset Validating (fix softlocked status)" | "Request Changes"
|
||||
|
||||
interface ReviewButton {
|
||||
name: Review,
|
||||
@@ -15,9 +12,7 @@ interface ReviewButton {
|
||||
}
|
||||
|
||||
interface ReviewId {
|
||||
mapfixId: string,
|
||||
mapfixStatus: number,
|
||||
mapfixSubmitter: number,
|
||||
mapfixId: string
|
||||
}
|
||||
|
||||
async function ReviewButtonClicked(action: ApiActions, mapfixId: string) {
|
||||
@@ -50,6 +45,7 @@ function ReviewButton(props: ReviewButton) {
|
||||
}
|
||||
|
||||
export default function ReviewButtons(props: ReviewId) {
|
||||
const mapfixId = props.mapfixId
|
||||
// When is each button visible?
|
||||
// Multiple buttons can be visible at once.
|
||||
// Action | Role | When Current Status is One of:
|
||||
@@ -63,86 +59,16 @@ export default function ReviewButtons(props: ReviewId) {
|
||||
// RequestChanges | Reviewer | Validated, Accepted, Submitted
|
||||
// Upload | MapAdmin | Validated
|
||||
// ResetUploading | MapAdmin | Uploading
|
||||
const { mapfixId, mapfixStatus } = props;
|
||||
const [user, setUser] = useState<number|null>(null);
|
||||
const [roles, setRoles] = useState<Roles>(RolesConstants.Empty);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [rolesData, userData] = await Promise.all([
|
||||
fetch("/api/session/roles").then(rolesResponse => rolesResponse.json()),
|
||||
fetch("/api/session/user").then(userResponse => userResponse.json())
|
||||
]);
|
||||
|
||||
setRoles(rolesData.Roles);
|
||||
setUser(userData.UserID);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, [mapfixId]);
|
||||
|
||||
if (loading) return <p>Loading...</p>;
|
||||
|
||||
const visibleButtons: ReviewButton[] = [];
|
||||
|
||||
const is_submitter = user === props.mapfixSubmitter;
|
||||
if (is_submitter) {
|
||||
if ([MapfixStatus.UnderConstruction, MapfixStatus.ChangesRequested].includes(mapfixStatus!)) {
|
||||
visibleButtons.push({ name: "Submit", action: "submit", color: "info", mapfixId });
|
||||
}
|
||||
if ([MapfixStatus.Submitted, MapfixStatus.ChangesRequested].includes(mapfixStatus!)) {
|
||||
visibleButtons.push({ name: "Revoke", action: "revoke", color: "info", mapfixId });
|
||||
}
|
||||
}
|
||||
|
||||
if (roles&RolesConstants.MapfixReview) {
|
||||
// you can't review your own mapfix!
|
||||
// note that this means there needs to be more than one person with MapfixReview
|
||||
if (!is_submitter && mapfixStatus === MapfixStatus.Submitted) {
|
||||
visibleButtons.push({ name: "Accept", action: "trigger-validate", color: "info", mapfixId });
|
||||
visibleButtons.push({ name: "Reject", action: "reject", color: "error", mapfixId });
|
||||
}
|
||||
if (mapfixStatus === MapfixStatus.Accepted) {
|
||||
visibleButtons.push({ name: "Validate", action: "retry-validate", color: "info", mapfixId });
|
||||
}
|
||||
if (mapfixStatus === MapfixStatus.Validating) {
|
||||
visibleButtons.push({ name: "Reset Validating (fix softlocked status)", action: "reset-validating", color: "error", mapfixId });
|
||||
}
|
||||
// this button serves the same purpose as Revoke if you are both
|
||||
// the map submitter and have MapfixReview when status is Submitted
|
||||
if (
|
||||
[MapfixStatus.Validated, MapfixStatus.Accepted].includes(mapfixStatus!)
|
||||
|| !is_submitter && mapfixStatus == MapfixStatus.Submitted
|
||||
) {
|
||||
visibleButtons.push({ name: "Request Changes", action: "request-changes", color: "error", mapfixId });
|
||||
}
|
||||
}
|
||||
|
||||
if (roles&RolesConstants.MapfixUpload) {
|
||||
if (mapfixStatus === MapfixStatus.Validated) {
|
||||
visibleButtons.push({ name: "Upload", action: "trigger-upload", color: "info", mapfixId });
|
||||
}
|
||||
if (mapfixStatus === MapfixStatus.Uploading) {
|
||||
visibleButtons.push({ name: "Reset Uploading (fix softlocked status)", action: "reset-uploading", color: "error", mapfixId });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="review-set">
|
||||
{visibleButtons.length === 0 ? (
|
||||
<p>No available actions</p>
|
||||
) : (
|
||||
visibleButtons.map((btn) => (
|
||||
<ReviewButton key={btn.action} {...btn} />
|
||||
))
|
||||
)}
|
||||
<ReviewButton color="info" name="Submit" action="submit" mapfixId={mapfixId}/>
|
||||
<ReviewButton color="info" name="Revoke" action="revoke" mapfixId={mapfixId}/>
|
||||
<ReviewButton color="info" name="Accept" action="trigger-validate" mapfixId={mapfixId}/>
|
||||
<ReviewButton color="info" name="Validate" action="retry-validate" mapfixId={mapfixId}/>
|
||||
<ReviewButton color="error" name="Reject" action="reject" mapfixId={mapfixId}/>
|
||||
<ReviewButton color="info" name="Upload" action="trigger-upload" mapfixId={mapfixId}/>
|
||||
<ReviewButton color="error" name="Reset Uploading (fix softlocked status)" action="reset-uploading" mapfixId={mapfixId}/>
|
||||
<ReviewButton color="error" name="Reset Validating (fix softlocked status)" action="reset-validating" mapfixId={mapfixId}/>
|
||||
</section>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ import { useState, useEffect } from "react";
|
||||
import "./(styles)/page.scss";
|
||||
|
||||
interface ReviewId {
|
||||
mapfixId: string,
|
||||
mapfixStatus: number;
|
||||
mapfixSubmitter: number,
|
||||
mapfixId: string
|
||||
}
|
||||
|
||||
function Ratings() {
|
||||
@@ -48,7 +46,7 @@ function RatingArea(mapfix: ReviewId) {
|
||||
<MapImage/>
|
||||
</section>
|
||||
<Ratings/>
|
||||
<ReviewButtons mapfixId={mapfix.mapfixId} mapfixStatus={mapfix.mapfixStatus} mapfixSubmitter={mapfix.mapfixSubmitter}/>
|
||||
<ReviewButtons mapfixId={mapfix.mapfixId}/>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -98,7 +96,7 @@ export default function MapfixInfoPage() {
|
||||
<Webpage>
|
||||
<main className="map-page-main">
|
||||
<section className="review-section">
|
||||
<RatingArea mapfixId={dynamicId.mapfixId} mapfixStatus={mapfix.StatusID} mapfixSubmitter={mapfix.Submitter}/>
|
||||
<RatingArea mapfixId={dynamicId.mapfixId}/>
|
||||
<TitleAndComments name={mapfix.DisplayName} creator={mapfix.Creator} review={mapfix.StatusID} status_message={mapfix.StatusMessage} asset_id={mapfix.AssetID} comments={[]}/>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import { MapInfo } from "@/app/ts/Map";
|
||||
|
||||
interface AssetID {
|
||||
id: MapInfo["ID"];
|
||||
}
|
||||
|
||||
function MapImage({ id }: AssetID) {
|
||||
if (!id) {
|
||||
return <p>Missing asset ID</p>;
|
||||
}
|
||||
|
||||
const imageUrl = `/thumbnails/asset/${id}`;
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt="Map Thumbnail"
|
||||
layout="responsive"
|
||||
width={512}
|
||||
height={512}
|
||||
priority={true}
|
||||
className="map-image"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { type AssetID, MapImage };
|
||||
@@ -13,7 +13,7 @@ interface MapfixPayload {
|
||||
TargetAssetID: number;
|
||||
}
|
||||
interface IdResponse {
|
||||
OperationID: number;
|
||||
ID: number;
|
||||
}
|
||||
|
||||
export default function MapfixInfoPage() {
|
||||
@@ -53,7 +53,7 @@ export default function MapfixInfoPage() {
|
||||
const id_response:IdResponse = await response.json();
|
||||
|
||||
// navigate to newly created mapfix
|
||||
window.location.assign(`/operations/${id_response.OperationID}`)
|
||||
window.location.assign(`/mapfixes/${id_response.ID}`)
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error submitting data:", error);
|
||||
|
||||
@@ -1,83 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import { MapInfo } from "@/app/ts/Map";
|
||||
import { MapImage } from "./_mapImage";
|
||||
import Webpage from "@/app/_components/webpage";
|
||||
import { useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface ButtonProps {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
function Button({ name, href }: ButtonProps) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<button className="mt-6 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-xl shadow transition">
|
||||
{name}
|
||||
</button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Map() {
|
||||
const { mapId } = useParams();
|
||||
const [map, setMap] = useState<MapInfo | null>(null);
|
||||
const { mapId } = useParams<{mapId: string}>()
|
||||
|
||||
useEffect(() => {
|
||||
async function getMap() {
|
||||
const res = await fetch(`/api/maps/${mapId}`);
|
||||
if (res.ok) {
|
||||
setMap(await res.json());
|
||||
}
|
||||
}
|
||||
getMap();
|
||||
}, [mapId]);
|
||||
|
||||
if (!map) {
|
||||
return (
|
||||
<Webpage>
|
||||
<div className="p-12 text-center text-gray-500">Loading map data...</div>
|
||||
</Webpage>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Webpage>
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="bg-white dark:bg-zinc-900 shadow-xl rounded-2xl p-8 space-y-8">
|
||||
{/* Title */}
|
||||
<h1 className="text-3xl font-bold text-center">{map.DisplayName}</h1>
|
||||
|
||||
{/* Image */}
|
||||
<div className="w-full overflow-hidden rounded-xl border border-zinc-300 dark:border-zinc-700 shadow-md">
|
||||
<MapImage id={map.ID} />
|
||||
</div>
|
||||
|
||||
{/* Info grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-center text-zinc-700 dark:text-zinc-300">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-zinc-500">Creator</p>
|
||||
<p className="text-lg">{map.Creator}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-zinc-500">Game ID</p>
|
||||
<p className="text-lg">{map.GameID}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-zinc-500">Release Date</p>
|
||||
<p className="text-lg">{map.Date}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Button */}
|
||||
<div className="text-center">
|
||||
<Button name="Submit A Mapfix For This Map" href={`/maps/${mapId}/fix`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Webpage>
|
||||
);
|
||||
return (
|
||||
<Webpage>
|
||||
<p>map { mapId }</p>
|
||||
</Webpage>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { CircularProgress, Typography, Card, CardContent, Button } from "@mui/material";
|
||||
import Webpage from "@/app/_components/webpage";
|
||||
@@ -17,31 +17,23 @@ interface Operation {
|
||||
}
|
||||
|
||||
export default function OperationStatusPage() {
|
||||
const router = useRouter();
|
||||
const { operationId } = useParams();
|
||||
|
||||
const router = useRouter();
|
||||
const [operation, setOperation] = useState<Operation | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [operation, setOperation] = useState<Operation | null>(null);
|
||||
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!operationId) return;
|
||||
|
||||
|
||||
const fetchOperation = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/operations/${operationId}`);
|
||||
|
||||
|
||||
if (!response.ok) throw new Error("Failed to fetch operation");
|
||||
|
||||
const data: Operation = await response.json();
|
||||
|
||||
const data = await response.json();
|
||||
setOperation(data);
|
||||
|
||||
if (data.Status !== 0 && intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
@@ -52,34 +44,39 @@ export default function OperationStatusPage() {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
fetchOperation();
|
||||
if (!intervalRef.current) {
|
||||
intervalRef.current = setInterval(fetchOperation, 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
const interval = setInterval(fetchOperation, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [operationId]);
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
const getStatusClass = (status: number) => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return "Created";
|
||||
case 1:
|
||||
return "Completed";
|
||||
case 2:
|
||||
return "Failed";
|
||||
default:
|
||||
return "Unknown";
|
||||
case 0:
|
||||
return "created";
|
||||
case 1:
|
||||
return "completed";
|
||||
case 2:
|
||||
return "failed";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusClass = (status: number) => getStatusText(status).toLowerCase();
|
||||
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return "Created";
|
||||
case 1:
|
||||
return "Completed";
|
||||
case 2:
|
||||
return "Failed";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Webpage>
|
||||
<main className="operation-status">
|
||||
@@ -99,13 +96,13 @@ export default function OperationStatusPage() {
|
||||
<Typography>Owner: {operation.Owner}</Typography>
|
||||
<Typography>Date: {new Date(operation.Date * 1000).toLocaleString()}</Typography>
|
||||
<Typography>Path: {operation.Path}</Typography>
|
||||
|
||||
|
||||
{operation.Status === 1 && (
|
||||
<div className="submission-button">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={() => router.push(operation.Path)}
|
||||
onClick={() => router.push(`/submissions/${operation.OperationID}`)}
|
||||
>
|
||||
View Submission
|
||||
</Button>
|
||||
@@ -119,4 +116,4 @@ export default function OperationStatusPage() {
|
||||
</main>
|
||||
</Webpage>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Roles, RolesConstants } from "@/app/ts/Roles";
|
||||
import { SubmissionStatus } from "@/app/ts/Submission";
|
||||
import { Button, ButtonOwnProps } from "@mui/material";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
type Actions = "Completed" | "Submit" | "Reject" | "Revoke"
|
||||
type ApiActions = Lowercase<Actions> | "request-changes" | "trigger-validate" | "retry-validate" | "trigger-upload" | "reset-uploading" | "reset-validating"
|
||||
type Review = Actions | "Request Changes" | "Accept" | "Validate" | "Upload" | "Reset Uploading (fix softlocked status)" | "Reset Validating (fix softlocked status)" | "Request Changes"
|
||||
type ApiActions = Lowercase<Actions> | "trigger-validate" | "retry-validate" | "trigger-upload" | "reset-uploading" | "reset-validating"
|
||||
type Review = Actions | "Accept" | "Validate" | "Upload" | "Reset Uploading (fix softlocked status)" | "Reset Validating (fix softlocked status)" | "Request Changes"
|
||||
|
||||
interface ReviewButton {
|
||||
name: Review,
|
||||
@@ -15,9 +12,7 @@ interface ReviewButton {
|
||||
}
|
||||
|
||||
interface ReviewId {
|
||||
submissionId: string,
|
||||
submissionStatus: number,
|
||||
submissionSubmitter: number,
|
||||
submissionId: string
|
||||
}
|
||||
|
||||
async function ReviewButtonClicked(action: ApiActions, submissionId: string) {
|
||||
@@ -50,6 +45,7 @@ function ReviewButton(props: ReviewButton) {
|
||||
}
|
||||
|
||||
export default function ReviewButtons(props: ReviewId) {
|
||||
const submissionId = props.submissionId
|
||||
// When is each button visible?
|
||||
// Multiple buttons can be visible at once.
|
||||
// Action | Role | When Current Status is One of:
|
||||
@@ -63,86 +59,16 @@ export default function ReviewButtons(props: ReviewId) {
|
||||
// RequestChanges | Reviewer | Validated, Accepted, Submitted
|
||||
// Upload | MapAdmin | Validated
|
||||
// ResetUploading | MapAdmin | Uploading
|
||||
const { submissionId, submissionStatus } = props;
|
||||
const [user, setUser] = useState<number|null>(null);
|
||||
const [roles, setRoles] = useState<Roles>(RolesConstants.Empty);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [rolesData, userData] = await Promise.all([
|
||||
fetch("/api/session/roles").then(rolesResponse => rolesResponse.json()),
|
||||
fetch("/api/session/user").then(userResponse => userResponse.json())
|
||||
]);
|
||||
|
||||
setRoles(rolesData.Roles);
|
||||
setUser(userData.UserID);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, [submissionId]);
|
||||
|
||||
if (loading) return <p>Loading...</p>;
|
||||
|
||||
const visibleButtons: ReviewButton[] = [];
|
||||
|
||||
const is_submitter = user === props.submissionSubmitter;
|
||||
if (is_submitter) {
|
||||
if ([SubmissionStatus.UnderConstruction, SubmissionStatus.ChangesRequested].includes(submissionStatus!)) {
|
||||
visibleButtons.push({ name: "Submit", action: "submit", color: "info", submissionId });
|
||||
}
|
||||
if ([SubmissionStatus.Submitted, SubmissionStatus.ChangesRequested].includes(submissionStatus!)) {
|
||||
visibleButtons.push({ name: "Revoke", action: "revoke", color: "info", submissionId });
|
||||
}
|
||||
}
|
||||
|
||||
if (roles&RolesConstants.SubmissionReview) {
|
||||
// you can't review your own submission!
|
||||
// note that this means there needs to be more than one person with SubmissionReview
|
||||
if (!is_submitter && submissionStatus === SubmissionStatus.Submitted) {
|
||||
visibleButtons.push({ name: "Accept", action: "trigger-validate", color: "info", submissionId });
|
||||
visibleButtons.push({ name: "Reject", action: "reject", color: "error", submissionId });
|
||||
}
|
||||
if (submissionStatus === SubmissionStatus.Accepted) {
|
||||
visibleButtons.push({ name: "Validate", action: "retry-validate", color: "info", submissionId });
|
||||
}
|
||||
if (submissionStatus === SubmissionStatus.Validating) {
|
||||
visibleButtons.push({ name: "Reset Validating (fix softlocked status)", action: "reset-validating", color: "error", submissionId });
|
||||
}
|
||||
// this button serves the same purpose as Revoke if you are both
|
||||
// the map submitter and have SubmissionReview when status is Submitted
|
||||
if (
|
||||
[SubmissionStatus.Validated, SubmissionStatus.Accepted].includes(submissionStatus!)
|
||||
|| !is_submitter && submissionStatus == SubmissionStatus.Submitted
|
||||
) {
|
||||
visibleButtons.push({ name: "Request Changes", action: "request-changes", color: "error", submissionId });
|
||||
}
|
||||
}
|
||||
|
||||
if (roles&RolesConstants.SubmissionUpload) {
|
||||
if (submissionStatus === SubmissionStatus.Validated) {
|
||||
visibleButtons.push({ name: "Upload", action: "trigger-upload", color: "info", submissionId });
|
||||
}
|
||||
if (submissionStatus === SubmissionStatus.Uploading) {
|
||||
visibleButtons.push({ name: "Reset Uploading (fix softlocked status)", action: "reset-uploading", color: "error", submissionId });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="review-set">
|
||||
{visibleButtons.length === 0 ? (
|
||||
<p>No available actions</p>
|
||||
) : (
|
||||
visibleButtons.map((btn) => (
|
||||
<ReviewButton key={btn.action} {...btn} />
|
||||
))
|
||||
)}
|
||||
<ReviewButton color="info" name="Submit" action="submit" submissionId={submissionId}/>
|
||||
<ReviewButton color="info" name="Revoke" action="revoke" submissionId={submissionId}/>
|
||||
<ReviewButton color="info" name="Accept" action="trigger-validate" submissionId={submissionId}/>
|
||||
<ReviewButton color="info" name="Validate" action="retry-validate" submissionId={submissionId}/>
|
||||
<ReviewButton color="error" name="Reject" action="reject" submissionId={submissionId}/>
|
||||
<ReviewButton color="info" name="Upload" action="trigger-upload" submissionId={submissionId}/>
|
||||
<ReviewButton color="error" name="Reset Uploading (fix softlocked status)" action="reset-uploading" submissionId={submissionId}/>
|
||||
<ReviewButton color="error" name="Reset Validating (fix softlocked status)" action="reset-validating" submissionId={submissionId}/>
|
||||
</section>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ import "./(styles)/page.scss";
|
||||
interface ReviewId {
|
||||
submissionId: string;
|
||||
assetId: number;
|
||||
submissionStatus: number;
|
||||
submissionSubmitter: number,
|
||||
}
|
||||
|
||||
function Ratings() {
|
||||
@@ -49,7 +47,7 @@ function RatingArea(submission: ReviewId) {
|
||||
<MapImage id={submission.assetId}/>
|
||||
</section>
|
||||
<Ratings/>
|
||||
<ReviewButtons submissionId={submission.submissionId} submissionStatus={submission.submissionStatus} submissionSubmitter={submission.submissionSubmitter}/>
|
||||
<ReviewButtons submissionId={submission.submissionId}/>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -99,7 +97,7 @@ export default function SubmissionInfoPage() {
|
||||
<Webpage>
|
||||
<main className="map-page-main">
|
||||
<section className="review-section">
|
||||
<RatingArea assetId={submission.AssetID} submissionId={dynamicId.submissionId} submissionStatus={submission.StatusID} submissionSubmitter={submission.Submitter}/>
|
||||
<RatingArea assetId={submission.AssetID} submissionId={dynamicId.submissionId}/>
|
||||
<TitleAndComments name={submission.DisplayName} creator={submission.Creator} review={submission.StatusID} status_message={submission.StatusMessage} asset_id={submission.AssetID} comments={[]}/>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
interface MapInfo {
|
||||
readonly ID: number,
|
||||
readonly DisplayName: string,
|
||||
readonly Creator: string,
|
||||
readonly GameID: number,
|
||||
readonly Date: number,
|
||||
}
|
||||
|
||||
export {
|
||||
type MapInfo
|
||||
}
|
||||
@@ -8,7 +8,6 @@ const enum MapfixStatus {
|
||||
Uploading = 6,
|
||||
Uploaded = 7,
|
||||
Rejected = 8,
|
||||
// MapfixStatus does not have a Released state
|
||||
}
|
||||
|
||||
interface MapfixInfo {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
type Roles = number;
|
||||
|
||||
// Constants
|
||||
const RolesConstants = {
|
||||
All: -1 as Roles,
|
||||
SubmissionUpload: 1 << 6 as Roles,
|
||||
SubmissionReview: 1 << 5 as Roles,
|
||||
SubmissionRelease: 1 << 4 as Roles,
|
||||
ScriptWrite: 1 << 3 as Roles,
|
||||
MapfixUpload: 1 << 2 as Roles,
|
||||
MapfixReview: 1 << 1 as Roles,
|
||||
MapDownload: 1 << 0 as Roles,
|
||||
Empty: 0 as Roles,
|
||||
};
|
||||
|
||||
// Operations
|
||||
function hasRole(flags: Roles, role: Roles): boolean {
|
||||
return (flags & role) === role;
|
||||
}
|
||||
|
||||
export {
|
||||
type Roles,
|
||||
RolesConstants,
|
||||
hasRole,
|
||||
};
|
||||
Reference in New Issue
Block a user