Compare commits
77 Commits
rm-alloc
...
mapfix-che
| Author | SHA1 | Date | |
|---|---|---|---|
|
474655f4a3
|
|||
|
9e47ca5177
|
|||
|
8894231b41
|
|||
|
19a6b0304c
|
|||
|
f00b2b8473
|
|||
|
7fdd72ffdd
|
|||
| 264ce38c08 | |||
|
b1e10dc50e
|
|||
|
755616f46c
|
|||
| e41d34dd3d | |||
| f49e27e230 | |||
| d500462fc7 | |||
| ee2bc94312 | |||
| 84edc71574 | |||
| 7c5d8a2163 | |||
| 7eaa84a0ed | |||
| cf0cf9da7a | |||
| 74565e567a | |||
| ea65794255 | |||
| 58706a5687 | |||
| efeb525e19 | |||
|
5a1fe60a7b
|
|||
| 01cfe67848 | |||
| a19bc4d380 | |||
| ae006565d6 | |||
| 57bca99109 | |||
| cd09c9b18e | |||
| e48cbaff72 | |||
| 140d58b808 | |||
| ba761549b8 | |||
| 86643fef8d | |||
| 96af864c5e | |||
| 7db89fd99b | |||
| f2bb1b078d | |||
| 66878fba4e | |||
| bda99550be | |||
| 8a216c7e82 | |||
| e5277c05a1 | |||
| e4af76cfd4 | |||
| 30db1cc375 | |||
| b50c84f8cf | |||
| 7589ef7df6 | |||
| 8ab8c441b0 | |||
| a26b228ebe | |||
| 3654755540 | |||
| c2b50ffab2 | |||
| 75756917b1 | |||
| 8989c08857 | |||
| b2232f4177 | |||
| 7d1c4d2b6c | |||
| ca401d4b96 | |||
|
9ab80931bf
|
|||
|
09022e7292
|
|||
| 47c0fff0ec | |||
| b7c28616ad | |||
| 89ab25dfb9 | |||
| b0b5ff0725 | |||
| 0532965d37 | |||
| f59979987f | |||
| a232269d54 | |||
| a7c4ca4b49 | |||
| ca9f82a5aa | |||
| e1a2f6f075 | |||
| dad904cd86 | |||
| ad7117a69c | |||
| d566591ea6 | |||
| 424ef6238b | |||
| 0f0ab4d3e0 | |||
| 3e2d782289 | |||
| dc446c545f | |||
|
e234a87d05
|
|||
| 8ab772ea81 | |||
| 9b58b1d26a | |||
| 7689001e74 | |||
| e89abed3d5 | |||
| b792d33164 | |||
| 929b5949f0 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1044,6 +1044,7 @@ dependencies = [
|
|||||||
"rust-grpc",
|
"rust-grpc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
"siphasher",
|
"siphasher",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tonic",
|
"tonic",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ services:
|
|||||||
"--data-rpc-host","dataservice:9000",
|
"--data-rpc-host","dataservice:9000",
|
||||||
]
|
]
|
||||||
env_file:
|
env_file:
|
||||||
- ~/auth-compose/strafesnet_staging.env
|
- /home/quat/auth-compose/strafesnet_staging.env
|
||||||
depends_on:
|
depends_on:
|
||||||
- authrpc
|
- authrpc
|
||||||
- nats
|
- nats
|
||||||
@@ -59,7 +59,7 @@ services:
|
|||||||
maptest-validator
|
maptest-validator
|
||||||
container_name: validation
|
container_name: validation
|
||||||
env_file:
|
env_file:
|
||||||
- ~/auth-compose/strafesnet_staging.env
|
- /home/quat/auth-compose/strafesnet_staging.env
|
||||||
environment:
|
environment:
|
||||||
- ROBLOX_GROUP_ID=17032139 # "None" is special case string value
|
- ROBLOX_GROUP_ID=17032139 # "None" is special case string value
|
||||||
- API_HOST_INTERNAL=http://submissions:8083/v1
|
- API_HOST_INTERNAL=http://submissions:8083/v1
|
||||||
@@ -105,7 +105,7 @@ services:
|
|||||||
- REDIS_ADDR=authredis:6379
|
- REDIS_ADDR=authredis:6379
|
||||||
- RBX_GROUP_ID=17032139
|
- RBX_GROUP_ID=17032139
|
||||||
env_file:
|
env_file:
|
||||||
- ~/auth-compose/auth-service.env
|
- /home/quat/auth-compose/auth-service.env
|
||||||
depends_on:
|
depends_on:
|
||||||
- authredis
|
- authredis
|
||||||
networks:
|
networks:
|
||||||
@@ -119,7 +119,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- REDIS_ADDR=authredis:6379
|
- REDIS_ADDR=authredis:6379
|
||||||
env_file:
|
env_file:
|
||||||
- ~/auth-compose/auth-service.env
|
- /home/quat/auth-compose/auth-service.env
|
||||||
depends_on:
|
depends_on:
|
||||||
- authredis
|
- authredis
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
45
go.mod
45
go.mod
@@ -11,17 +11,18 @@ require (
|
|||||||
github.com/dchest/siphash v1.2.3
|
github.com/dchest/siphash v1.2.3
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/go-faster/errors v0.7.1
|
github.com/go-faster/errors v0.7.1
|
||||||
github.com/go-faster/jx v1.1.0
|
github.com/go-faster/jx v1.2.0
|
||||||
github.com/nats-io/nats.go v1.37.0
|
github.com/nats-io/nats.go v1.37.0
|
||||||
github.com/ogen-go/ogen v1.2.1
|
github.com/ogen-go/ogen v1.18.0
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/swaggo/files v1.0.1
|
github.com/swaggo/files v1.0.1
|
||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
github.com/swaggo/swag v1.16.6
|
github.com/swaggo/swag v1.16.6
|
||||||
github.com/urfave/cli/v2 v2.27.6
|
github.com/urfave/cli/v2 v2.27.6
|
||||||
go.opentelemetry.io/otel v1.32.0
|
go.opentelemetry.io/otel v1.39.0
|
||||||
go.opentelemetry.io/otel/metric v1.32.0
|
go.opentelemetry.io/otel/metric v1.39.0
|
||||||
go.opentelemetry.io/otel/trace v1.32.0
|
go.opentelemetry.io/otel/trace v1.39.0
|
||||||
google.golang.org/grpc v1.48.0
|
google.golang.org/grpc v1.48.0
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.25.12
|
gorm.io/gorm v1.25.12
|
||||||
@@ -33,9 +34,11 @@ require (
|
|||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
github.com/bytedance/sonic v1.11.6 // indirect
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
@@ -55,7 +58,7 @@ require (
|
|||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.17.6 // indirect
|
github.com/klauspost/compress v1.18.1 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.6 // indirect
|
github.com/mailru/easyjson v0.7.6 // indirect
|
||||||
@@ -65,36 +68,38 @@ require (
|
|||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
golang.org/x/crypto v0.32.0 // indirect
|
golang.org/x/crypto v0.46.0 // indirect
|
||||||
golang.org/x/mod v0.17.0 // indirect
|
golang.org/x/mod v0.31.0 // indirect
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
golang.org/x/tools v0.40.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/fatih/color v1.17.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/go-faster/yaml v0.4.6 // indirect
|
github.com/go-faster/yaml v0.4.6 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
// github.com/golang/protobuf v1.5.4 // indirect
|
// github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/segmentio/asm v1.2.0 // indirect
|
github.com/segmentio/asm v1.2.1 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.0 // indirect
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||||
golang.org/x/net v0.34.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/sync v0.12.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
69
go.sum
69
go.sum
@@ -14,12 +14,18 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
|
|||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
@@ -39,8 +45,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
|
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
|
||||||
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
|
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
@@ -49,6 +59,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
|
|||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
@@ -63,11 +75,13 @@ github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AY
|
|||||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||||
github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg=
|
github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg=
|
||||||
github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg=
|
github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg=
|
||||||
|
github.com/go-faster/jx v1.2.0 h1:T2YHJPrFaYu21fJtUxC9GzmluKu8rVIFDwwGBKTDseI=
|
||||||
|
github.com/go-faster/jx v1.2.0/go.mod h1:UWLOVDmMG597a5tBFPLIWJdUxz5/2emOpfsj9Neg0PE=
|
||||||
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
|
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
|
||||||
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
|
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
@@ -113,8 +127,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -140,6 +154,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
|||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
||||||
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||||
|
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||||
|
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
@@ -159,6 +175,8 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA
|
|||||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
@@ -176,11 +194,15 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
|
|||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/ogen-go/ogen v1.2.1 h1:C5A0lvUMu2wl+eWIxnpXMWnuOJ26a2FyzR1CIC2qG0M=
|
github.com/ogen-go/ogen v1.2.1 h1:C5A0lvUMu2wl+eWIxnpXMWnuOJ26a2FyzR1CIC2qG0M=
|
||||||
github.com/ogen-go/ogen v1.2.1/go.mod h1:P2zQdEu8UqaVRfD5GEFvl+9q63VjMLvDquq1wVbyInM=
|
github.com/ogen-go/ogen v1.2.1/go.mod h1:P2zQdEu8UqaVRfD5GEFvl+9q63VjMLvDquq1wVbyInM=
|
||||||
|
github.com/ogen-go/ogen v1.18.0 h1:6RQ7lFBjOeNaUWu4getfqIh4GJbEY4hqKuzDtec/g60=
|
||||||
|
github.com/ogen-go/ogen v1.18.0/go.mod h1:dHFr2Wf6cA7tSxMI+zPC21UR5hAlDw8ZYUkK3PziURY=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
@@ -188,6 +210,10 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
|
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||||
|
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
@@ -204,8 +230,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||||
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||||
@@ -221,12 +248,14 @@ github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5
|
|||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
@@ -234,6 +263,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
|||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
@@ -242,15 +273,21 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
|
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
|
||||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||||
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||||
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
|
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -266,6 +303,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -275,6 +314,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -293,6 +334,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -303,6 +346,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
@@ -312,6 +357,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
307
openapi.yaml
307
openapi.yaml
@@ -14,15 +14,41 @@ tags:
|
|||||||
description: Long-running operations
|
description: Long-running operations
|
||||||
- name: Session
|
- name: Session
|
||||||
description: Session queries
|
description: Session queries
|
||||||
|
- name: Stats
|
||||||
|
description: Statistics queries
|
||||||
- name: Submissions
|
- name: Submissions
|
||||||
description: Submission operations
|
description: Submission operations
|
||||||
- name: Scripts
|
- name: Scripts
|
||||||
description: Script operations
|
description: Script operations
|
||||||
- name: ScriptPolicy
|
- name: ScriptPolicy
|
||||||
description: Script policy operations
|
description: Script policy operations
|
||||||
|
- name: Thumbnails
|
||||||
|
description: Thumbnail operations
|
||||||
|
- name: Users
|
||||||
|
description: User operations
|
||||||
security:
|
security:
|
||||||
- cookieAuth: []
|
- cookieAuth: []
|
||||||
paths:
|
paths:
|
||||||
|
/stats:
|
||||||
|
get:
|
||||||
|
summary: Get aggregate statistics
|
||||||
|
operationId: getStats
|
||||||
|
tags:
|
||||||
|
- Stats
|
||||||
|
security: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Successful response
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Stats"
|
||||||
|
default:
|
||||||
|
description: General Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
/session/user:
|
/session/user:
|
||||||
get:
|
get:
|
||||||
summary: Get information about the currently logged in user
|
summary: Get information about the currently logged in user
|
||||||
@@ -421,6 +447,30 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
/mapfixes/{MapfixID}/description:
|
||||||
|
patch:
|
||||||
|
summary: Update description (submitter only)
|
||||||
|
operationId: updateMapfixDescription
|
||||||
|
tags:
|
||||||
|
- Mapfixes
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/MapfixID'
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
maxLength: 256
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Successful response
|
||||||
|
default:
|
||||||
|
description: General Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
/mapfixes/{MapfixID}/completed:
|
/mapfixes/{MapfixID}/completed:
|
||||||
post:
|
post:
|
||||||
summary: Called by maptest when a player completes the map
|
summary: Called by maptest when a player completes the map
|
||||||
@@ -1438,6 +1488,222 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
|
/thumbnails/assets:
|
||||||
|
post:
|
||||||
|
summary: Batch fetch asset thumbnails
|
||||||
|
operationId: batchAssetThumbnails
|
||||||
|
tags:
|
||||||
|
- Thumbnails
|
||||||
|
security: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- assetIds
|
||||||
|
properties:
|
||||||
|
assetIds:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
maxItems: 100
|
||||||
|
description: Array of asset IDs (max 100)
|
||||||
|
size:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- "150x150"
|
||||||
|
- "420x420"
|
||||||
|
- "768x432"
|
||||||
|
default: "420x420"
|
||||||
|
description: Thumbnail size
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Successful response
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
thumbnails:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: Map of asset ID to thumbnail URL
|
||||||
|
default:
|
||||||
|
description: General Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
/thumbnails/asset/{AssetID}:
|
||||||
|
get:
|
||||||
|
summary: Get single asset thumbnail
|
||||||
|
operationId: getAssetThumbnail
|
||||||
|
tags:
|
||||||
|
- Thumbnails
|
||||||
|
security: []
|
||||||
|
parameters:
|
||||||
|
- name: AssetID
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
- name: size
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- "150x150"
|
||||||
|
- "420x420"
|
||||||
|
- "768x432"
|
||||||
|
default: "420x420"
|
||||||
|
responses:
|
||||||
|
"302":
|
||||||
|
description: Redirect to thumbnail URL
|
||||||
|
headers:
|
||||||
|
Location:
|
||||||
|
description: URL to redirect to
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
default:
|
||||||
|
description: General Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
/thumbnails/users:
|
||||||
|
post:
|
||||||
|
summary: Batch fetch user avatar thumbnails
|
||||||
|
operationId: batchUserThumbnails
|
||||||
|
tags:
|
||||||
|
- Thumbnails
|
||||||
|
security: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- userIds
|
||||||
|
properties:
|
||||||
|
userIds:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
maxItems: 100
|
||||||
|
description: Array of user IDs (max 100)
|
||||||
|
size:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- "150x150"
|
||||||
|
- "420x420"
|
||||||
|
- "768x432"
|
||||||
|
default: "150x150"
|
||||||
|
description: Thumbnail size
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Successful response
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
thumbnails:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: Map of user ID to thumbnail URL
|
||||||
|
default:
|
||||||
|
description: General Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
/thumbnails/user/{UserID}:
|
||||||
|
get:
|
||||||
|
summary: Get single user avatar thumbnail
|
||||||
|
operationId: getUserThumbnail
|
||||||
|
tags:
|
||||||
|
- Thumbnails
|
||||||
|
security: []
|
||||||
|
parameters:
|
||||||
|
- name: UserID
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
- name: size
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- "150x150"
|
||||||
|
- "420x420"
|
||||||
|
- "768x432"
|
||||||
|
default: "150x150"
|
||||||
|
responses:
|
||||||
|
"302":
|
||||||
|
description: Redirect to thumbnail URL
|
||||||
|
headers:
|
||||||
|
Location:
|
||||||
|
description: URL to redirect to
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
default:
|
||||||
|
description: General Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
/usernames:
|
||||||
|
post:
|
||||||
|
summary: Batch fetch usernames
|
||||||
|
operationId: batchUsernames
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
|
security: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- userIds
|
||||||
|
properties:
|
||||||
|
userIds:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
maxItems: 100
|
||||||
|
description: Array of user IDs (max 100)
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Successful response
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
usernames:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: Map of user ID to username
|
||||||
|
default:
|
||||||
|
description: General Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
components:
|
components:
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
cookieAuth:
|
cookieAuth:
|
||||||
@@ -2061,6 +2327,47 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
minimum: 0
|
minimum: 0
|
||||||
|
Stats:
|
||||||
|
description: Aggregate statistics for submissions and mapfixes
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
TotalSubmissions:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
description: Total number of submissions
|
||||||
|
TotalMapfixes:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
description: Total number of mapfixes
|
||||||
|
ReleasedSubmissions:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
description: Number of released submissions
|
||||||
|
ReleasedMapfixes:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
description: Number of released mapfixes
|
||||||
|
SubmittedSubmissions:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
description: Number of submissions under review
|
||||||
|
SubmittedMapfixes:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
description: Number of mapfixes under review
|
||||||
|
required:
|
||||||
|
- TotalSubmissions
|
||||||
|
- TotalMapfixes
|
||||||
|
- ReleasedSubmissions
|
||||||
|
- ReleasedMapfixes
|
||||||
|
- SubmittedSubmissions
|
||||||
|
- SubmittedMapfixes
|
||||||
Error:
|
Error:
|
||||||
description: Represents error object
|
description: Represents error object
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
"go.opentelemetry.io/otel/metric"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
|
||||||
|
|
||||||
ht "github.com/ogen-go/ogen/http"
|
ht "github.com/ogen-go/ogen/http"
|
||||||
"github.com/ogen-go/ogen/middleware"
|
"github.com/ogen-go/ogen/middleware"
|
||||||
"github.com/ogen-go/ogen/ogenerrors"
|
"github.com/ogen-go/ogen/ogenerrors"
|
||||||
"github.com/ogen-go/ogen/otelogen"
|
"github.com/ogen-go/ogen/otelogen"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -32,6 +32,7 @@ type otelConfig struct {
|
|||||||
Tracer trace.Tracer
|
Tracer trace.Tracer
|
||||||
MeterProvider metric.MeterProvider
|
MeterProvider metric.MeterProvider
|
||||||
Meter metric.Meter
|
Meter metric.Meter
|
||||||
|
Attributes []attribute.KeyValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *otelConfig) initOTEL() {
|
func (cfg *otelConfig) initOTEL() {
|
||||||
@@ -215,6 +216,13 @@ func WithMeterProvider(provider metric.MeterProvider) Option {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithAttributes specifies default otel attributes.
|
||||||
|
func WithAttributes(attributes ...attribute.KeyValue) Option {
|
||||||
|
return otelOptionFunc(func(cfg *otelConfig) {
|
||||||
|
cfg.Attributes = attributes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// WithClient specifies http client to use.
|
// WithClient specifies http client to use.
|
||||||
func WithClient(client ht.Client) ClientOption {
|
func WithClient(client ht.Client) ClientOption {
|
||||||
return optionFunc[clientConfig](func(cfg *clientConfig) {
|
return optionFunc[clientConfig](func(cfg *clientConfig) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
19
pkg/api/oas_defaults_gen.go
Normal file
19
pkg/api/oas_defaults_gen.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Code generated by ogen, DO NOT EDIT.
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
// setDefaults set default value of fields.
|
||||||
|
func (s *BatchAssetThumbnailsReq) setDefaults() {
|
||||||
|
{
|
||||||
|
val := BatchAssetThumbnailsReqSize("420x420")
|
||||||
|
s.Size.SetTo(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDefaults set default value of fields.
|
||||||
|
func (s *BatchUserThumbnailsReq) setDefaults() {
|
||||||
|
{
|
||||||
|
val := BatchUserThumbnailsReqSize("150x150")
|
||||||
|
s.Size.SetTo(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,9 @@ const (
|
|||||||
ActionSubmissionTriggerUploadOperation OperationName = "ActionSubmissionTriggerUpload"
|
ActionSubmissionTriggerUploadOperation OperationName = "ActionSubmissionTriggerUpload"
|
||||||
ActionSubmissionTriggerValidateOperation OperationName = "ActionSubmissionTriggerValidate"
|
ActionSubmissionTriggerValidateOperation OperationName = "ActionSubmissionTriggerValidate"
|
||||||
ActionSubmissionValidatedOperation OperationName = "ActionSubmissionValidated"
|
ActionSubmissionValidatedOperation OperationName = "ActionSubmissionValidated"
|
||||||
|
BatchAssetThumbnailsOperation OperationName = "BatchAssetThumbnails"
|
||||||
|
BatchUserThumbnailsOperation OperationName = "BatchUserThumbnails"
|
||||||
|
BatchUsernamesOperation OperationName = "BatchUsernames"
|
||||||
CreateMapfixOperation OperationName = "CreateMapfix"
|
CreateMapfixOperation OperationName = "CreateMapfix"
|
||||||
CreateMapfixAuditCommentOperation OperationName = "CreateMapfixAuditComment"
|
CreateMapfixAuditCommentOperation OperationName = "CreateMapfixAuditComment"
|
||||||
CreateScriptOperation OperationName = "CreateScript"
|
CreateScriptOperation OperationName = "CreateScript"
|
||||||
@@ -40,12 +43,15 @@ const (
|
|||||||
DeleteScriptOperation OperationName = "DeleteScript"
|
DeleteScriptOperation OperationName = "DeleteScript"
|
||||||
DeleteScriptPolicyOperation OperationName = "DeleteScriptPolicy"
|
DeleteScriptPolicyOperation OperationName = "DeleteScriptPolicy"
|
||||||
DownloadMapAssetOperation OperationName = "DownloadMapAsset"
|
DownloadMapAssetOperation OperationName = "DownloadMapAsset"
|
||||||
|
GetAssetThumbnailOperation OperationName = "GetAssetThumbnail"
|
||||||
GetMapOperation OperationName = "GetMap"
|
GetMapOperation OperationName = "GetMap"
|
||||||
GetMapfixOperation OperationName = "GetMapfix"
|
GetMapfixOperation OperationName = "GetMapfix"
|
||||||
GetOperationOperation OperationName = "GetOperation"
|
GetOperationOperation OperationName = "GetOperation"
|
||||||
GetScriptOperation OperationName = "GetScript"
|
GetScriptOperation OperationName = "GetScript"
|
||||||
GetScriptPolicyOperation OperationName = "GetScriptPolicy"
|
GetScriptPolicyOperation OperationName = "GetScriptPolicy"
|
||||||
|
GetStatsOperation OperationName = "GetStats"
|
||||||
GetSubmissionOperation OperationName = "GetSubmission"
|
GetSubmissionOperation OperationName = "GetSubmission"
|
||||||
|
GetUserThumbnailOperation OperationName = "GetUserThumbnail"
|
||||||
ListMapfixAuditEventsOperation OperationName = "ListMapfixAuditEvents"
|
ListMapfixAuditEventsOperation OperationName = "ListMapfixAuditEvents"
|
||||||
ListMapfixesOperation OperationName = "ListMapfixes"
|
ListMapfixesOperation OperationName = "ListMapfixes"
|
||||||
ListMapsOperation OperationName = "ListMaps"
|
ListMapsOperation OperationName = "ListMaps"
|
||||||
@@ -59,6 +65,7 @@ const (
|
|||||||
SessionValidateOperation OperationName = "SessionValidate"
|
SessionValidateOperation OperationName = "SessionValidate"
|
||||||
SetMapfixCompletedOperation OperationName = "SetMapfixCompleted"
|
SetMapfixCompletedOperation OperationName = "SetMapfixCompleted"
|
||||||
SetSubmissionCompletedOperation OperationName = "SetSubmissionCompleted"
|
SetSubmissionCompletedOperation OperationName = "SetSubmissionCompleted"
|
||||||
|
UpdateMapfixDescriptionOperation OperationName = "UpdateMapfixDescription"
|
||||||
UpdateMapfixModelOperation OperationName = "UpdateMapfixModel"
|
UpdateMapfixModelOperation OperationName = "UpdateMapfixModel"
|
||||||
UpdateScriptOperation OperationName = "UpdateScript"
|
UpdateScriptOperation OperationName = "UpdateScript"
|
||||||
UpdateScriptPolicyOperation OperationName = "UpdateScriptPolicy"
|
UpdateScriptPolicyOperation OperationName = "UpdateScriptPolicy"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
@@ -10,13 +11,13 @@ import (
|
|||||||
|
|
||||||
"github.com/go-faster/errors"
|
"github.com/go-faster/errors"
|
||||||
"github.com/go-faster/jx"
|
"github.com/go-faster/jx"
|
||||||
|
|
||||||
"github.com/ogen-go/ogen/ogenerrors"
|
"github.com/ogen-go/ogen/ogenerrors"
|
||||||
"github.com/ogen-go/ogen/validate"
|
"github.com/ogen-go/ogen/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
func (s *Server) decodeBatchAssetThumbnailsRequest(r *http.Request) (
|
||||||
req *MapfixTriggerCreate,
|
req *BatchAssetThumbnailsReq,
|
||||||
|
rawBody []byte,
|
||||||
close func() error,
|
close func() error,
|
||||||
rerr error,
|
rerr error,
|
||||||
) {
|
) {
|
||||||
@@ -37,22 +38,266 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
|||||||
}()
|
}()
|
||||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, errors.Wrap(err, "parse media type")
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case ct == "application/json":
|
case ct == "application/json":
|
||||||
if r.ContentLength == 0 {
|
if r.ContentLength == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
buf, err := io.ReadAll(r.Body)
|
buf, err := io.ReadAll(r.Body)
|
||||||
|
defer func() {
|
||||||
|
_ = r.Body.Close()
|
||||||
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the body to allow for downstream reading.
|
||||||
|
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||||
|
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawBody = append(rawBody, buf...)
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var request BatchAssetThumbnailsReq
|
||||||
|
if err := func() error {
|
||||||
|
if err := request.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return req, rawBody, close, err
|
||||||
|
}
|
||||||
|
if err := func() error {
|
||||||
|
if err := request.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||||
|
}
|
||||||
|
return &request, rawBody, close, nil
|
||||||
|
default:
|
||||||
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) decodeBatchUserThumbnailsRequest(r *http.Request) (
|
||||||
|
req *BatchUserThumbnailsReq,
|
||||||
|
rawBody []byte,
|
||||||
|
close func() error,
|
||||||
|
rerr error,
|
||||||
|
) {
|
||||||
|
var closers []func() error
|
||||||
|
close = func() error {
|
||||||
|
var merr error
|
||||||
|
// Close in reverse order, to match defer behavior.
|
||||||
|
for i := len(closers) - 1; i >= 0; i-- {
|
||||||
|
c := closers[i]
|
||||||
|
merr = errors.Join(merr, c())
|
||||||
|
}
|
||||||
|
return merr
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if rerr != nil {
|
||||||
|
rerr = errors.Join(rerr, close())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
if r.ContentLength == 0 {
|
||||||
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
|
}
|
||||||
|
buf, err := io.ReadAll(r.Body)
|
||||||
|
defer func() {
|
||||||
|
_ = r.Body.Close()
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return req, rawBody, close, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the body to allow for downstream reading.
|
||||||
|
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
rawBody = append(rawBody, buf...)
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var request BatchUserThumbnailsReq
|
||||||
|
if err := func() error {
|
||||||
|
if err := request.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return req, rawBody, close, err
|
||||||
|
}
|
||||||
|
if err := func() error {
|
||||||
|
if err := request.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||||
|
}
|
||||||
|
return &request, rawBody, close, nil
|
||||||
|
default:
|
||||||
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) decodeBatchUsernamesRequest(r *http.Request) (
|
||||||
|
req *BatchUsernamesReq,
|
||||||
|
rawBody []byte,
|
||||||
|
close func() error,
|
||||||
|
rerr error,
|
||||||
|
) {
|
||||||
|
var closers []func() error
|
||||||
|
close = func() error {
|
||||||
|
var merr error
|
||||||
|
// Close in reverse order, to match defer behavior.
|
||||||
|
for i := len(closers) - 1; i >= 0; i-- {
|
||||||
|
c := closers[i]
|
||||||
|
merr = errors.Join(merr, c())
|
||||||
|
}
|
||||||
|
return merr
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if rerr != nil {
|
||||||
|
rerr = errors.Join(rerr, close())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
if r.ContentLength == 0 {
|
||||||
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
|
}
|
||||||
|
buf, err := io.ReadAll(r.Body)
|
||||||
|
defer func() {
|
||||||
|
_ = r.Body.Close()
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return req, rawBody, close, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the body to allow for downstream reading.
|
||||||
|
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
rawBody = append(rawBody, buf...)
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var request BatchUsernamesReq
|
||||||
|
if err := func() error {
|
||||||
|
if err := request.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return req, rawBody, close, err
|
||||||
|
}
|
||||||
|
if err := func() error {
|
||||||
|
if err := request.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||||
|
}
|
||||||
|
return &request, rawBody, close, nil
|
||||||
|
default:
|
||||||
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
||||||
|
req *MapfixTriggerCreate,
|
||||||
|
rawBody []byte,
|
||||||
|
close func() error,
|
||||||
|
rerr error,
|
||||||
|
) {
|
||||||
|
var closers []func() error
|
||||||
|
close = func() error {
|
||||||
|
var merr error
|
||||||
|
// Close in reverse order, to match defer behavior.
|
||||||
|
for i := len(closers) - 1; i >= 0; i-- {
|
||||||
|
c := closers[i]
|
||||||
|
merr = errors.Join(merr, c())
|
||||||
|
}
|
||||||
|
return merr
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if rerr != nil {
|
||||||
|
rerr = errors.Join(rerr, close())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
if r.ContentLength == 0 {
|
||||||
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
|
}
|
||||||
|
buf, err := io.ReadAll(r.Body)
|
||||||
|
defer func() {
|
||||||
|
_ = r.Body.Close()
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return req, rawBody, close, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the body to allow for downstream reading.
|
||||||
|
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
rawBody = append(rawBody, buf...)
|
||||||
d := jx.DecodeBytes(buf)
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
var request MapfixTriggerCreate
|
var request MapfixTriggerCreate
|
||||||
@@ -70,7 +315,7 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
|||||||
Body: buf,
|
Body: buf,
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
if err := request.Validate(); err != nil {
|
if err := request.Validate(); err != nil {
|
||||||
@@ -78,16 +323,17 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}(); err != nil {
|
}(); err != nil {
|
||||||
return req, close, errors.Wrap(err, "validate")
|
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||||
}
|
}
|
||||||
return &request, close, nil
|
return &request, rawBody, close, nil
|
||||||
default:
|
default:
|
||||||
return req, close, validate.InvalidContentType(ct)
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) (
|
func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) (
|
||||||
req CreateMapfixAuditCommentReq,
|
req CreateMapfixAuditCommentReq,
|
||||||
|
rawBody []byte,
|
||||||
close func() error,
|
close func() error,
|
||||||
rerr error,
|
rerr error,
|
||||||
) {
|
) {
|
||||||
@@ -108,20 +354,21 @@ func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) (
|
|||||||
}()
|
}()
|
||||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, errors.Wrap(err, "parse media type")
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case ct == "text/plain":
|
case ct == "text/plain":
|
||||||
reader := r.Body
|
reader := r.Body
|
||||||
request := CreateMapfixAuditCommentReq{Data: reader}
|
request := CreateMapfixAuditCommentReq{Data: reader}
|
||||||
return request, close, nil
|
return request, rawBody, close, nil
|
||||||
default:
|
default:
|
||||||
return req, close, validate.InvalidContentType(ct)
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
||||||
req *ScriptCreate,
|
req *ScriptCreate,
|
||||||
|
rawBody []byte,
|
||||||
close func() error,
|
close func() error,
|
||||||
rerr error,
|
rerr error,
|
||||||
) {
|
) {
|
||||||
@@ -142,22 +389,29 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
|||||||
}()
|
}()
|
||||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, errors.Wrap(err, "parse media type")
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case ct == "application/json":
|
case ct == "application/json":
|
||||||
if r.ContentLength == 0 {
|
if r.ContentLength == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
buf, err := io.ReadAll(r.Body)
|
buf, err := io.ReadAll(r.Body)
|
||||||
|
defer func() {
|
||||||
|
_ = r.Body.Close()
|
||||||
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the body to allow for downstream reading.
|
||||||
|
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||||
|
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawBody = append(rawBody, buf...)
|
||||||
d := jx.DecodeBytes(buf)
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
var request ScriptCreate
|
var request ScriptCreate
|
||||||
@@ -175,7 +429,7 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
|||||||
Body: buf,
|
Body: buf,
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
if err := request.Validate(); err != nil {
|
if err := request.Validate(); err != nil {
|
||||||
@@ -183,16 +437,17 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}(); err != nil {
|
}(); err != nil {
|
||||||
return req, close, errors.Wrap(err, "validate")
|
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||||
}
|
}
|
||||||
return &request, close, nil
|
return &request, rawBody, close, nil
|
||||||
default:
|
default:
|
||||||
return req, close, validate.InvalidContentType(ct)
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
||||||
req *ScriptPolicyCreate,
|
req *ScriptPolicyCreate,
|
||||||
|
rawBody []byte,
|
||||||
close func() error,
|
close func() error,
|
||||||
rerr error,
|
rerr error,
|
||||||
) {
|
) {
|
||||||
@@ -213,22 +468,29 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
|||||||
}()
|
}()
|
||||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, errors.Wrap(err, "parse media type")
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case ct == "application/json":
|
case ct == "application/json":
|
||||||
if r.ContentLength == 0 {
|
if r.ContentLength == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
buf, err := io.ReadAll(r.Body)
|
buf, err := io.ReadAll(r.Body)
|
||||||
|
defer func() {
|
||||||
|
_ = r.Body.Close()
|
||||||
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the body to allow for downstream reading.
|
||||||
|
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||||
|
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawBody = append(rawBody, buf...)
|
||||||
d := jx.DecodeBytes(buf)
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
var request ScriptPolicyCreate
|
var request ScriptPolicyCreate
|
||||||
@@ -246,7 +508,7 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
|||||||
Body: buf,
|
Body: buf,
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
if err := request.Validate(); err != nil {
|
if err := request.Validate(); err != nil {
|
||||||
@@ -254,16 +516,17 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}(); err != nil {
|
}(); err != nil {
|
||||||
return req, close, errors.Wrap(err, "validate")
|
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||||
}
|
}
|
||||||
return &request, close, nil
|
return &request, rawBody, close, nil
|
||||||
default:
|
default:
|
||||||
return req, close, validate.InvalidContentType(ct)
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
||||||
req *SubmissionTriggerCreate,
|
req *SubmissionTriggerCreate,
|
||||||
|
rawBody []byte,
|
||||||
close func() error,
|
close func() error,
|
||||||
rerr error,
|
rerr error,
|
||||||
) {
|
) {
|
||||||
@@ -284,22 +547,29 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
|||||||
}()
|
}()
|
||||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, errors.Wrap(err, "parse media type")
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case ct == "application/json":
|
case ct == "application/json":
|
||||||
if r.ContentLength == 0 {
|
if r.ContentLength == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
buf, err := io.ReadAll(r.Body)
|
buf, err := io.ReadAll(r.Body)
|
||||||
|
defer func() {
|
||||||
|
_ = r.Body.Close()
|
||||||
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the body to allow for downstream reading.
|
||||||
|
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||||
|
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawBody = append(rawBody, buf...)
|
||||||
d := jx.DecodeBytes(buf)
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
var request SubmissionTriggerCreate
|
var request SubmissionTriggerCreate
|
||||||
@@ -317,7 +587,7 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
|||||||
Body: buf,
|
Body: buf,
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
if err := request.Validate(); err != nil {
|
if err := request.Validate(); err != nil {
|
||||||
@@ -325,16 +595,17 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}(); err != nil {
|
}(); err != nil {
|
||||||
return req, close, errors.Wrap(err, "validate")
|
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||||
}
|
}
|
||||||
return &request, close, nil
|
return &request, rawBody, close, nil
|
||||||
default:
|
default:
|
||||||
return req, close, validate.InvalidContentType(ct)
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
||||||
req *SubmissionTriggerCreate,
|
req *SubmissionTriggerCreate,
|
||||||
|
rawBody []byte,
|
||||||
close func() error,
|
close func() error,
|
||||||
rerr error,
|
rerr error,
|
||||||
) {
|
) {
|
||||||
@@ -355,22 +626,29 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
|||||||
}()
|
}()
|
||||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, errors.Wrap(err, "parse media type")
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case ct == "application/json":
|
case ct == "application/json":
|
||||||
if r.ContentLength == 0 {
|
if r.ContentLength == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
buf, err := io.ReadAll(r.Body)
|
buf, err := io.ReadAll(r.Body)
|
||||||
|
defer func() {
|
||||||
|
_ = r.Body.Close()
|
||||||
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the body to allow for downstream reading.
|
||||||
|
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||||
|
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawBody = append(rawBody, buf...)
|
||||||
d := jx.DecodeBytes(buf)
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
var request SubmissionTriggerCreate
|
var request SubmissionTriggerCreate
|
||||||
@@ -388,7 +666,7 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
|||||||
Body: buf,
|
Body: buf,
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
if err := request.Validate(); err != nil {
|
if err := request.Validate(); err != nil {
|
||||||
@@ -396,16 +674,17 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}(); err != nil {
|
}(); err != nil {
|
||||||
return req, close, errors.Wrap(err, "validate")
|
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||||
}
|
}
|
||||||
return &request, close, nil
|
return &request, rawBody, close, nil
|
||||||
default:
|
default:
|
||||||
return req, close, validate.InvalidContentType(ct)
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
|
func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
|
||||||
req CreateSubmissionAuditCommentReq,
|
req CreateSubmissionAuditCommentReq,
|
||||||
|
rawBody []byte,
|
||||||
close func() error,
|
close func() error,
|
||||||
rerr error,
|
rerr error,
|
||||||
) {
|
) {
|
||||||
@@ -426,20 +705,21 @@ func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
|
|||||||
}()
|
}()
|
||||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, errors.Wrap(err, "parse media type")
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case ct == "text/plain":
|
case ct == "text/plain":
|
||||||
reader := r.Body
|
reader := r.Body
|
||||||
request := CreateSubmissionAuditCommentReq{Data: reader}
|
request := CreateSubmissionAuditCommentReq{Data: reader}
|
||||||
return request, close, nil
|
return request, rawBody, close, nil
|
||||||
default:
|
default:
|
||||||
return req, close, validate.InvalidContentType(ct)
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
||||||
req []ReleaseInfo,
|
req []ReleaseInfo,
|
||||||
|
rawBody []byte,
|
||||||
close func() error,
|
close func() error,
|
||||||
rerr error,
|
rerr error,
|
||||||
) {
|
) {
|
||||||
@@ -460,22 +740,29 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
|||||||
}()
|
}()
|
||||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, errors.Wrap(err, "parse media type")
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case ct == "application/json":
|
case ct == "application/json":
|
||||||
if r.ContentLength == 0 {
|
if r.ContentLength == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
buf, err := io.ReadAll(r.Body)
|
buf, err := io.ReadAll(r.Body)
|
||||||
|
defer func() {
|
||||||
|
_ = r.Body.Close()
|
||||||
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the body to allow for downstream reading.
|
||||||
|
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||||
|
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawBody = append(rawBody, buf...)
|
||||||
d := jx.DecodeBytes(buf)
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
var request []ReleaseInfo
|
var request []ReleaseInfo
|
||||||
@@ -501,7 +788,7 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
|||||||
Body: buf,
|
Body: buf,
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
if request == nil {
|
if request == nil {
|
||||||
@@ -534,16 +821,17 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}(); err != nil {
|
}(); err != nil {
|
||||||
return req, close, errors.Wrap(err, "validate")
|
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||||
}
|
}
|
||||||
return request, close, nil
|
return request, rawBody, close, nil
|
||||||
default:
|
default:
|
||||||
return req, close, validate.InvalidContentType(ct)
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
func (s *Server) decodeUpdateMapfixDescriptionRequest(r *http.Request) (
|
||||||
req *ScriptUpdate,
|
req UpdateMapfixDescriptionReq,
|
||||||
|
rawBody []byte,
|
||||||
close func() error,
|
close func() error,
|
||||||
rerr error,
|
rerr error,
|
||||||
) {
|
) {
|
||||||
@@ -564,22 +852,64 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
|||||||
}()
|
}()
|
||||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, errors.Wrap(err, "parse media type")
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "text/plain":
|
||||||
|
reader := r.Body
|
||||||
|
request := UpdateMapfixDescriptionReq{Data: reader}
|
||||||
|
return request, rawBody, close, nil
|
||||||
|
default:
|
||||||
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
||||||
|
req *ScriptUpdate,
|
||||||
|
rawBody []byte,
|
||||||
|
close func() error,
|
||||||
|
rerr error,
|
||||||
|
) {
|
||||||
|
var closers []func() error
|
||||||
|
close = func() error {
|
||||||
|
var merr error
|
||||||
|
// Close in reverse order, to match defer behavior.
|
||||||
|
for i := len(closers) - 1; i >= 0; i-- {
|
||||||
|
c := closers[i]
|
||||||
|
merr = errors.Join(merr, c())
|
||||||
|
}
|
||||||
|
return merr
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if rerr != nil {
|
||||||
|
rerr = errors.Join(rerr, close())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case ct == "application/json":
|
case ct == "application/json":
|
||||||
if r.ContentLength == 0 {
|
if r.ContentLength == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
buf, err := io.ReadAll(r.Body)
|
buf, err := io.ReadAll(r.Body)
|
||||||
|
defer func() {
|
||||||
|
_ = r.Body.Close()
|
||||||
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the body to allow for downstream reading.
|
||||||
|
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||||
|
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawBody = append(rawBody, buf...)
|
||||||
d := jx.DecodeBytes(buf)
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
var request ScriptUpdate
|
var request ScriptUpdate
|
||||||
@@ -597,7 +927,7 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
|||||||
Body: buf,
|
Body: buf,
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
if err := request.Validate(); err != nil {
|
if err := request.Validate(); err != nil {
|
||||||
@@ -605,16 +935,17 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}(); err != nil {
|
}(); err != nil {
|
||||||
return req, close, errors.Wrap(err, "validate")
|
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||||
}
|
}
|
||||||
return &request, close, nil
|
return &request, rawBody, close, nil
|
||||||
default:
|
default:
|
||||||
return req, close, validate.InvalidContentType(ct)
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
||||||
req *ScriptPolicyUpdate,
|
req *ScriptPolicyUpdate,
|
||||||
|
rawBody []byte,
|
||||||
close func() error,
|
close func() error,
|
||||||
rerr error,
|
rerr error,
|
||||||
) {
|
) {
|
||||||
@@ -635,22 +966,29 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
|||||||
}()
|
}()
|
||||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, errors.Wrap(err, "parse media type")
|
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case ct == "application/json":
|
case ct == "application/json":
|
||||||
if r.ContentLength == 0 {
|
if r.ContentLength == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
buf, err := io.ReadAll(r.Body)
|
buf, err := io.ReadAll(r.Body)
|
||||||
|
defer func() {
|
||||||
|
_ = r.Body.Close()
|
||||||
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the body to allow for downstream reading.
|
||||||
|
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||||
|
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
return req, close, validate.ErrBodyRequired
|
return req, rawBody, close, validate.ErrBodyRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawBody = append(rawBody, buf...)
|
||||||
d := jx.DecodeBytes(buf)
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
var request ScriptPolicyUpdate
|
var request ScriptPolicyUpdate
|
||||||
@@ -668,7 +1006,7 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
|||||||
Body: buf,
|
Body: buf,
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
return req, close, err
|
return req, rawBody, close, err
|
||||||
}
|
}
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
if err := request.Validate(); err != nil {
|
if err := request.Validate(); err != nil {
|
||||||
@@ -676,10 +1014,10 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}(); err != nil {
|
}(); err != nil {
|
||||||
return req, close, errors.Wrap(err, "validate")
|
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||||
}
|
}
|
||||||
return &request, close, nil
|
return &request, rawBody, close, nil
|
||||||
default:
|
default:
|
||||||
return req, close, validate.InvalidContentType(ct)
|
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,51 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-faster/jx"
|
"github.com/go-faster/jx"
|
||||||
|
|
||||||
ht "github.com/ogen-go/ogen/http"
|
ht "github.com/ogen-go/ogen/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func encodeBatchAssetThumbnailsRequest(
|
||||||
|
req *BatchAssetThumbnailsReq,
|
||||||
|
r *http.Request,
|
||||||
|
) error {
|
||||||
|
const contentType = "application/json"
|
||||||
|
e := new(jx.Encoder)
|
||||||
|
{
|
||||||
|
req.Encode(e)
|
||||||
|
}
|
||||||
|
encoded := e.Bytes()
|
||||||
|
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeBatchUserThumbnailsRequest(
|
||||||
|
req *BatchUserThumbnailsReq,
|
||||||
|
r *http.Request,
|
||||||
|
) error {
|
||||||
|
const contentType = "application/json"
|
||||||
|
e := new(jx.Encoder)
|
||||||
|
{
|
||||||
|
req.Encode(e)
|
||||||
|
}
|
||||||
|
encoded := e.Bytes()
|
||||||
|
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeBatchUsernamesRequest(
|
||||||
|
req *BatchUsernamesReq,
|
||||||
|
r *http.Request,
|
||||||
|
) error {
|
||||||
|
const contentType = "application/json"
|
||||||
|
e := new(jx.Encoder)
|
||||||
|
{
|
||||||
|
req.Encode(e)
|
||||||
|
}
|
||||||
|
encoded := e.Bytes()
|
||||||
|
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func encodeCreateMapfixRequest(
|
func encodeCreateMapfixRequest(
|
||||||
req *MapfixTriggerCreate,
|
req *MapfixTriggerCreate,
|
||||||
r *http.Request,
|
r *http.Request,
|
||||||
@@ -119,6 +160,16 @@ func encodeReleaseSubmissionsRequest(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeUpdateMapfixDescriptionRequest(
|
||||||
|
req UpdateMapfixDescriptionReq,
|
||||||
|
r *http.Request,
|
||||||
|
) error {
|
||||||
|
const contentType = "text/plain"
|
||||||
|
body := req
|
||||||
|
ht.SetBody(r, body, contentType)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func encodeUpdateScriptRequest(
|
func encodeUpdateScriptRequest(
|
||||||
req *ScriptUpdate,
|
req *ScriptUpdate,
|
||||||
r *http.Request,
|
r *http.Request,
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import (
|
|||||||
|
|
||||||
"github.com/go-faster/errors"
|
"github.com/go-faster/errors"
|
||||||
"github.com/go-faster/jx"
|
"github.com/go-faster/jx"
|
||||||
|
"github.com/ogen-go/ogen/conv"
|
||||||
"github.com/ogen-go/ogen/ogenerrors"
|
"github.com/ogen-go/ogen/ogenerrors"
|
||||||
|
"github.com/ogen-go/ogen/uri"
|
||||||
"github.com/ogen-go/ogen/validate"
|
"github.com/ogen-go/ogen/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1456,6 +1457,282 @@ func decodeActionSubmissionValidatedResponse(resp *http.Response) (res *ActionSu
|
|||||||
return res, errors.Wrap(defRes, "error")
|
return res, errors.Wrap(defRes, "error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeBatchAssetThumbnailsResponse(resp *http.Response) (res *BatchAssetThumbnailsOK, _ error) {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case 200:
|
||||||
|
// Code 200.
|
||||||
|
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var response BatchAssetThumbnailsOK
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
|
default:
|
||||||
|
return res, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Convenient error response.
|
||||||
|
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||||
|
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var response Error
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
// Validate response.
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return res, errors.Wrap(err, "validate")
|
||||||
|
}
|
||||||
|
return &ErrorStatusCode{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Response: response,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return res, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return res, errors.Wrap(defRes, "error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeBatchUserThumbnailsResponse(resp *http.Response) (res *BatchUserThumbnailsOK, _ error) {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case 200:
|
||||||
|
// Code 200.
|
||||||
|
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var response BatchUserThumbnailsOK
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
|
default:
|
||||||
|
return res, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Convenient error response.
|
||||||
|
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||||
|
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var response Error
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
// Validate response.
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return res, errors.Wrap(err, "validate")
|
||||||
|
}
|
||||||
|
return &ErrorStatusCode{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Response: response,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return res, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return res, errors.Wrap(defRes, "error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeBatchUsernamesResponse(resp *http.Response) (res *BatchUsernamesOK, _ error) {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case 200:
|
||||||
|
// Code 200.
|
||||||
|
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var response BatchUsernamesOK
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
|
default:
|
||||||
|
return res, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Convenient error response.
|
||||||
|
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||||
|
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var response Error
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
// Validate response.
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return res, errors.Wrap(err, "validate")
|
||||||
|
}
|
||||||
|
return &ErrorStatusCode{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Response: response,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return res, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return res, errors.Wrap(defRes, "error")
|
||||||
|
}
|
||||||
|
|
||||||
func decodeCreateMapfixResponse(resp *http.Response) (res *OperationID, _ error) {
|
func decodeCreateMapfixResponse(resp *http.Response) (res *OperationID, _ error) {
|
||||||
switch resp.StatusCode {
|
switch resp.StatusCode {
|
||||||
case 201:
|
case 201:
|
||||||
@@ -2277,6 +2554,105 @@ func decodeDownloadMapAssetResponse(resp *http.Response) (res DownloadMapAssetOK
|
|||||||
return res, errors.Wrap(defRes, "error")
|
return res, errors.Wrap(defRes, "error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeGetAssetThumbnailResponse(resp *http.Response) (res *GetAssetThumbnailFound, _ error) {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case 302:
|
||||||
|
// Code 302.
|
||||||
|
var wrapper GetAssetThumbnailFound
|
||||||
|
h := uri.NewHeaderDecoder(resp.Header)
|
||||||
|
// Parse "Location" header.
|
||||||
|
{
|
||||||
|
cfg := uri.HeaderParameterDecodingConfig{
|
||||||
|
Name: "Location",
|
||||||
|
Explode: false,
|
||||||
|
}
|
||||||
|
if err := func() error {
|
||||||
|
if err := h.HasParam(cfg); err == nil {
|
||||||
|
if err := h.DecodeParam(cfg, func(d uri.Decoder) error {
|
||||||
|
var wrapperDotLocationVal string
|
||||||
|
if err := func() error {
|
||||||
|
val, err := d.DecodeValue()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := conv.ToString(val)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapperDotLocationVal = c
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wrapper.Location.SetTo(wrapperDotLocationVal)
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse Location header")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &wrapper, nil
|
||||||
|
}
|
||||||
|
// Convenient error response.
|
||||||
|
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||||
|
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var response Error
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
// Validate response.
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return res, errors.Wrap(err, "validate")
|
||||||
|
}
|
||||||
|
return &ErrorStatusCode{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Response: response,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return res, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return res, errors.Wrap(defRes, "error")
|
||||||
|
}
|
||||||
|
|
||||||
func decodeGetMapResponse(resp *http.Response) (res *Map, _ error) {
|
func decodeGetMapResponse(resp *http.Response) (res *Map, _ error) {
|
||||||
switch resp.StatusCode {
|
switch resp.StatusCode {
|
||||||
case 200:
|
case 200:
|
||||||
@@ -2782,6 +3158,107 @@ func decodeGetScriptPolicyResponse(resp *http.Response) (res *ScriptPolicy, _ er
|
|||||||
return res, errors.Wrap(defRes, "error")
|
return res, errors.Wrap(defRes, "error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeGetStatsResponse(resp *http.Response) (res *Stats, _ error) {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case 200:
|
||||||
|
// Code 200.
|
||||||
|
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var response Stats
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
// Validate response.
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return res, errors.Wrap(err, "validate")
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
|
default:
|
||||||
|
return res, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Convenient error response.
|
||||||
|
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||||
|
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var response Error
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
// Validate response.
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return res, errors.Wrap(err, "validate")
|
||||||
|
}
|
||||||
|
return &ErrorStatusCode{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Response: response,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return res, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return res, errors.Wrap(defRes, "error")
|
||||||
|
}
|
||||||
|
|
||||||
func decodeGetSubmissionResponse(resp *http.Response) (res *Submission, _ error) {
|
func decodeGetSubmissionResponse(resp *http.Response) (res *Submission, _ error) {
|
||||||
switch resp.StatusCode {
|
switch resp.StatusCode {
|
||||||
case 200:
|
case 200:
|
||||||
@@ -2883,6 +3360,105 @@ func decodeGetSubmissionResponse(resp *http.Response) (res *Submission, _ error)
|
|||||||
return res, errors.Wrap(defRes, "error")
|
return res, errors.Wrap(defRes, "error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeGetUserThumbnailResponse(resp *http.Response) (res *GetUserThumbnailFound, _ error) {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case 302:
|
||||||
|
// Code 302.
|
||||||
|
var wrapper GetUserThumbnailFound
|
||||||
|
h := uri.NewHeaderDecoder(resp.Header)
|
||||||
|
// Parse "Location" header.
|
||||||
|
{
|
||||||
|
cfg := uri.HeaderParameterDecodingConfig{
|
||||||
|
Name: "Location",
|
||||||
|
Explode: false,
|
||||||
|
}
|
||||||
|
if err := func() error {
|
||||||
|
if err := h.HasParam(cfg); err == nil {
|
||||||
|
if err := h.DecodeParam(cfg, func(d uri.Decoder) error {
|
||||||
|
var wrapperDotLocationVal string
|
||||||
|
if err := func() error {
|
||||||
|
val, err := d.DecodeValue()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := conv.ToString(val)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapperDotLocationVal = c
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wrapper.Location.SetTo(wrapperDotLocationVal)
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse Location header")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &wrapper, nil
|
||||||
|
}
|
||||||
|
// Convenient error response.
|
||||||
|
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||||
|
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var response Error
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
// Validate response.
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return res, errors.Wrap(err, "validate")
|
||||||
|
}
|
||||||
|
return &ErrorStatusCode{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Response: response,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return res, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return res, errors.Wrap(defRes, "error")
|
||||||
|
}
|
||||||
|
|
||||||
func decodeListMapfixAuditEventsResponse(resp *http.Response) (res []AuditEvent, _ error) {
|
func decodeListMapfixAuditEventsResponse(resp *http.Response) (res []AuditEvent, _ error) {
|
||||||
switch resp.StatusCode {
|
switch resp.StatusCode {
|
||||||
case 200:
|
case 200:
|
||||||
@@ -4232,6 +4808,66 @@ func decodeSetSubmissionCompletedResponse(resp *http.Response) (res *SetSubmissi
|
|||||||
return res, errors.Wrap(defRes, "error")
|
return res, errors.Wrap(defRes, "error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeUpdateMapfixDescriptionResponse(resp *http.Response) (res *UpdateMapfixDescriptionNoContent, _ error) {
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case 204:
|
||||||
|
// Code 204.
|
||||||
|
return &UpdateMapfixDescriptionNoContent{}, nil
|
||||||
|
}
|
||||||
|
// Convenient error response.
|
||||||
|
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||||
|
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrap(err, "parse media type")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case ct == "application/json":
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
d := jx.DecodeBytes(buf)
|
||||||
|
|
||||||
|
var response Error
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Decode(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.Skip(); err != io.EOF {
|
||||||
|
return errors.New("unexpected trailing data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
err = &ogenerrors.DecodeBodyError{
|
||||||
|
ContentType: ct,
|
||||||
|
Body: buf,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
// Validate response.
|
||||||
|
if err := func() error {
|
||||||
|
if err := response.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return res, errors.Wrap(err, "validate")
|
||||||
|
}
|
||||||
|
return &ErrorStatusCode{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Response: response,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return res, validate.InvalidContentType(ct)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return res, errors.Wrap(defRes, "error")
|
||||||
|
}
|
||||||
|
|
||||||
func decodeUpdateMapfixModelResponse(resp *http.Response) (res *UpdateMapfixModelNoContent, _ error) {
|
func decodeUpdateMapfixModelResponse(resp *http.Response) (res *UpdateMapfixModelNoContent, _ error) {
|
||||||
switch resp.StatusCode {
|
switch resp.StatusCode {
|
||||||
case 204:
|
case 204:
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import (
|
|||||||
|
|
||||||
"github.com/go-faster/errors"
|
"github.com/go-faster/errors"
|
||||||
"github.com/go-faster/jx"
|
"github.com/go-faster/jx"
|
||||||
|
"github.com/ogen-go/ogen/conv"
|
||||||
|
ht "github.com/ogen-go/ogen/http"
|
||||||
|
"github.com/ogen-go/ogen/uri"
|
||||||
"go.opentelemetry.io/otel/codes"
|
"go.opentelemetry.io/otel/codes"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
|
||||||
ht "github.com/ogen-go/ogen/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func encodeActionMapfixAcceptedResponse(response *ActionMapfixAcceptedNoContent, w http.ResponseWriter, span trace.Span) error {
|
func encodeActionMapfixAcceptedResponse(response *ActionMapfixAcceptedNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||||
@@ -182,6 +183,48 @@ func encodeActionSubmissionValidatedResponse(response *ActionSubmissionValidated
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeBatchAssetThumbnailsResponse(response *BatchAssetThumbnailsOK, w http.ResponseWriter, span trace.Span) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||||
|
|
||||||
|
e := new(jx.Encoder)
|
||||||
|
response.Encode(e)
|
||||||
|
if _, err := e.WriteTo(w); err != nil {
|
||||||
|
return errors.Wrap(err, "write")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeBatchUserThumbnailsResponse(response *BatchUserThumbnailsOK, w http.ResponseWriter, span trace.Span) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||||
|
|
||||||
|
e := new(jx.Encoder)
|
||||||
|
response.Encode(e)
|
||||||
|
if _, err := e.WriteTo(w); err != nil {
|
||||||
|
return errors.Wrap(err, "write")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeBatchUsernamesResponse(response *BatchUsernamesOK, w http.ResponseWriter, span trace.Span) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||||
|
|
||||||
|
e := new(jx.Encoder)
|
||||||
|
response.Encode(e)
|
||||||
|
if _, err := e.WriteTo(w); err != nil {
|
||||||
|
return errors.Wrap(err, "write")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func encodeCreateMapfixResponse(response *OperationID, w http.ResponseWriter, span trace.Span) error {
|
func encodeCreateMapfixResponse(response *OperationID, w http.ResponseWriter, span trace.Span) error {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
w.WriteHeader(201)
|
w.WriteHeader(201)
|
||||||
@@ -296,6 +339,32 @@ func encodeDownloadMapAssetResponse(response DownloadMapAssetOK, w http.Response
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeGetAssetThumbnailResponse(response *GetAssetThumbnailFound, w http.ResponseWriter, span trace.Span) error {
|
||||||
|
// Encoding response headers.
|
||||||
|
{
|
||||||
|
h := uri.NewHeaderEncoder(w.Header())
|
||||||
|
// Encode "Location" header.
|
||||||
|
{
|
||||||
|
cfg := uri.HeaderParameterEncodingConfig{
|
||||||
|
Name: "Location",
|
||||||
|
Explode: false,
|
||||||
|
}
|
||||||
|
if err := h.EncodeParam(cfg, func(e uri.Encoder) error {
|
||||||
|
if val, ok := response.Location.Get(); ok {
|
||||||
|
return e.EncodeValue(conv.StringToString(val))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return errors.Wrap(err, "encode Location header")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(302)
|
||||||
|
span.SetStatus(codes.Ok, http.StatusText(302))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func encodeGetMapResponse(response *Map, w http.ResponseWriter, span trace.Span) error {
|
func encodeGetMapResponse(response *Map, w http.ResponseWriter, span trace.Span) error {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
@@ -366,6 +435,20 @@ func encodeGetScriptPolicyResponse(response *ScriptPolicy, w http.ResponseWriter
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeGetStatsResponse(response *Stats, w http.ResponseWriter, span trace.Span) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||||
|
|
||||||
|
e := new(jx.Encoder)
|
||||||
|
response.Encode(e)
|
||||||
|
if _, err := e.WriteTo(w); err != nil {
|
||||||
|
return errors.Wrap(err, "write")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func encodeGetSubmissionResponse(response *Submission, w http.ResponseWriter, span trace.Span) error {
|
func encodeGetSubmissionResponse(response *Submission, w http.ResponseWriter, span trace.Span) error {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
@@ -380,6 +463,32 @@ func encodeGetSubmissionResponse(response *Submission, w http.ResponseWriter, sp
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeGetUserThumbnailResponse(response *GetUserThumbnailFound, w http.ResponseWriter, span trace.Span) error {
|
||||||
|
// Encoding response headers.
|
||||||
|
{
|
||||||
|
h := uri.NewHeaderEncoder(w.Header())
|
||||||
|
// Encode "Location" header.
|
||||||
|
{
|
||||||
|
cfg := uri.HeaderParameterEncodingConfig{
|
||||||
|
Name: "Location",
|
||||||
|
Explode: false,
|
||||||
|
}
|
||||||
|
if err := h.EncodeParam(cfg, func(e uri.Encoder) error {
|
||||||
|
if val, ok := response.Location.Get(); ok {
|
||||||
|
return e.EncodeValue(conv.StringToString(val))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return errors.Wrap(err, "encode Location header")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(302)
|
||||||
|
span.SetStatus(codes.Ok, http.StatusText(302))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func encodeListMapfixAuditEventsResponse(response []AuditEvent, w http.ResponseWriter, span trace.Span) error {
|
func encodeListMapfixAuditEventsResponse(response []AuditEvent, w http.ResponseWriter, span trace.Span) error {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
@@ -568,6 +677,13 @@ func encodeSetSubmissionCompletedResponse(response *SetSubmissionCompletedNoCont
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeUpdateMapfixDescriptionResponse(response *UpdateMapfixDescriptionNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||||
|
w.WriteHeader(204)
|
||||||
|
span.SetStatus(codes.Ok, http.StatusText(204))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func encodeUpdateMapfixModelResponse(response *UpdateMapfixModelNoContent, w http.ResponseWriter, span trace.Span) error {
|
func encodeUpdateMapfixModelResponse(response *UpdateMapfixModelNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||||
w.WriteHeader(204)
|
w.WriteHeader(204)
|
||||||
span.SetStatus(codes.Ok, http.StatusText(204))
|
span.SetStatus(codes.Ok, http.StatusText(204))
|
||||||
|
|||||||
@@ -216,6 +216,28 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'd': // Prefix: "description"
|
||||||
|
|
||||||
|
if l := len("description"); len(elem) >= l && elem[0:l] == "description" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch r.Method {
|
||||||
|
case "PATCH":
|
||||||
|
s.handleUpdateMapfixDescriptionRequest([1]string{
|
||||||
|
args[0],
|
||||||
|
}, elemIsEscaped, w, r)
|
||||||
|
default:
|
||||||
|
s.notAllowed(w, r, "PATCH")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
case 'm': // Prefix: "model"
|
case 'm': // Prefix: "model"
|
||||||
|
|
||||||
if l := len("model"); len(elem) >= l && elem[0:l] == "model" {
|
if l := len("model"); len(elem) >= l && elem[0:l] == "model" {
|
||||||
@@ -939,6 +961,26 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 't': // Prefix: "tats"
|
||||||
|
|
||||||
|
if l := len("tats"); len(elem) >= l && elem[0:l] == "tats" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
s.handleGetStatsRequest([0]string{}, elemIsEscaped, w, r)
|
||||||
|
default:
|
||||||
|
s.notAllowed(w, r, "GET")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
case 'u': // Prefix: "ubmissions"
|
case 'u': // Prefix: "ubmissions"
|
||||||
|
|
||||||
if l := len("ubmissions"); len(elem) >= l && elem[0:l] == "ubmissions" {
|
if l := len("ubmissions"); len(elem) >= l && elem[0:l] == "ubmissions" {
|
||||||
@@ -1431,6 +1473,170 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 't': // Prefix: "thumbnails/"
|
||||||
|
|
||||||
|
if l := len("thumbnails/"); len(elem) >= l && elem[0:l] == "thumbnails/" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch elem[0] {
|
||||||
|
case 'a': // Prefix: "asset"
|
||||||
|
|
||||||
|
if l := len("asset"); len(elem) >= l && elem[0:l] == "asset" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch elem[0] {
|
||||||
|
case '/': // Prefix: "/"
|
||||||
|
|
||||||
|
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param: "AssetID"
|
||||||
|
// Leaf parameter, slashes are prohibited
|
||||||
|
idx := strings.IndexByte(elem, '/')
|
||||||
|
if idx >= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
args[0] = elem
|
||||||
|
elem = ""
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
s.handleGetAssetThumbnailRequest([1]string{
|
||||||
|
args[0],
|
||||||
|
}, elemIsEscaped, w, r)
|
||||||
|
default:
|
||||||
|
s.notAllowed(w, r, "GET")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case 's': // Prefix: "s"
|
||||||
|
|
||||||
|
if l := len("s"); len(elem) >= l && elem[0:l] == "s" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch r.Method {
|
||||||
|
case "POST":
|
||||||
|
s.handleBatchAssetThumbnailsRequest([0]string{}, elemIsEscaped, w, r)
|
||||||
|
default:
|
||||||
|
s.notAllowed(w, r, "POST")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'u': // Prefix: "user"
|
||||||
|
|
||||||
|
if l := len("user"); len(elem) >= l && elem[0:l] == "user" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch elem[0] {
|
||||||
|
case '/': // Prefix: "/"
|
||||||
|
|
||||||
|
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param: "UserID"
|
||||||
|
// Leaf parameter, slashes are prohibited
|
||||||
|
idx := strings.IndexByte(elem, '/')
|
||||||
|
if idx >= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
args[0] = elem
|
||||||
|
elem = ""
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
s.handleGetUserThumbnailRequest([1]string{
|
||||||
|
args[0],
|
||||||
|
}, elemIsEscaped, w, r)
|
||||||
|
default:
|
||||||
|
s.notAllowed(w, r, "GET")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case 's': // Prefix: "s"
|
||||||
|
|
||||||
|
if l := len("s"); len(elem) >= l && elem[0:l] == "s" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch r.Method {
|
||||||
|
case "POST":
|
||||||
|
s.handleBatchUserThumbnailsRequest([0]string{}, elemIsEscaped, w, r)
|
||||||
|
default:
|
||||||
|
s.notAllowed(w, r, "POST")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'u': // Prefix: "usernames"
|
||||||
|
|
||||||
|
if l := len("usernames"); len(elem) >= l && elem[0:l] == "usernames" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch r.Method {
|
||||||
|
case "POST":
|
||||||
|
s.handleBatchUsernamesRequest([0]string{}, elemIsEscaped, w, r)
|
||||||
|
default:
|
||||||
|
s.notAllowed(w, r, "POST")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1440,12 +1646,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Route is route object.
|
// Route is route object.
|
||||||
type Route struct {
|
type Route struct {
|
||||||
name string
|
name string
|
||||||
summary string
|
summary string
|
||||||
operationID string
|
operationID string
|
||||||
pathPattern string
|
operationGroup string
|
||||||
count int
|
pathPattern string
|
||||||
args [1]string
|
count int
|
||||||
|
args [1]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns ogen operation name.
|
// Name returns ogen operation name.
|
||||||
@@ -1465,6 +1672,11 @@ func (r Route) OperationID() string {
|
|||||||
return r.operationID
|
return r.operationID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OperationGroup returns the x-ogen-operation-group value.
|
||||||
|
func (r Route) OperationGroup() string {
|
||||||
|
return r.operationGroup
|
||||||
|
}
|
||||||
|
|
||||||
// PathPattern returns OpenAPI path.
|
// PathPattern returns OpenAPI path.
|
||||||
func (r Route) PathPattern() string {
|
func (r Route) PathPattern() string {
|
||||||
return r.pathPattern
|
return r.pathPattern
|
||||||
@@ -1551,6 +1763,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ListMapfixesOperation
|
r.name = ListMapfixesOperation
|
||||||
r.summary = "Get list of mapfixes"
|
r.summary = "Get list of mapfixes"
|
||||||
r.operationID = "listMapfixes"
|
r.operationID = "listMapfixes"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes"
|
r.pathPattern = "/mapfixes"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -1559,6 +1772,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = CreateMapfixOperation
|
r.name = CreateMapfixOperation
|
||||||
r.summary = "Trigger the validator to create a mapfix"
|
r.summary = "Trigger the validator to create a mapfix"
|
||||||
r.operationID = "createMapfix"
|
r.operationID = "createMapfix"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes"
|
r.pathPattern = "/mapfixes"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -1591,6 +1805,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = GetMapfixOperation
|
r.name = GetMapfixOperation
|
||||||
r.summary = "Retrieve map with ID"
|
r.summary = "Retrieve map with ID"
|
||||||
r.operationID = "getMapfix"
|
r.operationID = "getMapfix"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}"
|
r.pathPattern = "/mapfixes/{MapfixID}"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1627,6 +1842,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ListMapfixAuditEventsOperation
|
r.name = ListMapfixAuditEventsOperation
|
||||||
r.summary = "Retrieve a list of audit events"
|
r.summary = "Retrieve a list of audit events"
|
||||||
r.operationID = "listMapfixAuditEvents"
|
r.operationID = "listMapfixAuditEvents"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/audit-events"
|
r.pathPattern = "/mapfixes/{MapfixID}/audit-events"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1663,6 +1879,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = CreateMapfixAuditCommentOperation
|
r.name = CreateMapfixAuditCommentOperation
|
||||||
r.summary = "Post a comment to the audit log"
|
r.summary = "Post a comment to the audit log"
|
||||||
r.operationID = "createMapfixAuditComment"
|
r.operationID = "createMapfixAuditComment"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/comment"
|
r.pathPattern = "/mapfixes/{MapfixID}/comment"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1687,6 +1904,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = SetMapfixCompletedOperation
|
r.name = SetMapfixCompletedOperation
|
||||||
r.summary = "Called by maptest when a player completes the map"
|
r.summary = "Called by maptest when a player completes the map"
|
||||||
r.operationID = "setMapfixCompleted"
|
r.operationID = "setMapfixCompleted"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/completed"
|
r.pathPattern = "/mapfixes/{MapfixID}/completed"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1698,6 +1916,31 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'd': // Prefix: "description"
|
||||||
|
|
||||||
|
if l := len("description"); len(elem) >= l && elem[0:l] == "description" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch method {
|
||||||
|
case "PATCH":
|
||||||
|
r.name = UpdateMapfixDescriptionOperation
|
||||||
|
r.summary = "Update description (submitter only)"
|
||||||
|
r.operationID = "updateMapfixDescription"
|
||||||
|
r.operationGroup = ""
|
||||||
|
r.pathPattern = "/mapfixes/{MapfixID}/description"
|
||||||
|
r.args = args
|
||||||
|
r.count = 1
|
||||||
|
return r, true
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case 'm': // Prefix: "model"
|
case 'm': // Prefix: "model"
|
||||||
|
|
||||||
if l := len("model"); len(elem) >= l && elem[0:l] == "model" {
|
if l := len("model"); len(elem) >= l && elem[0:l] == "model" {
|
||||||
@@ -1713,6 +1956,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = UpdateMapfixModelOperation
|
r.name = UpdateMapfixModelOperation
|
||||||
r.summary = "Update model following role restrictions"
|
r.summary = "Update model following role restrictions"
|
||||||
r.operationID = "updateMapfixModel"
|
r.operationID = "updateMapfixModel"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/model"
|
r.pathPattern = "/mapfixes/{MapfixID}/model"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1761,6 +2005,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixRejectOperation
|
r.name = ActionMapfixRejectOperation
|
||||||
r.summary = "Role Reviewer changes status from Submitted -> Rejected"
|
r.summary = "Role Reviewer changes status from Submitted -> Rejected"
|
||||||
r.operationID = "actionMapfixReject"
|
r.operationID = "actionMapfixReject"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/reject"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/reject"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1785,6 +2030,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixRequestChangesOperation
|
r.name = ActionMapfixRequestChangesOperation
|
||||||
r.summary = "Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested"
|
r.summary = "Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested"
|
||||||
r.operationID = "actionMapfixRequestChanges"
|
r.operationID = "actionMapfixRequestChanges"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/request-changes"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/request-changes"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1821,6 +2067,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixUploadedOperation
|
r.name = ActionMapfixUploadedOperation
|
||||||
r.summary = "Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded"
|
r.summary = "Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded"
|
||||||
r.operationID = "actionMapfixUploaded"
|
r.operationID = "actionMapfixUploaded"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-releasing"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-releasing"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1845,6 +2092,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixResetSubmittingOperation
|
r.name = ActionMapfixResetSubmittingOperation
|
||||||
r.summary = "Role Submitter manually resets submitting softlock and changes status from Submitting -> UnderConstruction"
|
r.summary = "Role Submitter manually resets submitting softlock and changes status from Submitting -> UnderConstruction"
|
||||||
r.operationID = "actionMapfixResetSubmitting"
|
r.operationID = "actionMapfixResetSubmitting"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-submitting"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-submitting"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1869,6 +2117,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixValidatedOperation
|
r.name = ActionMapfixValidatedOperation
|
||||||
r.summary = "Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated"
|
r.summary = "Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated"
|
||||||
r.operationID = "actionMapfixValidated"
|
r.operationID = "actionMapfixValidated"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-uploading"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-uploading"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1893,6 +2142,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixAcceptedOperation
|
r.name = ActionMapfixAcceptedOperation
|
||||||
r.summary = "Role Reviewer manually resets validating softlock and changes status from Validating -> Accepted"
|
r.summary = "Role Reviewer manually resets validating softlock and changes status from Validating -> Accepted"
|
||||||
r.operationID = "actionMapfixAccepted"
|
r.operationID = "actionMapfixAccepted"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-validating"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/reset-validating"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1919,6 +2169,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixRetryValidateOperation
|
r.name = ActionMapfixRetryValidateOperation
|
||||||
r.summary = "Role Reviewer re-runs validation and changes status from Accepted -> Validating"
|
r.summary = "Role Reviewer re-runs validation and changes status from Accepted -> Validating"
|
||||||
r.operationID = "actionMapfixRetryValidate"
|
r.operationID = "actionMapfixRetryValidate"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/retry-validate"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/retry-validate"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1943,6 +2194,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixRevokeOperation
|
r.name = ActionMapfixRevokeOperation
|
||||||
r.summary = "Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction"
|
r.summary = "Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction"
|
||||||
r.operationID = "actionMapfixRevoke"
|
r.operationID = "actionMapfixRevoke"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/revoke"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/revoke"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -1981,6 +2233,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixTriggerReleaseOperation
|
r.name = ActionMapfixTriggerReleaseOperation
|
||||||
r.summary = "Role MapfixUpload changes status from Uploaded -> Releasing"
|
r.summary = "Role MapfixUpload changes status from Uploaded -> Releasing"
|
||||||
r.operationID = "actionMapfixTriggerRelease"
|
r.operationID = "actionMapfixTriggerRelease"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-release"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-release"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2004,6 +2257,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixTriggerSubmitOperation
|
r.name = ActionMapfixTriggerSubmitOperation
|
||||||
r.summary = "Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting"
|
r.summary = "Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting"
|
||||||
r.operationID = "actionMapfixTriggerSubmit"
|
r.operationID = "actionMapfixTriggerSubmit"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-submit"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-submit"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2028,6 +2282,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixTriggerSubmitUncheckedOperation
|
r.name = ActionMapfixTriggerSubmitUncheckedOperation
|
||||||
r.summary = "Role Reviewer changes status from ChangesRequested -> Submitting"
|
r.summary = "Role Reviewer changes status from ChangesRequested -> Submitting"
|
||||||
r.operationID = "actionMapfixTriggerSubmitUnchecked"
|
r.operationID = "actionMapfixTriggerSubmitUnchecked"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-submit-unchecked"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-submit-unchecked"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2054,6 +2309,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixTriggerUploadOperation
|
r.name = ActionMapfixTriggerUploadOperation
|
||||||
r.summary = "Role MapfixUpload changes status from Validated -> Uploading"
|
r.summary = "Role MapfixUpload changes status from Validated -> Uploading"
|
||||||
r.operationID = "actionMapfixTriggerUpload"
|
r.operationID = "actionMapfixTriggerUpload"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-upload"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-upload"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2078,6 +2334,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionMapfixTriggerValidateOperation
|
r.name = ActionMapfixTriggerValidateOperation
|
||||||
r.summary = "Role Reviewer triggers validation and changes status from Submitted -> Validating"
|
r.summary = "Role Reviewer triggers validation and changes status from Submitted -> Validating"
|
||||||
r.operationID = "actionMapfixTriggerValidate"
|
r.operationID = "actionMapfixTriggerValidate"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-validate"
|
r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-validate"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2111,6 +2368,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ListMapsOperation
|
r.name = ListMapsOperation
|
||||||
r.summary = "Get list of maps"
|
r.summary = "Get list of maps"
|
||||||
r.operationID = "listMaps"
|
r.operationID = "listMaps"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/maps"
|
r.pathPattern = "/maps"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -2143,6 +2401,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = GetMapOperation
|
r.name = GetMapOperation
|
||||||
r.summary = "Retrieve map with ID"
|
r.summary = "Retrieve map with ID"
|
||||||
r.operationID = "getMap"
|
r.operationID = "getMap"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/maps/{MapID}"
|
r.pathPattern = "/maps/{MapID}"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2167,6 +2426,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = DownloadMapAssetOperation
|
r.name = DownloadMapAssetOperation
|
||||||
r.summary = "Download the map asset"
|
r.summary = "Download the map asset"
|
||||||
r.operationID = "downloadMapAsset"
|
r.operationID = "downloadMapAsset"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/maps/{MapID}/download"
|
r.pathPattern = "/maps/{MapID}/download"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2206,6 +2466,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = GetOperationOperation
|
r.name = GetOperationOperation
|
||||||
r.summary = "Retrieve operation with ID"
|
r.summary = "Retrieve operation with ID"
|
||||||
r.operationID = "getOperation"
|
r.operationID = "getOperation"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/operations/{OperationID}"
|
r.pathPattern = "/operations/{OperationID}"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2230,6 +2491,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ReleaseSubmissionsOperation
|
r.name = ReleaseSubmissionsOperation
|
||||||
r.summary = "Release a set of uploaded maps. Role SubmissionRelease"
|
r.summary = "Release a set of uploaded maps. Role SubmissionRelease"
|
||||||
r.operationID = "releaseSubmissions"
|
r.operationID = "releaseSubmissions"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/release-submissions"
|
r.pathPattern = "/release-submissions"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -2277,6 +2539,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ListScriptPolicyOperation
|
r.name = ListScriptPolicyOperation
|
||||||
r.summary = "Get list of script policies"
|
r.summary = "Get list of script policies"
|
||||||
r.operationID = "listScriptPolicy"
|
r.operationID = "listScriptPolicy"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/script-policy"
|
r.pathPattern = "/script-policy"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -2285,6 +2548,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = CreateScriptPolicyOperation
|
r.name = CreateScriptPolicyOperation
|
||||||
r.summary = "Create a new script policy"
|
r.summary = "Create a new script policy"
|
||||||
r.operationID = "createScriptPolicy"
|
r.operationID = "createScriptPolicy"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/script-policy"
|
r.pathPattern = "/script-policy"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -2318,6 +2582,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = DeleteScriptPolicyOperation
|
r.name = DeleteScriptPolicyOperation
|
||||||
r.summary = "Delete the specified script policy by ID"
|
r.summary = "Delete the specified script policy by ID"
|
||||||
r.operationID = "deleteScriptPolicy"
|
r.operationID = "deleteScriptPolicy"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/script-policy/{ScriptPolicyID}"
|
r.pathPattern = "/script-policy/{ScriptPolicyID}"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2326,6 +2591,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = GetScriptPolicyOperation
|
r.name = GetScriptPolicyOperation
|
||||||
r.summary = "Get the specified script policy by ID"
|
r.summary = "Get the specified script policy by ID"
|
||||||
r.operationID = "getScriptPolicy"
|
r.operationID = "getScriptPolicy"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/script-policy/{ScriptPolicyID}"
|
r.pathPattern = "/script-policy/{ScriptPolicyID}"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2334,6 +2600,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = UpdateScriptPolicyOperation
|
r.name = UpdateScriptPolicyOperation
|
||||||
r.summary = "Update the specified script policy by ID"
|
r.summary = "Update the specified script policy by ID"
|
||||||
r.operationID = "updateScriptPolicy"
|
r.operationID = "updateScriptPolicy"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/script-policy/{ScriptPolicyID}"
|
r.pathPattern = "/script-policy/{ScriptPolicyID}"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2359,6 +2626,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ListScriptsOperation
|
r.name = ListScriptsOperation
|
||||||
r.summary = "Get list of scripts"
|
r.summary = "Get list of scripts"
|
||||||
r.operationID = "listScripts"
|
r.operationID = "listScripts"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/scripts"
|
r.pathPattern = "/scripts"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -2367,6 +2635,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = CreateScriptOperation
|
r.name = CreateScriptOperation
|
||||||
r.summary = "Create a new script"
|
r.summary = "Create a new script"
|
||||||
r.operationID = "createScript"
|
r.operationID = "createScript"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/scripts"
|
r.pathPattern = "/scripts"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -2400,6 +2669,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = DeleteScriptOperation
|
r.name = DeleteScriptOperation
|
||||||
r.summary = "Delete the specified script by ID"
|
r.summary = "Delete the specified script by ID"
|
||||||
r.operationID = "deleteScript"
|
r.operationID = "deleteScript"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/scripts/{ScriptID}"
|
r.pathPattern = "/scripts/{ScriptID}"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2408,6 +2678,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = GetScriptOperation
|
r.name = GetScriptOperation
|
||||||
r.summary = "Get the specified script by ID"
|
r.summary = "Get the specified script by ID"
|
||||||
r.operationID = "getScript"
|
r.operationID = "getScript"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/scripts/{ScriptID}"
|
r.pathPattern = "/scripts/{ScriptID}"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2416,6 +2687,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = UpdateScriptOperation
|
r.name = UpdateScriptOperation
|
||||||
r.summary = "Update the specified script by ID"
|
r.summary = "Update the specified script by ID"
|
||||||
r.operationID = "updateScript"
|
r.operationID = "updateScript"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/scripts/{ScriptID}"
|
r.pathPattern = "/scripts/{ScriptID}"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2456,6 +2728,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = SessionRolesOperation
|
r.name = SessionRolesOperation
|
||||||
r.summary = "Get list of roles for the current session"
|
r.summary = "Get list of roles for the current session"
|
||||||
r.operationID = "sessionRoles"
|
r.operationID = "sessionRoles"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/session/roles"
|
r.pathPattern = "/session/roles"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -2480,6 +2753,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = SessionUserOperation
|
r.name = SessionUserOperation
|
||||||
r.summary = "Get information about the currently logged in user"
|
r.summary = "Get information about the currently logged in user"
|
||||||
r.operationID = "sessionUser"
|
r.operationID = "sessionUser"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/session/user"
|
r.pathPattern = "/session/user"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -2504,6 +2778,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = SessionValidateOperation
|
r.name = SessionValidateOperation
|
||||||
r.summary = "Ask if the current session is valid"
|
r.summary = "Ask if the current session is valid"
|
||||||
r.operationID = "sessionValidate"
|
r.operationID = "sessionValidate"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/session/validate"
|
r.pathPattern = "/session/validate"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -2515,6 +2790,31 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 't': // Prefix: "tats"
|
||||||
|
|
||||||
|
if l := len("tats"); len(elem) >= l && elem[0:l] == "tats" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch method {
|
||||||
|
case "GET":
|
||||||
|
r.name = GetStatsOperation
|
||||||
|
r.summary = "Get aggregate statistics"
|
||||||
|
r.operationID = "getStats"
|
||||||
|
r.operationGroup = ""
|
||||||
|
r.pathPattern = "/stats"
|
||||||
|
r.args = args
|
||||||
|
r.count = 0
|
||||||
|
return r, true
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case 'u': // Prefix: "ubmissions"
|
case 'u': // Prefix: "ubmissions"
|
||||||
|
|
||||||
if l := len("ubmissions"); len(elem) >= l && elem[0:l] == "ubmissions" {
|
if l := len("ubmissions"); len(elem) >= l && elem[0:l] == "ubmissions" {
|
||||||
@@ -2529,6 +2829,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ListSubmissionsOperation
|
r.name = ListSubmissionsOperation
|
||||||
r.summary = "Get list of submissions"
|
r.summary = "Get list of submissions"
|
||||||
r.operationID = "listSubmissions"
|
r.operationID = "listSubmissions"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions"
|
r.pathPattern = "/submissions"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -2537,6 +2838,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = CreateSubmissionOperation
|
r.name = CreateSubmissionOperation
|
||||||
r.summary = "Trigger the validator to create a new submission"
|
r.summary = "Trigger the validator to create a new submission"
|
||||||
r.operationID = "createSubmission"
|
r.operationID = "createSubmission"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions"
|
r.pathPattern = "/submissions"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -2561,6 +2863,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = CreateSubmissionAdminOperation
|
r.name = CreateSubmissionAdminOperation
|
||||||
r.summary = "Trigger the validator to create a new submission"
|
r.summary = "Trigger the validator to create a new submission"
|
||||||
r.operationID = "createSubmissionAdmin"
|
r.operationID = "createSubmissionAdmin"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions-admin"
|
r.pathPattern = "/submissions-admin"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 0
|
r.count = 0
|
||||||
@@ -2593,6 +2896,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = GetSubmissionOperation
|
r.name = GetSubmissionOperation
|
||||||
r.summary = "Retrieve map with ID"
|
r.summary = "Retrieve map with ID"
|
||||||
r.operationID = "getSubmission"
|
r.operationID = "getSubmission"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}"
|
r.pathPattern = "/submissions/{SubmissionID}"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2629,6 +2933,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ListSubmissionAuditEventsOperation
|
r.name = ListSubmissionAuditEventsOperation
|
||||||
r.summary = "Retrieve a list of audit events"
|
r.summary = "Retrieve a list of audit events"
|
||||||
r.operationID = "listSubmissionAuditEvents"
|
r.operationID = "listSubmissionAuditEvents"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/audit-events"
|
r.pathPattern = "/submissions/{SubmissionID}/audit-events"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2665,6 +2970,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = CreateSubmissionAuditCommentOperation
|
r.name = CreateSubmissionAuditCommentOperation
|
||||||
r.summary = "Post a comment to the audit log"
|
r.summary = "Post a comment to the audit log"
|
||||||
r.operationID = "createSubmissionAuditComment"
|
r.operationID = "createSubmissionAuditComment"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/comment"
|
r.pathPattern = "/submissions/{SubmissionID}/comment"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2689,6 +2995,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = SetSubmissionCompletedOperation
|
r.name = SetSubmissionCompletedOperation
|
||||||
r.summary = "Called by maptest when a player completes the map"
|
r.summary = "Called by maptest when a player completes the map"
|
||||||
r.operationID = "setSubmissionCompleted"
|
r.operationID = "setSubmissionCompleted"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/completed"
|
r.pathPattern = "/submissions/{SubmissionID}/completed"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2715,6 +3022,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = UpdateSubmissionModelOperation
|
r.name = UpdateSubmissionModelOperation
|
||||||
r.summary = "Update model following role restrictions"
|
r.summary = "Update model following role restrictions"
|
||||||
r.operationID = "updateSubmissionModel"
|
r.operationID = "updateSubmissionModel"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/model"
|
r.pathPattern = "/submissions/{SubmissionID}/model"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2763,6 +3071,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionSubmissionRejectOperation
|
r.name = ActionSubmissionRejectOperation
|
||||||
r.summary = "Role Reviewer changes status from Submitted -> Rejected"
|
r.summary = "Role Reviewer changes status from Submitted -> Rejected"
|
||||||
r.operationID = "actionSubmissionReject"
|
r.operationID = "actionSubmissionReject"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/status/reject"
|
r.pathPattern = "/submissions/{SubmissionID}/status/reject"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2787,6 +3096,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionSubmissionRequestChangesOperation
|
r.name = ActionSubmissionRequestChangesOperation
|
||||||
r.summary = "Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested"
|
r.summary = "Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested"
|
||||||
r.operationID = "actionSubmissionRequestChanges"
|
r.operationID = "actionSubmissionRequestChanges"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/status/request-changes"
|
r.pathPattern = "/submissions/{SubmissionID}/status/request-changes"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2823,6 +3133,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionSubmissionResetSubmittingOperation
|
r.name = ActionSubmissionResetSubmittingOperation
|
||||||
r.summary = "Role Submitter manually resets submitting softlock and changes status from Submitting -> UnderConstruction"
|
r.summary = "Role Submitter manually resets submitting softlock and changes status from Submitting -> UnderConstruction"
|
||||||
r.operationID = "actionSubmissionResetSubmitting"
|
r.operationID = "actionSubmissionResetSubmitting"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/status/reset-submitting"
|
r.pathPattern = "/submissions/{SubmissionID}/status/reset-submitting"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2847,6 +3158,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionSubmissionValidatedOperation
|
r.name = ActionSubmissionValidatedOperation
|
||||||
r.summary = "Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> Validated"
|
r.summary = "Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> Validated"
|
||||||
r.operationID = "actionSubmissionValidated"
|
r.operationID = "actionSubmissionValidated"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/status/reset-uploading"
|
r.pathPattern = "/submissions/{SubmissionID}/status/reset-uploading"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2871,6 +3183,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionSubmissionAcceptedOperation
|
r.name = ActionSubmissionAcceptedOperation
|
||||||
r.summary = "Role Reviewer manually resets validating softlock and changes status from Validating -> Accepted"
|
r.summary = "Role Reviewer manually resets validating softlock and changes status from Validating -> Accepted"
|
||||||
r.operationID = "actionSubmissionAccepted"
|
r.operationID = "actionSubmissionAccepted"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/status/reset-validating"
|
r.pathPattern = "/submissions/{SubmissionID}/status/reset-validating"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2897,6 +3210,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionSubmissionRetryValidateOperation
|
r.name = ActionSubmissionRetryValidateOperation
|
||||||
r.summary = "Role Reviewer re-runs validation and changes status from Accepted -> Validating"
|
r.summary = "Role Reviewer re-runs validation and changes status from Accepted -> Validating"
|
||||||
r.operationID = "actionSubmissionRetryValidate"
|
r.operationID = "actionSubmissionRetryValidate"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/status/retry-validate"
|
r.pathPattern = "/submissions/{SubmissionID}/status/retry-validate"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2921,6 +3235,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionSubmissionRevokeOperation
|
r.name = ActionSubmissionRevokeOperation
|
||||||
r.summary = "Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction"
|
r.summary = "Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction"
|
||||||
r.operationID = "actionSubmissionRevoke"
|
r.operationID = "actionSubmissionRevoke"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/status/revoke"
|
r.pathPattern = "/submissions/{SubmissionID}/status/revoke"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2958,6 +3273,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionSubmissionTriggerSubmitOperation
|
r.name = ActionSubmissionTriggerSubmitOperation
|
||||||
r.summary = "Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting"
|
r.summary = "Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting"
|
||||||
r.operationID = "actionSubmissionTriggerSubmit"
|
r.operationID = "actionSubmissionTriggerSubmit"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-submit"
|
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-submit"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -2982,6 +3298,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionSubmissionTriggerSubmitUncheckedOperation
|
r.name = ActionSubmissionTriggerSubmitUncheckedOperation
|
||||||
r.summary = "Role Reviewer changes status from ChangesRequested -> Submitting"
|
r.summary = "Role Reviewer changes status from ChangesRequested -> Submitting"
|
||||||
r.operationID = "actionSubmissionTriggerSubmitUnchecked"
|
r.operationID = "actionSubmissionTriggerSubmitUnchecked"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-submit-unchecked"
|
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-submit-unchecked"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -3008,6 +3325,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionSubmissionTriggerUploadOperation
|
r.name = ActionSubmissionTriggerUploadOperation
|
||||||
r.summary = "Role SubmissionUpload changes status from Validated -> Uploading"
|
r.summary = "Role SubmissionUpload changes status from Validated -> Uploading"
|
||||||
r.operationID = "actionSubmissionTriggerUpload"
|
r.operationID = "actionSubmissionTriggerUpload"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-upload"
|
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-upload"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -3032,6 +3350,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
r.name = ActionSubmissionTriggerValidateOperation
|
r.name = ActionSubmissionTriggerValidateOperation
|
||||||
r.summary = "Role Reviewer triggers validation and changes status from Submitted -> Validating"
|
r.summary = "Role Reviewer triggers validation and changes status from Submitted -> Validating"
|
||||||
r.operationID = "actionSubmissionTriggerValidate"
|
r.operationID = "actionSubmissionTriggerValidate"
|
||||||
|
r.operationGroup = ""
|
||||||
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-validate"
|
r.pathPattern = "/submissions/{SubmissionID}/status/trigger-validate"
|
||||||
r.args = args
|
r.args = args
|
||||||
r.count = 1
|
r.count = 1
|
||||||
@@ -3053,6 +3372,191 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 't': // Prefix: "thumbnails/"
|
||||||
|
|
||||||
|
if l := len("thumbnails/"); len(elem) >= l && elem[0:l] == "thumbnails/" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch elem[0] {
|
||||||
|
case 'a': // Prefix: "asset"
|
||||||
|
|
||||||
|
if l := len("asset"); len(elem) >= l && elem[0:l] == "asset" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch elem[0] {
|
||||||
|
case '/': // Prefix: "/"
|
||||||
|
|
||||||
|
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param: "AssetID"
|
||||||
|
// Leaf parameter, slashes are prohibited
|
||||||
|
idx := strings.IndexByte(elem, '/')
|
||||||
|
if idx >= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
args[0] = elem
|
||||||
|
elem = ""
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch method {
|
||||||
|
case "GET":
|
||||||
|
r.name = GetAssetThumbnailOperation
|
||||||
|
r.summary = "Get single asset thumbnail"
|
||||||
|
r.operationID = "getAssetThumbnail"
|
||||||
|
r.operationGroup = ""
|
||||||
|
r.pathPattern = "/thumbnails/asset/{AssetID}"
|
||||||
|
r.args = args
|
||||||
|
r.count = 1
|
||||||
|
return r, true
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 's': // Prefix: "s"
|
||||||
|
|
||||||
|
if l := len("s"); len(elem) >= l && elem[0:l] == "s" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch method {
|
||||||
|
case "POST":
|
||||||
|
r.name = BatchAssetThumbnailsOperation
|
||||||
|
r.summary = "Batch fetch asset thumbnails"
|
||||||
|
r.operationID = "batchAssetThumbnails"
|
||||||
|
r.operationGroup = ""
|
||||||
|
r.pathPattern = "/thumbnails/assets"
|
||||||
|
r.args = args
|
||||||
|
r.count = 0
|
||||||
|
return r, true
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'u': // Prefix: "user"
|
||||||
|
|
||||||
|
if l := len("user"); len(elem) >= l && elem[0:l] == "user" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch elem[0] {
|
||||||
|
case '/': // Prefix: "/"
|
||||||
|
|
||||||
|
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param: "UserID"
|
||||||
|
// Leaf parameter, slashes are prohibited
|
||||||
|
idx := strings.IndexByte(elem, '/')
|
||||||
|
if idx >= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
args[0] = elem
|
||||||
|
elem = ""
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch method {
|
||||||
|
case "GET":
|
||||||
|
r.name = GetUserThumbnailOperation
|
||||||
|
r.summary = "Get single user avatar thumbnail"
|
||||||
|
r.operationID = "getUserThumbnail"
|
||||||
|
r.operationGroup = ""
|
||||||
|
r.pathPattern = "/thumbnails/user/{UserID}"
|
||||||
|
r.args = args
|
||||||
|
r.count = 1
|
||||||
|
return r, true
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 's': // Prefix: "s"
|
||||||
|
|
||||||
|
if l := len("s"); len(elem) >= l && elem[0:l] == "s" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch method {
|
||||||
|
case "POST":
|
||||||
|
r.name = BatchUserThumbnailsOperation
|
||||||
|
r.summary = "Batch fetch user avatar thumbnails"
|
||||||
|
r.operationID = "batchUserThumbnails"
|
||||||
|
r.operationGroup = ""
|
||||||
|
r.pathPattern = "/thumbnails/users"
|
||||||
|
r.args = args
|
||||||
|
r.count = 0
|
||||||
|
return r, true
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'u': // Prefix: "usernames"
|
||||||
|
|
||||||
|
if l := len("usernames"); len(elem) >= l && elem[0:l] == "usernames" {
|
||||||
|
elem = elem[l:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(elem) == 0 {
|
||||||
|
// Leaf node.
|
||||||
|
switch method {
|
||||||
|
case "POST":
|
||||||
|
r.name = BatchUsernamesOperation
|
||||||
|
r.summary = "Batch fetch usernames"
|
||||||
|
r.operationID = "batchUsernames"
|
||||||
|
r.operationGroup = ""
|
||||||
|
r.pathPattern = "/usernames"
|
||||||
|
r.args = args
|
||||||
|
r.count = 0
|
||||||
|
return r, true
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-faster/errors"
|
||||||
"github.com/go-faster/jx"
|
"github.com/go-faster/jx"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -192,6 +193,254 @@ func (s *AuditEventEventData) init() AuditEventEventData {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BatchAssetThumbnailsOK struct {
|
||||||
|
// Map of asset ID to thumbnail URL.
|
||||||
|
Thumbnails OptBatchAssetThumbnailsOKThumbnails `json:"thumbnails"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetThumbnails returns the value of Thumbnails.
|
||||||
|
func (s *BatchAssetThumbnailsOK) GetThumbnails() OptBatchAssetThumbnailsOKThumbnails {
|
||||||
|
return s.Thumbnails
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetThumbnails sets the value of Thumbnails.
|
||||||
|
func (s *BatchAssetThumbnailsOK) SetThumbnails(val OptBatchAssetThumbnailsOKThumbnails) {
|
||||||
|
s.Thumbnails = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map of asset ID to thumbnail URL.
|
||||||
|
type BatchAssetThumbnailsOKThumbnails map[string]string
|
||||||
|
|
||||||
|
func (s *BatchAssetThumbnailsOKThumbnails) init() BatchAssetThumbnailsOKThumbnails {
|
||||||
|
m := *s
|
||||||
|
if m == nil {
|
||||||
|
m = map[string]string{}
|
||||||
|
*s = m
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchAssetThumbnailsReq struct {
|
||||||
|
// Array of asset IDs (max 100).
|
||||||
|
AssetIds []uint64 `json:"assetIds"`
|
||||||
|
// Thumbnail size.
|
||||||
|
Size OptBatchAssetThumbnailsReqSize `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAssetIds returns the value of AssetIds.
|
||||||
|
func (s *BatchAssetThumbnailsReq) GetAssetIds() []uint64 {
|
||||||
|
return s.AssetIds
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the value of Size.
|
||||||
|
func (s *BatchAssetThumbnailsReq) GetSize() OptBatchAssetThumbnailsReqSize {
|
||||||
|
return s.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAssetIds sets the value of AssetIds.
|
||||||
|
func (s *BatchAssetThumbnailsReq) SetAssetIds(val []uint64) {
|
||||||
|
s.AssetIds = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSize sets the value of Size.
|
||||||
|
func (s *BatchAssetThumbnailsReq) SetSize(val OptBatchAssetThumbnailsReqSize) {
|
||||||
|
s.Size = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbnail size.
|
||||||
|
type BatchAssetThumbnailsReqSize string
|
||||||
|
|
||||||
|
const (
|
||||||
|
BatchAssetThumbnailsReqSize150x150 BatchAssetThumbnailsReqSize = "150x150"
|
||||||
|
BatchAssetThumbnailsReqSize420x420 BatchAssetThumbnailsReqSize = "420x420"
|
||||||
|
BatchAssetThumbnailsReqSize768x432 BatchAssetThumbnailsReqSize = "768x432"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllValues returns all BatchAssetThumbnailsReqSize values.
|
||||||
|
func (BatchAssetThumbnailsReqSize) AllValues() []BatchAssetThumbnailsReqSize {
|
||||||
|
return []BatchAssetThumbnailsReqSize{
|
||||||
|
BatchAssetThumbnailsReqSize150x150,
|
||||||
|
BatchAssetThumbnailsReqSize420x420,
|
||||||
|
BatchAssetThumbnailsReqSize768x432,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements encoding.TextMarshaler.
|
||||||
|
func (s BatchAssetThumbnailsReqSize) MarshalText() ([]byte, error) {
|
||||||
|
switch s {
|
||||||
|
case BatchAssetThumbnailsReqSize150x150:
|
||||||
|
return []byte(s), nil
|
||||||
|
case BatchAssetThumbnailsReqSize420x420:
|
||||||
|
return []byte(s), nil
|
||||||
|
case BatchAssetThumbnailsReqSize768x432:
|
||||||
|
return []byte(s), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("invalid value: %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||||
|
func (s *BatchAssetThumbnailsReqSize) UnmarshalText(data []byte) error {
|
||||||
|
switch BatchAssetThumbnailsReqSize(data) {
|
||||||
|
case BatchAssetThumbnailsReqSize150x150:
|
||||||
|
*s = BatchAssetThumbnailsReqSize150x150
|
||||||
|
return nil
|
||||||
|
case BatchAssetThumbnailsReqSize420x420:
|
||||||
|
*s = BatchAssetThumbnailsReqSize420x420
|
||||||
|
return nil
|
||||||
|
case BatchAssetThumbnailsReqSize768x432:
|
||||||
|
*s = BatchAssetThumbnailsReqSize768x432
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.Errorf("invalid value: %q", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchUserThumbnailsOK struct {
|
||||||
|
// Map of user ID to thumbnail URL.
|
||||||
|
Thumbnails OptBatchUserThumbnailsOKThumbnails `json:"thumbnails"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetThumbnails returns the value of Thumbnails.
|
||||||
|
func (s *BatchUserThumbnailsOK) GetThumbnails() OptBatchUserThumbnailsOKThumbnails {
|
||||||
|
return s.Thumbnails
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetThumbnails sets the value of Thumbnails.
|
||||||
|
func (s *BatchUserThumbnailsOK) SetThumbnails(val OptBatchUserThumbnailsOKThumbnails) {
|
||||||
|
s.Thumbnails = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map of user ID to thumbnail URL.
|
||||||
|
type BatchUserThumbnailsOKThumbnails map[string]string
|
||||||
|
|
||||||
|
func (s *BatchUserThumbnailsOKThumbnails) init() BatchUserThumbnailsOKThumbnails {
|
||||||
|
m := *s
|
||||||
|
if m == nil {
|
||||||
|
m = map[string]string{}
|
||||||
|
*s = m
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchUserThumbnailsReq struct {
|
||||||
|
// Array of user IDs (max 100).
|
||||||
|
UserIds []uint64 `json:"userIds"`
|
||||||
|
// Thumbnail size.
|
||||||
|
Size OptBatchUserThumbnailsReqSize `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserIds returns the value of UserIds.
|
||||||
|
func (s *BatchUserThumbnailsReq) GetUserIds() []uint64 {
|
||||||
|
return s.UserIds
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the value of Size.
|
||||||
|
func (s *BatchUserThumbnailsReq) GetSize() OptBatchUserThumbnailsReqSize {
|
||||||
|
return s.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserIds sets the value of UserIds.
|
||||||
|
func (s *BatchUserThumbnailsReq) SetUserIds(val []uint64) {
|
||||||
|
s.UserIds = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSize sets the value of Size.
|
||||||
|
func (s *BatchUserThumbnailsReq) SetSize(val OptBatchUserThumbnailsReqSize) {
|
||||||
|
s.Size = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbnail size.
|
||||||
|
type BatchUserThumbnailsReqSize string
|
||||||
|
|
||||||
|
const (
|
||||||
|
BatchUserThumbnailsReqSize150x150 BatchUserThumbnailsReqSize = "150x150"
|
||||||
|
BatchUserThumbnailsReqSize420x420 BatchUserThumbnailsReqSize = "420x420"
|
||||||
|
BatchUserThumbnailsReqSize768x432 BatchUserThumbnailsReqSize = "768x432"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllValues returns all BatchUserThumbnailsReqSize values.
|
||||||
|
func (BatchUserThumbnailsReqSize) AllValues() []BatchUserThumbnailsReqSize {
|
||||||
|
return []BatchUserThumbnailsReqSize{
|
||||||
|
BatchUserThumbnailsReqSize150x150,
|
||||||
|
BatchUserThumbnailsReqSize420x420,
|
||||||
|
BatchUserThumbnailsReqSize768x432,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements encoding.TextMarshaler.
|
||||||
|
func (s BatchUserThumbnailsReqSize) MarshalText() ([]byte, error) {
|
||||||
|
switch s {
|
||||||
|
case BatchUserThumbnailsReqSize150x150:
|
||||||
|
return []byte(s), nil
|
||||||
|
case BatchUserThumbnailsReqSize420x420:
|
||||||
|
return []byte(s), nil
|
||||||
|
case BatchUserThumbnailsReqSize768x432:
|
||||||
|
return []byte(s), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("invalid value: %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||||
|
func (s *BatchUserThumbnailsReqSize) UnmarshalText(data []byte) error {
|
||||||
|
switch BatchUserThumbnailsReqSize(data) {
|
||||||
|
case BatchUserThumbnailsReqSize150x150:
|
||||||
|
*s = BatchUserThumbnailsReqSize150x150
|
||||||
|
return nil
|
||||||
|
case BatchUserThumbnailsReqSize420x420:
|
||||||
|
*s = BatchUserThumbnailsReqSize420x420
|
||||||
|
return nil
|
||||||
|
case BatchUserThumbnailsReqSize768x432:
|
||||||
|
*s = BatchUserThumbnailsReqSize768x432
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.Errorf("invalid value: %q", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchUsernamesOK struct {
|
||||||
|
// Map of user ID to username.
|
||||||
|
Usernames OptBatchUsernamesOKUsernames `json:"usernames"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsernames returns the value of Usernames.
|
||||||
|
func (s *BatchUsernamesOK) GetUsernames() OptBatchUsernamesOKUsernames {
|
||||||
|
return s.Usernames
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUsernames sets the value of Usernames.
|
||||||
|
func (s *BatchUsernamesOK) SetUsernames(val OptBatchUsernamesOKUsernames) {
|
||||||
|
s.Usernames = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map of user ID to username.
|
||||||
|
type BatchUsernamesOKUsernames map[string]string
|
||||||
|
|
||||||
|
func (s *BatchUsernamesOKUsernames) init() BatchUsernamesOKUsernames {
|
||||||
|
m := *s
|
||||||
|
if m == nil {
|
||||||
|
m = map[string]string{}
|
||||||
|
*s = m
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchUsernamesReq struct {
|
||||||
|
// Array of user IDs (max 100).
|
||||||
|
UserIds []uint64 `json:"userIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserIds returns the value of UserIds.
|
||||||
|
func (s *BatchUsernamesReq) GetUserIds() []uint64 {
|
||||||
|
return s.UserIds
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserIds sets the value of UserIds.
|
||||||
|
func (s *BatchUsernamesReq) SetUserIds(val []uint64) {
|
||||||
|
s.UserIds = val
|
||||||
|
}
|
||||||
|
|
||||||
type CookieAuth struct {
|
type CookieAuth struct {
|
||||||
APIKey string
|
APIKey string
|
||||||
Roles []string
|
Roles []string
|
||||||
@@ -324,6 +573,132 @@ func (s *ErrorStatusCode) SetResponse(val Error) {
|
|||||||
s.Response = val
|
s.Response = val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAssetThumbnailFound is response for GetAssetThumbnail operation.
|
||||||
|
type GetAssetThumbnailFound struct {
|
||||||
|
Location OptString
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocation returns the value of Location.
|
||||||
|
func (s *GetAssetThumbnailFound) GetLocation() OptString {
|
||||||
|
return s.Location
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLocation sets the value of Location.
|
||||||
|
func (s *GetAssetThumbnailFound) SetLocation(val OptString) {
|
||||||
|
s.Location = val
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAssetThumbnailSize string
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetAssetThumbnailSize150x150 GetAssetThumbnailSize = "150x150"
|
||||||
|
GetAssetThumbnailSize420x420 GetAssetThumbnailSize = "420x420"
|
||||||
|
GetAssetThumbnailSize768x432 GetAssetThumbnailSize = "768x432"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllValues returns all GetAssetThumbnailSize values.
|
||||||
|
func (GetAssetThumbnailSize) AllValues() []GetAssetThumbnailSize {
|
||||||
|
return []GetAssetThumbnailSize{
|
||||||
|
GetAssetThumbnailSize150x150,
|
||||||
|
GetAssetThumbnailSize420x420,
|
||||||
|
GetAssetThumbnailSize768x432,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements encoding.TextMarshaler.
|
||||||
|
func (s GetAssetThumbnailSize) MarshalText() ([]byte, error) {
|
||||||
|
switch s {
|
||||||
|
case GetAssetThumbnailSize150x150:
|
||||||
|
return []byte(s), nil
|
||||||
|
case GetAssetThumbnailSize420x420:
|
||||||
|
return []byte(s), nil
|
||||||
|
case GetAssetThumbnailSize768x432:
|
||||||
|
return []byte(s), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("invalid value: %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||||
|
func (s *GetAssetThumbnailSize) UnmarshalText(data []byte) error {
|
||||||
|
switch GetAssetThumbnailSize(data) {
|
||||||
|
case GetAssetThumbnailSize150x150:
|
||||||
|
*s = GetAssetThumbnailSize150x150
|
||||||
|
return nil
|
||||||
|
case GetAssetThumbnailSize420x420:
|
||||||
|
*s = GetAssetThumbnailSize420x420
|
||||||
|
return nil
|
||||||
|
case GetAssetThumbnailSize768x432:
|
||||||
|
*s = GetAssetThumbnailSize768x432
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.Errorf("invalid value: %q", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserThumbnailFound is response for GetUserThumbnail operation.
|
||||||
|
type GetUserThumbnailFound struct {
|
||||||
|
Location OptString
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocation returns the value of Location.
|
||||||
|
func (s *GetUserThumbnailFound) GetLocation() OptString {
|
||||||
|
return s.Location
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLocation sets the value of Location.
|
||||||
|
func (s *GetUserThumbnailFound) SetLocation(val OptString) {
|
||||||
|
s.Location = val
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUserThumbnailSize string
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetUserThumbnailSize150x150 GetUserThumbnailSize = "150x150"
|
||||||
|
GetUserThumbnailSize420x420 GetUserThumbnailSize = "420x420"
|
||||||
|
GetUserThumbnailSize768x432 GetUserThumbnailSize = "768x432"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllValues returns all GetUserThumbnailSize values.
|
||||||
|
func (GetUserThumbnailSize) AllValues() []GetUserThumbnailSize {
|
||||||
|
return []GetUserThumbnailSize{
|
||||||
|
GetUserThumbnailSize150x150,
|
||||||
|
GetUserThumbnailSize420x420,
|
||||||
|
GetUserThumbnailSize768x432,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements encoding.TextMarshaler.
|
||||||
|
func (s GetUserThumbnailSize) MarshalText() ([]byte, error) {
|
||||||
|
switch s {
|
||||||
|
case GetUserThumbnailSize150x150:
|
||||||
|
return []byte(s), nil
|
||||||
|
case GetUserThumbnailSize420x420:
|
||||||
|
return []byte(s), nil
|
||||||
|
case GetUserThumbnailSize768x432:
|
||||||
|
return []byte(s), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("invalid value: %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||||
|
func (s *GetUserThumbnailSize) UnmarshalText(data []byte) error {
|
||||||
|
switch GetUserThumbnailSize(data) {
|
||||||
|
case GetUserThumbnailSize150x150:
|
||||||
|
*s = GetUserThumbnailSize150x150
|
||||||
|
return nil
|
||||||
|
case GetUserThumbnailSize420x420:
|
||||||
|
*s = GetUserThumbnailSize420x420
|
||||||
|
return nil
|
||||||
|
case GetUserThumbnailSize768x432:
|
||||||
|
*s = GetUserThumbnailSize768x432
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.Errorf("invalid value: %q", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ref: #/components/schemas/Map
|
// Ref: #/components/schemas/Map
|
||||||
type Map struct {
|
type Map struct {
|
||||||
ID int64 `json:"ID"`
|
ID int64 `json:"ID"`
|
||||||
@@ -777,6 +1152,328 @@ func (s *OperationID) SetOperationID(val int32) {
|
|||||||
s.OperationID = val
|
s.OperationID = val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewOptBatchAssetThumbnailsOKThumbnails returns new OptBatchAssetThumbnailsOKThumbnails with value set to v.
|
||||||
|
func NewOptBatchAssetThumbnailsOKThumbnails(v BatchAssetThumbnailsOKThumbnails) OptBatchAssetThumbnailsOKThumbnails {
|
||||||
|
return OptBatchAssetThumbnailsOKThumbnails{
|
||||||
|
Value: v,
|
||||||
|
Set: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptBatchAssetThumbnailsOKThumbnails is optional BatchAssetThumbnailsOKThumbnails.
|
||||||
|
type OptBatchAssetThumbnailsOKThumbnails struct {
|
||||||
|
Value BatchAssetThumbnailsOKThumbnails
|
||||||
|
Set bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSet returns true if OptBatchAssetThumbnailsOKThumbnails was set.
|
||||||
|
func (o OptBatchAssetThumbnailsOKThumbnails) IsSet() bool { return o.Set }
|
||||||
|
|
||||||
|
// Reset unsets value.
|
||||||
|
func (o *OptBatchAssetThumbnailsOKThumbnails) Reset() {
|
||||||
|
var v BatchAssetThumbnailsOKThumbnails
|
||||||
|
o.Value = v
|
||||||
|
o.Set = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTo sets value to v.
|
||||||
|
func (o *OptBatchAssetThumbnailsOKThumbnails) SetTo(v BatchAssetThumbnailsOKThumbnails) {
|
||||||
|
o.Set = true
|
||||||
|
o.Value = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns value and boolean that denotes whether value was set.
|
||||||
|
func (o OptBatchAssetThumbnailsOKThumbnails) Get() (v BatchAssetThumbnailsOKThumbnails, ok bool) {
|
||||||
|
if !o.Set {
|
||||||
|
return v, false
|
||||||
|
}
|
||||||
|
return o.Value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or returns value if set, or given parameter if does not.
|
||||||
|
func (o OptBatchAssetThumbnailsOKThumbnails) Or(d BatchAssetThumbnailsOKThumbnails) BatchAssetThumbnailsOKThumbnails {
|
||||||
|
if v, ok := o.Get(); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptBatchAssetThumbnailsReqSize returns new OptBatchAssetThumbnailsReqSize with value set to v.
|
||||||
|
func NewOptBatchAssetThumbnailsReqSize(v BatchAssetThumbnailsReqSize) OptBatchAssetThumbnailsReqSize {
|
||||||
|
return OptBatchAssetThumbnailsReqSize{
|
||||||
|
Value: v,
|
||||||
|
Set: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptBatchAssetThumbnailsReqSize is optional BatchAssetThumbnailsReqSize.
|
||||||
|
type OptBatchAssetThumbnailsReqSize struct {
|
||||||
|
Value BatchAssetThumbnailsReqSize
|
||||||
|
Set bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSet returns true if OptBatchAssetThumbnailsReqSize was set.
|
||||||
|
func (o OptBatchAssetThumbnailsReqSize) IsSet() bool { return o.Set }
|
||||||
|
|
||||||
|
// Reset unsets value.
|
||||||
|
func (o *OptBatchAssetThumbnailsReqSize) Reset() {
|
||||||
|
var v BatchAssetThumbnailsReqSize
|
||||||
|
o.Value = v
|
||||||
|
o.Set = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTo sets value to v.
|
||||||
|
func (o *OptBatchAssetThumbnailsReqSize) SetTo(v BatchAssetThumbnailsReqSize) {
|
||||||
|
o.Set = true
|
||||||
|
o.Value = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns value and boolean that denotes whether value was set.
|
||||||
|
func (o OptBatchAssetThumbnailsReqSize) Get() (v BatchAssetThumbnailsReqSize, ok bool) {
|
||||||
|
if !o.Set {
|
||||||
|
return v, false
|
||||||
|
}
|
||||||
|
return o.Value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or returns value if set, or given parameter if does not.
|
||||||
|
func (o OptBatchAssetThumbnailsReqSize) Or(d BatchAssetThumbnailsReqSize) BatchAssetThumbnailsReqSize {
|
||||||
|
if v, ok := o.Get(); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptBatchUserThumbnailsOKThumbnails returns new OptBatchUserThumbnailsOKThumbnails with value set to v.
|
||||||
|
func NewOptBatchUserThumbnailsOKThumbnails(v BatchUserThumbnailsOKThumbnails) OptBatchUserThumbnailsOKThumbnails {
|
||||||
|
return OptBatchUserThumbnailsOKThumbnails{
|
||||||
|
Value: v,
|
||||||
|
Set: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptBatchUserThumbnailsOKThumbnails is optional BatchUserThumbnailsOKThumbnails.
|
||||||
|
type OptBatchUserThumbnailsOKThumbnails struct {
|
||||||
|
Value BatchUserThumbnailsOKThumbnails
|
||||||
|
Set bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSet returns true if OptBatchUserThumbnailsOKThumbnails was set.
|
||||||
|
func (o OptBatchUserThumbnailsOKThumbnails) IsSet() bool { return o.Set }
|
||||||
|
|
||||||
|
// Reset unsets value.
|
||||||
|
func (o *OptBatchUserThumbnailsOKThumbnails) Reset() {
|
||||||
|
var v BatchUserThumbnailsOKThumbnails
|
||||||
|
o.Value = v
|
||||||
|
o.Set = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTo sets value to v.
|
||||||
|
func (o *OptBatchUserThumbnailsOKThumbnails) SetTo(v BatchUserThumbnailsOKThumbnails) {
|
||||||
|
o.Set = true
|
||||||
|
o.Value = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns value and boolean that denotes whether value was set.
|
||||||
|
func (o OptBatchUserThumbnailsOKThumbnails) Get() (v BatchUserThumbnailsOKThumbnails, ok bool) {
|
||||||
|
if !o.Set {
|
||||||
|
return v, false
|
||||||
|
}
|
||||||
|
return o.Value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or returns value if set, or given parameter if does not.
|
||||||
|
func (o OptBatchUserThumbnailsOKThumbnails) Or(d BatchUserThumbnailsOKThumbnails) BatchUserThumbnailsOKThumbnails {
|
||||||
|
if v, ok := o.Get(); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptBatchUserThumbnailsReqSize returns new OptBatchUserThumbnailsReqSize with value set to v.
|
||||||
|
func NewOptBatchUserThumbnailsReqSize(v BatchUserThumbnailsReqSize) OptBatchUserThumbnailsReqSize {
|
||||||
|
return OptBatchUserThumbnailsReqSize{
|
||||||
|
Value: v,
|
||||||
|
Set: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptBatchUserThumbnailsReqSize is optional BatchUserThumbnailsReqSize.
|
||||||
|
type OptBatchUserThumbnailsReqSize struct {
|
||||||
|
Value BatchUserThumbnailsReqSize
|
||||||
|
Set bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSet returns true if OptBatchUserThumbnailsReqSize was set.
|
||||||
|
func (o OptBatchUserThumbnailsReqSize) IsSet() bool { return o.Set }
|
||||||
|
|
||||||
|
// Reset unsets value.
|
||||||
|
func (o *OptBatchUserThumbnailsReqSize) Reset() {
|
||||||
|
var v BatchUserThumbnailsReqSize
|
||||||
|
o.Value = v
|
||||||
|
o.Set = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTo sets value to v.
|
||||||
|
func (o *OptBatchUserThumbnailsReqSize) SetTo(v BatchUserThumbnailsReqSize) {
|
||||||
|
o.Set = true
|
||||||
|
o.Value = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns value and boolean that denotes whether value was set.
|
||||||
|
func (o OptBatchUserThumbnailsReqSize) Get() (v BatchUserThumbnailsReqSize, ok bool) {
|
||||||
|
if !o.Set {
|
||||||
|
return v, false
|
||||||
|
}
|
||||||
|
return o.Value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or returns value if set, or given parameter if does not.
|
||||||
|
func (o OptBatchUserThumbnailsReqSize) Or(d BatchUserThumbnailsReqSize) BatchUserThumbnailsReqSize {
|
||||||
|
if v, ok := o.Get(); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptBatchUsernamesOKUsernames returns new OptBatchUsernamesOKUsernames with value set to v.
|
||||||
|
func NewOptBatchUsernamesOKUsernames(v BatchUsernamesOKUsernames) OptBatchUsernamesOKUsernames {
|
||||||
|
return OptBatchUsernamesOKUsernames{
|
||||||
|
Value: v,
|
||||||
|
Set: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptBatchUsernamesOKUsernames is optional BatchUsernamesOKUsernames.
|
||||||
|
type OptBatchUsernamesOKUsernames struct {
|
||||||
|
Value BatchUsernamesOKUsernames
|
||||||
|
Set bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSet returns true if OptBatchUsernamesOKUsernames was set.
|
||||||
|
func (o OptBatchUsernamesOKUsernames) IsSet() bool { return o.Set }
|
||||||
|
|
||||||
|
// Reset unsets value.
|
||||||
|
func (o *OptBatchUsernamesOKUsernames) Reset() {
|
||||||
|
var v BatchUsernamesOKUsernames
|
||||||
|
o.Value = v
|
||||||
|
o.Set = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTo sets value to v.
|
||||||
|
func (o *OptBatchUsernamesOKUsernames) SetTo(v BatchUsernamesOKUsernames) {
|
||||||
|
o.Set = true
|
||||||
|
o.Value = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns value and boolean that denotes whether value was set.
|
||||||
|
func (o OptBatchUsernamesOKUsernames) Get() (v BatchUsernamesOKUsernames, ok bool) {
|
||||||
|
if !o.Set {
|
||||||
|
return v, false
|
||||||
|
}
|
||||||
|
return o.Value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or returns value if set, or given parameter if does not.
|
||||||
|
func (o OptBatchUsernamesOKUsernames) Or(d BatchUsernamesOKUsernames) BatchUsernamesOKUsernames {
|
||||||
|
if v, ok := o.Get(); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptGetAssetThumbnailSize returns new OptGetAssetThumbnailSize with value set to v.
|
||||||
|
func NewOptGetAssetThumbnailSize(v GetAssetThumbnailSize) OptGetAssetThumbnailSize {
|
||||||
|
return OptGetAssetThumbnailSize{
|
||||||
|
Value: v,
|
||||||
|
Set: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptGetAssetThumbnailSize is optional GetAssetThumbnailSize.
|
||||||
|
type OptGetAssetThumbnailSize struct {
|
||||||
|
Value GetAssetThumbnailSize
|
||||||
|
Set bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSet returns true if OptGetAssetThumbnailSize was set.
|
||||||
|
func (o OptGetAssetThumbnailSize) IsSet() bool { return o.Set }
|
||||||
|
|
||||||
|
// Reset unsets value.
|
||||||
|
func (o *OptGetAssetThumbnailSize) Reset() {
|
||||||
|
var v GetAssetThumbnailSize
|
||||||
|
o.Value = v
|
||||||
|
o.Set = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTo sets value to v.
|
||||||
|
func (o *OptGetAssetThumbnailSize) SetTo(v GetAssetThumbnailSize) {
|
||||||
|
o.Set = true
|
||||||
|
o.Value = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns value and boolean that denotes whether value was set.
|
||||||
|
func (o OptGetAssetThumbnailSize) Get() (v GetAssetThumbnailSize, ok bool) {
|
||||||
|
if !o.Set {
|
||||||
|
return v, false
|
||||||
|
}
|
||||||
|
return o.Value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or returns value if set, or given parameter if does not.
|
||||||
|
func (o OptGetAssetThumbnailSize) Or(d GetAssetThumbnailSize) GetAssetThumbnailSize {
|
||||||
|
if v, ok := o.Get(); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptGetUserThumbnailSize returns new OptGetUserThumbnailSize with value set to v.
|
||||||
|
func NewOptGetUserThumbnailSize(v GetUserThumbnailSize) OptGetUserThumbnailSize {
|
||||||
|
return OptGetUserThumbnailSize{
|
||||||
|
Value: v,
|
||||||
|
Set: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptGetUserThumbnailSize is optional GetUserThumbnailSize.
|
||||||
|
type OptGetUserThumbnailSize struct {
|
||||||
|
Value GetUserThumbnailSize
|
||||||
|
Set bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSet returns true if OptGetUserThumbnailSize was set.
|
||||||
|
func (o OptGetUserThumbnailSize) IsSet() bool { return o.Set }
|
||||||
|
|
||||||
|
// Reset unsets value.
|
||||||
|
func (o *OptGetUserThumbnailSize) Reset() {
|
||||||
|
var v GetUserThumbnailSize
|
||||||
|
o.Value = v
|
||||||
|
o.Set = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTo sets value to v.
|
||||||
|
func (o *OptGetUserThumbnailSize) SetTo(v GetUserThumbnailSize) {
|
||||||
|
o.Set = true
|
||||||
|
o.Value = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns value and boolean that denotes whether value was set.
|
||||||
|
func (o OptGetUserThumbnailSize) Get() (v GetUserThumbnailSize, ok bool) {
|
||||||
|
if !o.Set {
|
||||||
|
return v, false
|
||||||
|
}
|
||||||
|
return o.Value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or returns value if set, or given parameter if does not.
|
||||||
|
func (o OptGetUserThumbnailSize) Or(d GetUserThumbnailSize) GetUserThumbnailSize {
|
||||||
|
if v, ok := o.Get(); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
// NewOptInt32 returns new OptInt32 with value set to v.
|
// NewOptInt32 returns new OptInt32 with value set to v.
|
||||||
func NewOptInt32(v int32) OptInt32 {
|
func NewOptInt32(v int32) OptInt32 {
|
||||||
return OptInt32{
|
return OptInt32{
|
||||||
@@ -1302,6 +1999,83 @@ type SetMapfixCompletedNoContent struct{}
|
|||||||
// SetSubmissionCompletedNoContent is response for SetSubmissionCompleted operation.
|
// SetSubmissionCompletedNoContent is response for SetSubmissionCompleted operation.
|
||||||
type SetSubmissionCompletedNoContent struct{}
|
type SetSubmissionCompletedNoContent struct{}
|
||||||
|
|
||||||
|
// Aggregate statistics for submissions and mapfixes.
|
||||||
|
// Ref: #/components/schemas/Stats
|
||||||
|
type Stats struct {
|
||||||
|
// Total number of submissions.
|
||||||
|
TotalSubmissions int64 `json:"TotalSubmissions"`
|
||||||
|
// Total number of mapfixes.
|
||||||
|
TotalMapfixes int64 `json:"TotalMapfixes"`
|
||||||
|
// Number of released submissions.
|
||||||
|
ReleasedSubmissions int64 `json:"ReleasedSubmissions"`
|
||||||
|
// Number of released mapfixes.
|
||||||
|
ReleasedMapfixes int64 `json:"ReleasedMapfixes"`
|
||||||
|
// Number of submissions under review.
|
||||||
|
SubmittedSubmissions int64 `json:"SubmittedSubmissions"`
|
||||||
|
// Number of mapfixes under review.
|
||||||
|
SubmittedMapfixes int64 `json:"SubmittedMapfixes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTotalSubmissions returns the value of TotalSubmissions.
|
||||||
|
func (s *Stats) GetTotalSubmissions() int64 {
|
||||||
|
return s.TotalSubmissions
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTotalMapfixes returns the value of TotalMapfixes.
|
||||||
|
func (s *Stats) GetTotalMapfixes() int64 {
|
||||||
|
return s.TotalMapfixes
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReleasedSubmissions returns the value of ReleasedSubmissions.
|
||||||
|
func (s *Stats) GetReleasedSubmissions() int64 {
|
||||||
|
return s.ReleasedSubmissions
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReleasedMapfixes returns the value of ReleasedMapfixes.
|
||||||
|
func (s *Stats) GetReleasedMapfixes() int64 {
|
||||||
|
return s.ReleasedMapfixes
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSubmittedSubmissions returns the value of SubmittedSubmissions.
|
||||||
|
func (s *Stats) GetSubmittedSubmissions() int64 {
|
||||||
|
return s.SubmittedSubmissions
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSubmittedMapfixes returns the value of SubmittedMapfixes.
|
||||||
|
func (s *Stats) GetSubmittedMapfixes() int64 {
|
||||||
|
return s.SubmittedMapfixes
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTotalSubmissions sets the value of TotalSubmissions.
|
||||||
|
func (s *Stats) SetTotalSubmissions(val int64) {
|
||||||
|
s.TotalSubmissions = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTotalMapfixes sets the value of TotalMapfixes.
|
||||||
|
func (s *Stats) SetTotalMapfixes(val int64) {
|
||||||
|
s.TotalMapfixes = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReleasedSubmissions sets the value of ReleasedSubmissions.
|
||||||
|
func (s *Stats) SetReleasedSubmissions(val int64) {
|
||||||
|
s.ReleasedSubmissions = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReleasedMapfixes sets the value of ReleasedMapfixes.
|
||||||
|
func (s *Stats) SetReleasedMapfixes(val int64) {
|
||||||
|
s.ReleasedMapfixes = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSubmittedSubmissions sets the value of SubmittedSubmissions.
|
||||||
|
func (s *Stats) SetSubmittedSubmissions(val int64) {
|
||||||
|
s.SubmittedSubmissions = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSubmittedMapfixes sets the value of SubmittedMapfixes.
|
||||||
|
func (s *Stats) SetSubmittedMapfixes(val int64) {
|
||||||
|
s.SubmittedMapfixes = val
|
||||||
|
}
|
||||||
|
|
||||||
// Ref: #/components/schemas/Submission
|
// Ref: #/components/schemas/Submission
|
||||||
type Submission struct {
|
type Submission struct {
|
||||||
ID int64 `json:"ID"`
|
ID int64 `json:"ID"`
|
||||||
@@ -1534,6 +2308,23 @@ func (s *Submissions) SetSubmissions(val []Submission) {
|
|||||||
s.Submissions = val
|
s.Submissions = val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateMapfixDescriptionNoContent is response for UpdateMapfixDescription operation.
|
||||||
|
type UpdateMapfixDescriptionNoContent struct{}
|
||||||
|
|
||||||
|
type UpdateMapfixDescriptionReq struct {
|
||||||
|
Data io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads data from the Data reader.
|
||||||
|
//
|
||||||
|
// Kept to satisfy the io.Reader interface.
|
||||||
|
func (s UpdateMapfixDescriptionReq) Read(p []byte) (n int, err error) {
|
||||||
|
if s.Data == nil {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
return s.Data.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateMapfixModelNoContent is response for UpdateMapfixModel operation.
|
// UpdateMapfixModelNoContent is response for UpdateMapfixModel operation.
|
||||||
type UpdateMapfixModelNoContent struct{}
|
type UpdateMapfixModelNoContent struct{}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-faster/errors"
|
"github.com/go-faster/errors"
|
||||||
|
|
||||||
"github.com/ogen-go/ogen/ogenerrors"
|
"github.com/ogen-go/ogen/ogenerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,6 +74,7 @@ var operationRolesCookieAuth = map[string][]string{
|
|||||||
SessionValidateOperation: []string{},
|
SessionValidateOperation: []string{},
|
||||||
SetMapfixCompletedOperation: []string{},
|
SetMapfixCompletedOperation: []string{},
|
||||||
SetSubmissionCompletedOperation: []string{},
|
SetSubmissionCompletedOperation: []string{},
|
||||||
|
UpdateMapfixDescriptionOperation: []string{},
|
||||||
UpdateMapfixModelOperation: []string{},
|
UpdateMapfixModelOperation: []string{},
|
||||||
UpdateScriptOperation: []string{},
|
UpdateScriptOperation: []string{},
|
||||||
UpdateScriptPolicyOperation: []string{},
|
UpdateScriptPolicyOperation: []string{},
|
||||||
|
|||||||
@@ -155,6 +155,24 @@ type Handler interface {
|
|||||||
//
|
//
|
||||||
// POST /submissions/{SubmissionID}/status/reset-uploading
|
// POST /submissions/{SubmissionID}/status/reset-uploading
|
||||||
ActionSubmissionValidated(ctx context.Context, params ActionSubmissionValidatedParams) error
|
ActionSubmissionValidated(ctx context.Context, params ActionSubmissionValidatedParams) error
|
||||||
|
// BatchAssetThumbnails implements batchAssetThumbnails operation.
|
||||||
|
//
|
||||||
|
// Batch fetch asset thumbnails.
|
||||||
|
//
|
||||||
|
// POST /thumbnails/assets
|
||||||
|
BatchAssetThumbnails(ctx context.Context, req *BatchAssetThumbnailsReq) (*BatchAssetThumbnailsOK, error)
|
||||||
|
// BatchUserThumbnails implements batchUserThumbnails operation.
|
||||||
|
//
|
||||||
|
// Batch fetch user avatar thumbnails.
|
||||||
|
//
|
||||||
|
// POST /thumbnails/users
|
||||||
|
BatchUserThumbnails(ctx context.Context, req *BatchUserThumbnailsReq) (*BatchUserThumbnailsOK, error)
|
||||||
|
// BatchUsernames implements batchUsernames operation.
|
||||||
|
//
|
||||||
|
// Batch fetch usernames.
|
||||||
|
//
|
||||||
|
// POST /usernames
|
||||||
|
BatchUsernames(ctx context.Context, req *BatchUsernamesReq) (*BatchUsernamesOK, error)
|
||||||
// CreateMapfix implements createMapfix operation.
|
// CreateMapfix implements createMapfix operation.
|
||||||
//
|
//
|
||||||
// Trigger the validator to create a mapfix.
|
// Trigger the validator to create a mapfix.
|
||||||
@@ -215,6 +233,12 @@ type Handler interface {
|
|||||||
//
|
//
|
||||||
// GET /maps/{MapID}/download
|
// GET /maps/{MapID}/download
|
||||||
DownloadMapAsset(ctx context.Context, params DownloadMapAssetParams) (DownloadMapAssetOK, error)
|
DownloadMapAsset(ctx context.Context, params DownloadMapAssetParams) (DownloadMapAssetOK, error)
|
||||||
|
// GetAssetThumbnail implements getAssetThumbnail operation.
|
||||||
|
//
|
||||||
|
// Get single asset thumbnail.
|
||||||
|
//
|
||||||
|
// GET /thumbnails/asset/{AssetID}
|
||||||
|
GetAssetThumbnail(ctx context.Context, params GetAssetThumbnailParams) (*GetAssetThumbnailFound, error)
|
||||||
// GetMap implements getMap operation.
|
// GetMap implements getMap operation.
|
||||||
//
|
//
|
||||||
// Retrieve map with ID.
|
// Retrieve map with ID.
|
||||||
@@ -245,12 +269,24 @@ type Handler interface {
|
|||||||
//
|
//
|
||||||
// GET /script-policy/{ScriptPolicyID}
|
// GET /script-policy/{ScriptPolicyID}
|
||||||
GetScriptPolicy(ctx context.Context, params GetScriptPolicyParams) (*ScriptPolicy, error)
|
GetScriptPolicy(ctx context.Context, params GetScriptPolicyParams) (*ScriptPolicy, error)
|
||||||
|
// GetStats implements getStats operation.
|
||||||
|
//
|
||||||
|
// Get aggregate statistics.
|
||||||
|
//
|
||||||
|
// GET /stats
|
||||||
|
GetStats(ctx context.Context) (*Stats, error)
|
||||||
// GetSubmission implements getSubmission operation.
|
// GetSubmission implements getSubmission operation.
|
||||||
//
|
//
|
||||||
// Retrieve map with ID.
|
// Retrieve map with ID.
|
||||||
//
|
//
|
||||||
// GET /submissions/{SubmissionID}
|
// GET /submissions/{SubmissionID}
|
||||||
GetSubmission(ctx context.Context, params GetSubmissionParams) (*Submission, error)
|
GetSubmission(ctx context.Context, params GetSubmissionParams) (*Submission, error)
|
||||||
|
// GetUserThumbnail implements getUserThumbnail operation.
|
||||||
|
//
|
||||||
|
// Get single user avatar thumbnail.
|
||||||
|
//
|
||||||
|
// GET /thumbnails/user/{UserID}
|
||||||
|
GetUserThumbnail(ctx context.Context, params GetUserThumbnailParams) (*GetUserThumbnailFound, error)
|
||||||
// ListMapfixAuditEvents implements listMapfixAuditEvents operation.
|
// ListMapfixAuditEvents implements listMapfixAuditEvents operation.
|
||||||
//
|
//
|
||||||
// Retrieve a list of audit events.
|
// Retrieve a list of audit events.
|
||||||
@@ -329,6 +365,12 @@ type Handler interface {
|
|||||||
//
|
//
|
||||||
// POST /submissions/{SubmissionID}/completed
|
// POST /submissions/{SubmissionID}/completed
|
||||||
SetSubmissionCompleted(ctx context.Context, params SetSubmissionCompletedParams) error
|
SetSubmissionCompleted(ctx context.Context, params SetSubmissionCompletedParams) error
|
||||||
|
// UpdateMapfixDescription implements updateMapfixDescription operation.
|
||||||
|
//
|
||||||
|
// Update description (submitter only).
|
||||||
|
//
|
||||||
|
// PATCH /mapfixes/{MapfixID}/description
|
||||||
|
UpdateMapfixDescription(ctx context.Context, req UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) error
|
||||||
// UpdateMapfixModel implements updateMapfixModel operation.
|
// UpdateMapfixModel implements updateMapfixModel operation.
|
||||||
//
|
//
|
||||||
// Update model following role restrictions.
|
// Update model following role restrictions.
|
||||||
|
|||||||
@@ -232,6 +232,33 @@ func (UnimplementedHandler) ActionSubmissionValidated(ctx context.Context, param
|
|||||||
return ht.ErrNotImplemented
|
return ht.ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchAssetThumbnails implements batchAssetThumbnails operation.
|
||||||
|
//
|
||||||
|
// Batch fetch asset thumbnails.
|
||||||
|
//
|
||||||
|
// POST /thumbnails/assets
|
||||||
|
func (UnimplementedHandler) BatchAssetThumbnails(ctx context.Context, req *BatchAssetThumbnailsReq) (r *BatchAssetThumbnailsOK, _ error) {
|
||||||
|
return r, ht.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUserThumbnails implements batchUserThumbnails operation.
|
||||||
|
//
|
||||||
|
// Batch fetch user avatar thumbnails.
|
||||||
|
//
|
||||||
|
// POST /thumbnails/users
|
||||||
|
func (UnimplementedHandler) BatchUserThumbnails(ctx context.Context, req *BatchUserThumbnailsReq) (r *BatchUserThumbnailsOK, _ error) {
|
||||||
|
return r, ht.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUsernames implements batchUsernames operation.
|
||||||
|
//
|
||||||
|
// Batch fetch usernames.
|
||||||
|
//
|
||||||
|
// POST /usernames
|
||||||
|
func (UnimplementedHandler) BatchUsernames(ctx context.Context, req *BatchUsernamesReq) (r *BatchUsernamesOK, _ error) {
|
||||||
|
return r, ht.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
// CreateMapfix implements createMapfix operation.
|
// CreateMapfix implements createMapfix operation.
|
||||||
//
|
//
|
||||||
// Trigger the validator to create a mapfix.
|
// Trigger the validator to create a mapfix.
|
||||||
@@ -322,6 +349,15 @@ func (UnimplementedHandler) DownloadMapAsset(ctx context.Context, params Downloa
|
|||||||
return r, ht.ErrNotImplemented
|
return r, ht.ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAssetThumbnail implements getAssetThumbnail operation.
|
||||||
|
//
|
||||||
|
// Get single asset thumbnail.
|
||||||
|
//
|
||||||
|
// GET /thumbnails/asset/{AssetID}
|
||||||
|
func (UnimplementedHandler) GetAssetThumbnail(ctx context.Context, params GetAssetThumbnailParams) (r *GetAssetThumbnailFound, _ error) {
|
||||||
|
return r, ht.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
// GetMap implements getMap operation.
|
// GetMap implements getMap operation.
|
||||||
//
|
//
|
||||||
// Retrieve map with ID.
|
// Retrieve map with ID.
|
||||||
@@ -367,6 +403,15 @@ func (UnimplementedHandler) GetScriptPolicy(ctx context.Context, params GetScrip
|
|||||||
return r, ht.ErrNotImplemented
|
return r, ht.ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetStats implements getStats operation.
|
||||||
|
//
|
||||||
|
// Get aggregate statistics.
|
||||||
|
//
|
||||||
|
// GET /stats
|
||||||
|
func (UnimplementedHandler) GetStats(ctx context.Context) (r *Stats, _ error) {
|
||||||
|
return r, ht.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
// GetSubmission implements getSubmission operation.
|
// GetSubmission implements getSubmission operation.
|
||||||
//
|
//
|
||||||
// Retrieve map with ID.
|
// Retrieve map with ID.
|
||||||
@@ -376,6 +421,15 @@ func (UnimplementedHandler) GetSubmission(ctx context.Context, params GetSubmiss
|
|||||||
return r, ht.ErrNotImplemented
|
return r, ht.ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserThumbnail implements getUserThumbnail operation.
|
||||||
|
//
|
||||||
|
// Get single user avatar thumbnail.
|
||||||
|
//
|
||||||
|
// GET /thumbnails/user/{UserID}
|
||||||
|
func (UnimplementedHandler) GetUserThumbnail(ctx context.Context, params GetUserThumbnailParams) (r *GetUserThumbnailFound, _ error) {
|
||||||
|
return r, ht.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
// ListMapfixAuditEvents implements listMapfixAuditEvents operation.
|
// ListMapfixAuditEvents implements listMapfixAuditEvents operation.
|
||||||
//
|
//
|
||||||
// Retrieve a list of audit events.
|
// Retrieve a list of audit events.
|
||||||
@@ -493,6 +547,15 @@ func (UnimplementedHandler) SetSubmissionCompleted(ctx context.Context, params S
|
|||||||
return ht.ErrNotImplemented
|
return ht.ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateMapfixDescription implements updateMapfixDescription operation.
|
||||||
|
//
|
||||||
|
// Update description (submitter only).
|
||||||
|
//
|
||||||
|
// PATCH /mapfixes/{MapfixID}/description
|
||||||
|
func (UnimplementedHandler) UpdateMapfixDescription(ctx context.Context, req UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) error {
|
||||||
|
return ht.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateMapfixModel implements updateMapfixModel operation.
|
// UpdateMapfixModel implements updateMapfixModel operation.
|
||||||
//
|
//
|
||||||
// Update model following role restrictions.
|
// Update model following role restrictions.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ import (
|
|||||||
"git.itzana.me/strafesnet/maps-service/pkg/validator_controller"
|
"git.itzana.me/strafesnet/maps-service/pkg/validator_controller"
|
||||||
"git.itzana.me/strafesnet/maps-service/pkg/web_api"
|
"git.itzana.me/strafesnet/maps-service/pkg/web_api"
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -102,6 +103,24 @@ func NewServeCommand() *cli.Command {
|
|||||||
EnvVars: []string{"RBX_API_KEY"},
|
EnvVars: []string{"RBX_API_KEY"},
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "redis-host",
|
||||||
|
Usage: "Host of Redis cache",
|
||||||
|
EnvVars: []string{"REDIS_HOST"},
|
||||||
|
Value: "localhost:6379",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "redis-password",
|
||||||
|
Usage: "Password for Redis",
|
||||||
|
EnvVars: []string{"REDIS_PASSWORD"},
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "redis-db",
|
||||||
|
Usage: "Redis database number",
|
||||||
|
EnvVars: []string{"REDIS_DB"},
|
||||||
|
Value: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,6 +148,24 @@ func serve(ctx *cli.Context) error {
|
|||||||
log.WithError(err).Fatal("failed to add stream")
|
log.WithError(err).Fatal("failed to add stream")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize Redis client
|
||||||
|
redisClient := redis.NewClient(&redis.Options{
|
||||||
|
Addr: ctx.String("redis-host"),
|
||||||
|
Password: ctx.String("redis-password"),
|
||||||
|
DB: ctx.Int("redis-db"),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test Redis connection
|
||||||
|
if err := redisClient.Ping(ctx.Context).Err(); err != nil {
|
||||||
|
log.WithError(err).Warn("failed to connect to Redis - thumbnails will not be cached")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Roblox client
|
||||||
|
robloxClient := &roblox.Client{
|
||||||
|
HttpClient: http.DefaultClient,
|
||||||
|
ApiKey: ctx.String("rbx-api-key"),
|
||||||
|
}
|
||||||
|
|
||||||
// connect to main game database
|
// connect to main game database
|
||||||
conn, err := grpc.Dial(ctx.String("data-rpc-host"), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
conn, err := grpc.Dial(ctx.String("data-rpc-host"), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -139,13 +176,15 @@ func serve(ctx *cli.Context) error {
|
|||||||
js,
|
js,
|
||||||
maps.NewMapsServiceClient(conn),
|
maps.NewMapsServiceClient(conn),
|
||||||
users.NewUsersServiceClient(conn),
|
users.NewUsersServiceClient(conn),
|
||||||
|
robloxClient,
|
||||||
|
redisClient,
|
||||||
)
|
)
|
||||||
|
|
||||||
svc_external := web_api.NewService(
|
svc_external := web_api.NewService(
|
||||||
&svc_inner,
|
&svc_inner,
|
||||||
roblox.Client{
|
roblox.Client{
|
||||||
HttpClient: http.DefaultClient,
|
HttpClient: http.DefaultClient,
|
||||||
ApiKey: ctx.String("rbx-api-key"),
|
ApiKey: ctx.String("rbx-api-key"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -31,9 +31,12 @@ type CheckSubmissionRequest struct{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CheckMapfixRequest struct{
|
type CheckMapfixRequest struct{
|
||||||
MapfixID int64
|
MapfixID int64
|
||||||
ModelID uint64
|
ModelID uint64
|
||||||
SkipChecks bool
|
SkipChecks bool
|
||||||
|
DisplayName string
|
||||||
|
Creator string
|
||||||
|
GameID uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type ValidateSubmissionRequest struct {
|
type ValidateSubmissionRequest struct {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type ScriptPolicy struct {
|
|||||||
// Hash of the source code that leads to this policy.
|
// Hash of the source code that leads to this policy.
|
||||||
// If this is a replacement mapping, the original source may not be pointed to by any policy.
|
// If this is a replacement mapping, the original source may not be pointed to by any policy.
|
||||||
// The original source should still exist in the scripts table, which can be located by the same hash.
|
// The original source should still exist in the scripts table, which can be located by the same hash.
|
||||||
FromScriptHash int64 // postgres does not support unsigned integers, so we have to pretend
|
FromScriptHash int64 `gorm:"uniqueIndex"` // postgres does not support unsigned integers, so we have to pretend
|
||||||
// The ID of the replacement source (ScriptPolicyReplace)
|
// The ID of the replacement source (ScriptPolicyReplace)
|
||||||
// or verbatim source (ScriptPolicyAllowed)
|
// or verbatim source (ScriptPolicyAllowed)
|
||||||
// or 0 (other)
|
// or 0 (other)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func HashParse(hash string) (uint64, error){
|
|||||||
type Script struct {
|
type Script struct {
|
||||||
ID int64 `gorm:"primaryKey"`
|
ID int64 `gorm:"primaryKey"`
|
||||||
Name string
|
Name string
|
||||||
Hash int64 // postgres does not support unsigned integers, so we have to pretend
|
Hash int64 `gorm:"uniqueIndex"` // postgres does not support unsigned integers, so we have to pretend
|
||||||
Source string
|
Source string
|
||||||
ResourceType ResourceType // is this a submission or is it a mapfix
|
ResourceType ResourceType // is this a submission or is it a mapfix
|
||||||
ResourceID int64 // which submission / mapfix did this script first appear in
|
ResourceID int64 // which submission / mapfix did this script first appear in
|
||||||
|
|||||||
160
pkg/roblox/thumbnails.go
Normal file
160
pkg/roblox/thumbnails.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package roblox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ThumbnailSize represents valid Roblox thumbnail sizes
|
||||||
|
type ThumbnailSize string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Size150x150 ThumbnailSize = "150x150"
|
||||||
|
Size420x420 ThumbnailSize = "420x420"
|
||||||
|
Size768x432 ThumbnailSize = "768x432"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ThumbnailFormat represents the image format
|
||||||
|
type ThumbnailFormat string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatPng ThumbnailFormat = "Png"
|
||||||
|
FormatJpeg ThumbnailFormat = "Jpeg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ThumbnailRequest represents a single thumbnail request
|
||||||
|
type ThumbnailRequest struct {
|
||||||
|
RequestID string `json:"requestId,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
TargetID uint64 `json:"targetId"`
|
||||||
|
Size string `json:"size,omitempty"`
|
||||||
|
Format string `json:"format,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThumbnailData represents a single thumbnail response
|
||||||
|
type ThumbnailData struct {
|
||||||
|
TargetID uint64 `json:"targetId"`
|
||||||
|
State string `json:"state"` // "Completed", "Error", "Pending"
|
||||||
|
ImageURL string `json:"imageUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchThumbnailsResponse represents the API response
|
||||||
|
type BatchThumbnailsResponse struct {
|
||||||
|
Data []ThumbnailData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAssetThumbnails fetches thumbnails for multiple assets in a single batch request
|
||||||
|
// Roblox allows up to 100 assets per batch
|
||||||
|
func (c *Client) GetAssetThumbnails(assetIDs []uint64, size ThumbnailSize, format ThumbnailFormat) ([]ThumbnailData, error) {
|
||||||
|
if len(assetIDs) == 0 {
|
||||||
|
return []ThumbnailData{}, nil
|
||||||
|
}
|
||||||
|
if len(assetIDs) > 100 {
|
||||||
|
return nil, GetError("batch size cannot exceed 100 assets")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build request payload - the API expects an array directly, not wrapped in an object
|
||||||
|
requests := make([]ThumbnailRequest, len(assetIDs))
|
||||||
|
for i, assetID := range assetIDs {
|
||||||
|
requests[i] = ThumbnailRequest{
|
||||||
|
Type: "Asset",
|
||||||
|
TargetID: assetID,
|
||||||
|
Size: string(size),
|
||||||
|
Format: string(format),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(requests)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetError("JSONMarshalError: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "https://thumbnails.roblox.com/v1/batch", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetError("RequestCreationError: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.HttpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetError("RequestError: " + err.Error())
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, GetError(fmt.Sprintf("ResponseError: status code %d, body: %s", resp.StatusCode, string(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetError("ReadBodyError: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var response BatchThumbnailsResponse
|
||||||
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
|
return nil, GetError("JSONUnmarshalError: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserAvatarThumbnails fetches avatar thumbnails for multiple users in a single batch request
|
||||||
|
func (c *Client) GetUserAvatarThumbnails(userIDs []uint64, size ThumbnailSize, format ThumbnailFormat) ([]ThumbnailData, error) {
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return []ThumbnailData{}, nil
|
||||||
|
}
|
||||||
|
if len(userIDs) > 100 {
|
||||||
|
return nil, GetError("batch size cannot exceed 100 users")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build request payload - the API expects an array directly, not wrapped in an object
|
||||||
|
requests := make([]ThumbnailRequest, len(userIDs))
|
||||||
|
for i, userID := range userIDs {
|
||||||
|
requests[i] = ThumbnailRequest{
|
||||||
|
Type: "AvatarHeadShot",
|
||||||
|
TargetID: userID,
|
||||||
|
Size: string(size),
|
||||||
|
Format: string(format),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(requests)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetError("JSONMarshalError: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "https://thumbnails.roblox.com/v1/batch", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetError("RequestCreationError: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.HttpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetError("RequestError: " + err.Error())
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, GetError(fmt.Sprintf("ResponseError: status code %d, body: %s", resp.StatusCode, string(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetError("ReadBodyError: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var response BatchThumbnailsResponse
|
||||||
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
|
return nil, GetError("JSONUnmarshalError: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Data, nil
|
||||||
|
}
|
||||||
72
pkg/roblox/users.go
Normal file
72
pkg/roblox/users.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package roblox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserData represents a single user's information
|
||||||
|
type UserData struct {
|
||||||
|
ID uint64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUsersResponse represents the API response for batch user requests
|
||||||
|
type BatchUsersResponse struct {
|
||||||
|
Data []UserData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsernames fetches usernames for multiple users in a single batch request
|
||||||
|
// Roblox allows up to 100 users per batch
|
||||||
|
func (c *Client) GetUsernames(userIDs []uint64) ([]UserData, error) {
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return []UserData{}, nil
|
||||||
|
}
|
||||||
|
if len(userIDs) > 100 {
|
||||||
|
return nil, GetError("batch size cannot exceed 100 users")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build request payload
|
||||||
|
payload := map[string][]uint64{
|
||||||
|
"userIds": userIDs,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetError("JSONMarshalError: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "https://users.roblox.com/v1/users", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetError("RequestCreationError: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.HttpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetError("RequestError: " + err.Error())
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, GetError(fmt.Sprintf("ResponseError: status code %d, body: %s", resp.StatusCode, string(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetError("ReadBodyError: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var response BatchUsersResponse
|
||||||
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
|
return nil, GetError("JSONUnmarshalError: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Data, nil
|
||||||
|
}
|
||||||
@@ -33,14 +33,20 @@ func (svc *Service) NatsCreateMapfix(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) NatsCheckMapfix(
|
func (svc *Service) NatsCheckMapfix(
|
||||||
MapfixID int64,
|
MapfixID int64,
|
||||||
ModelID uint64,
|
ModelID uint64,
|
||||||
SkipChecks bool,
|
SkipChecks bool,
|
||||||
|
DisplayName string,
|
||||||
|
Creator string,
|
||||||
|
GameID uint32,
|
||||||
) error {
|
) error {
|
||||||
validate_request := model.CheckMapfixRequest{
|
validate_request := model.CheckMapfixRequest{
|
||||||
MapfixID: MapfixID,
|
MapfixID: MapfixID,
|
||||||
ModelID: ModelID,
|
ModelID: ModelID,
|
||||||
SkipChecks: SkipChecks,
|
SkipChecks: SkipChecks,
|
||||||
|
DisplayName: DisplayName,
|
||||||
|
Creator: Creator,
|
||||||
|
GameID: GameID,
|
||||||
}
|
}
|
||||||
|
|
||||||
j, err := json.Marshal(validate_request)
|
j, err := json.Marshal(validate_request)
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"git.itzana.me/strafesnet/go-grpc/maps"
|
"git.itzana.me/strafesnet/go-grpc/maps"
|
||||||
"git.itzana.me/strafesnet/go-grpc/users"
|
"git.itzana.me/strafesnet/go-grpc/users"
|
||||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||||
|
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
db datastore.Datastore
|
db datastore.Datastore
|
||||||
nats nats.JetStreamContext
|
nats nats.JetStreamContext
|
||||||
maps maps.MapsServiceClient
|
maps maps.MapsServiceClient
|
||||||
users users.UsersServiceClient
|
users users.UsersServiceClient
|
||||||
|
thumbnailService *ThumbnailService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
@@ -19,11 +24,44 @@ func NewService(
|
|||||||
nats nats.JetStreamContext,
|
nats nats.JetStreamContext,
|
||||||
maps maps.MapsServiceClient,
|
maps maps.MapsServiceClient,
|
||||||
users users.UsersServiceClient,
|
users users.UsersServiceClient,
|
||||||
|
robloxClient *roblox.Client,
|
||||||
|
redisClient *redis.Client,
|
||||||
) Service {
|
) Service {
|
||||||
return Service{
|
return Service{
|
||||||
db: db,
|
db: db,
|
||||||
nats: nats,
|
nats: nats,
|
||||||
maps: maps,
|
maps: maps,
|
||||||
users: users,
|
users: users,
|
||||||
|
thumbnailService: NewThumbnailService(robloxClient, redisClient),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAssetThumbnails proxies to the thumbnail service
|
||||||
|
func (s *Service) GetAssetThumbnails(ctx context.Context, assetIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
|
||||||
|
return s.thumbnailService.GetAssetThumbnails(ctx, assetIDs, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserAvatarThumbnails proxies to the thumbnail service
|
||||||
|
func (s *Service) GetUserAvatarThumbnails(ctx context.Context, userIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
|
||||||
|
return s.thumbnailService.GetUserAvatarThumbnails(ctx, userIDs, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSingleAssetThumbnail proxies to the thumbnail service
|
||||||
|
func (s *Service) GetSingleAssetThumbnail(ctx context.Context, assetID uint64, size roblox.ThumbnailSize) (string, error) {
|
||||||
|
return s.thumbnailService.GetSingleAssetThumbnail(ctx, assetID, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSingleUserAvatarThumbnail proxies to the thumbnail service
|
||||||
|
func (s *Service) GetSingleUserAvatarThumbnail(ctx context.Context, userID uint64, size roblox.ThumbnailSize) (string, error) {
|
||||||
|
return s.thumbnailService.GetSingleUserAvatarThumbnail(ctx, userID, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsernames proxies to the thumbnail service
|
||||||
|
func (s *Service) GetUsernames(ctx context.Context, userIDs []uint64) (map[uint64]string, error) {
|
||||||
|
return s.thumbnailService.GetUsernames(ctx, userIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSingleUsername proxies to the thumbnail service
|
||||||
|
func (s *Service) GetSingleUsername(ctx context.Context, userID uint64) (string, error) {
|
||||||
|
return s.thumbnailService.GetSingleUsername(ctx, userID)
|
||||||
|
}
|
||||||
|
|||||||
218
pkg/service/thumbnails.go
Normal file
218
pkg/service/thumbnails.go
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ThumbnailService struct {
|
||||||
|
robloxClient *roblox.Client
|
||||||
|
redisClient *redis.Client
|
||||||
|
cacheTTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewThumbnailService(robloxClient *roblox.Client, redisClient *redis.Client) *ThumbnailService {
|
||||||
|
return &ThumbnailService{
|
||||||
|
robloxClient: robloxClient,
|
||||||
|
redisClient: redisClient,
|
||||||
|
cacheTTL: 24 * time.Hour, // Cache thumbnails for 24 hours
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CachedThumbnail represents a cached thumbnail entry
|
||||||
|
type CachedThumbnail struct {
|
||||||
|
ImageURL string `json:"imageUrl"`
|
||||||
|
State string `json:"state"`
|
||||||
|
CachedAt time.Time `json:"cachedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAssetThumbnails fetches thumbnails with Redis caching and batching
|
||||||
|
func (s *ThumbnailService) GetAssetThumbnails(ctx context.Context, assetIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
|
||||||
|
if len(assetIDs) == 0 {
|
||||||
|
return map[uint64]string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uint64]string)
|
||||||
|
var missingIDs []uint64
|
||||||
|
|
||||||
|
// Try to get from cache first
|
||||||
|
for _, assetID := range assetIDs {
|
||||||
|
cacheKey := fmt.Sprintf("thumbnail:asset:%d:%s", assetID, size)
|
||||||
|
cached, err := s.redisClient.Get(ctx, cacheKey).Result()
|
||||||
|
|
||||||
|
if err == redis.Nil {
|
||||||
|
// Cache miss
|
||||||
|
missingIDs = append(missingIDs, assetID)
|
||||||
|
} else if err != nil {
|
||||||
|
// Redis error - treat as cache miss
|
||||||
|
missingIDs = append(missingIDs, assetID)
|
||||||
|
} else {
|
||||||
|
// Cache hit
|
||||||
|
var thumbnail CachedThumbnail
|
||||||
|
if err := json.Unmarshal([]byte(cached), &thumbnail); err == nil && thumbnail.State == "Completed" {
|
||||||
|
result[assetID] = thumbnail.ImageURL
|
||||||
|
} else {
|
||||||
|
missingIDs = append(missingIDs, assetID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all were cached, return early
|
||||||
|
if len(missingIDs) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch fetch missing thumbnails from Roblox API
|
||||||
|
// Split into batches of 100 (Roblox API limit)
|
||||||
|
for i := 0; i < len(missingIDs); i += 100 {
|
||||||
|
end := i + 100
|
||||||
|
if end > len(missingIDs) {
|
||||||
|
end = len(missingIDs)
|
||||||
|
}
|
||||||
|
batch := missingIDs[i:end]
|
||||||
|
|
||||||
|
thumbnails, err := s.robloxClient.GetAssetThumbnails(batch, size, roblox.FormatPng)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch thumbnails: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process results and cache them
|
||||||
|
for _, thumb := range thumbnails {
|
||||||
|
cached := CachedThumbnail{
|
||||||
|
ImageURL: thumb.ImageURL,
|
||||||
|
State: thumb.State,
|
||||||
|
CachedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if thumb.State == "Completed" && thumb.ImageURL != "" {
|
||||||
|
result[thumb.TargetID] = thumb.ImageURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the result (even if incomplete, to avoid repeated API calls)
|
||||||
|
cacheKey := fmt.Sprintf("thumbnail:asset:%d:%s", thumb.TargetID, size)
|
||||||
|
cachedJSON, _ := json.Marshal(cached)
|
||||||
|
|
||||||
|
// Use shorter TTL for incomplete thumbnails
|
||||||
|
ttl := s.cacheTTL
|
||||||
|
if thumb.State != "Completed" {
|
||||||
|
ttl = 5 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
s.redisClient.Set(ctx, cacheKey, cachedJSON, ttl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserAvatarThumbnails fetches user avatar thumbnails with Redis caching and batching
|
||||||
|
func (s *ThumbnailService) GetUserAvatarThumbnails(ctx context.Context, userIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return map[uint64]string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uint64]string)
|
||||||
|
var missingIDs []uint64
|
||||||
|
|
||||||
|
// Try to get from cache first
|
||||||
|
for _, userID := range userIDs {
|
||||||
|
cacheKey := fmt.Sprintf("thumbnail:user:%d:%s", userID, size)
|
||||||
|
cached, err := s.redisClient.Get(ctx, cacheKey).Result()
|
||||||
|
|
||||||
|
if err == redis.Nil {
|
||||||
|
// Cache miss
|
||||||
|
missingIDs = append(missingIDs, userID)
|
||||||
|
} else if err != nil {
|
||||||
|
// Redis error - treat as cache miss
|
||||||
|
missingIDs = append(missingIDs, userID)
|
||||||
|
} else {
|
||||||
|
// Cache hit
|
||||||
|
var thumbnail CachedThumbnail
|
||||||
|
if err := json.Unmarshal([]byte(cached), &thumbnail); err == nil && thumbnail.State == "Completed" {
|
||||||
|
result[userID] = thumbnail.ImageURL
|
||||||
|
} else {
|
||||||
|
missingIDs = append(missingIDs, userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all were cached, return early
|
||||||
|
if len(missingIDs) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch fetch missing thumbnails from Roblox API
|
||||||
|
// Split into batches of 100 (Roblox API limit)
|
||||||
|
for i := 0; i < len(missingIDs); i += 100 {
|
||||||
|
end := i + 100
|
||||||
|
if end > len(missingIDs) {
|
||||||
|
end = len(missingIDs)
|
||||||
|
}
|
||||||
|
batch := missingIDs[i:end]
|
||||||
|
|
||||||
|
thumbnails, err := s.robloxClient.GetUserAvatarThumbnails(batch, size, roblox.FormatPng)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch user thumbnails: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process results and cache them
|
||||||
|
for _, thumb := range thumbnails {
|
||||||
|
cached := CachedThumbnail{
|
||||||
|
ImageURL: thumb.ImageURL,
|
||||||
|
State: thumb.State,
|
||||||
|
CachedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if thumb.State == "Completed" && thumb.ImageURL != "" {
|
||||||
|
result[thumb.TargetID] = thumb.ImageURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
cacheKey := fmt.Sprintf("thumbnail:user:%d:%s", thumb.TargetID, size)
|
||||||
|
cachedJSON, _ := json.Marshal(cached)
|
||||||
|
|
||||||
|
// Use shorter TTL for incomplete thumbnails
|
||||||
|
ttl := s.cacheTTL
|
||||||
|
if thumb.State != "Completed" {
|
||||||
|
ttl = 5 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
s.redisClient.Set(ctx, cacheKey, cachedJSON, ttl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSingleAssetThumbnail is a convenience method for fetching a single asset thumbnail
|
||||||
|
func (s *ThumbnailService) GetSingleAssetThumbnail(ctx context.Context, assetID uint64, size roblox.ThumbnailSize) (string, error) {
|
||||||
|
results, err := s.GetAssetThumbnails(ctx, []uint64{assetID}, size)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if url, ok := results[assetID]; ok {
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("thumbnail not available for asset %d", assetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSingleUserAvatarThumbnail is a convenience method for fetching a single user avatar thumbnail
|
||||||
|
func (s *ThumbnailService) GetSingleUserAvatarThumbnail(ctx context.Context, userID uint64, size roblox.ThumbnailSize) (string, error) {
|
||||||
|
results, err := s.GetUserAvatarThumbnails(ctx, []uint64{userID}, size)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if url, ok := results[userID]; ok {
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("thumbnail not available for user %d", userID)
|
||||||
|
}
|
||||||
108
pkg/service/users.go
Normal file
108
pkg/service/users.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CachedUser represents a cached user entry
|
||||||
|
type CachedUser struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
CachedAt time.Time `json:"cachedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsernames fetches usernames with Redis caching and batching
|
||||||
|
func (s *ThumbnailService) GetUsernames(ctx context.Context, userIDs []uint64) (map[uint64]string, error) {
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return map[uint64]string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uint64]string)
|
||||||
|
var missingIDs []uint64
|
||||||
|
|
||||||
|
// Try to get from cache first
|
||||||
|
for _, userID := range userIDs {
|
||||||
|
cacheKey := fmt.Sprintf("user:name:%d", userID)
|
||||||
|
cached, err := s.redisClient.Get(ctx, cacheKey).Result()
|
||||||
|
|
||||||
|
if err == redis.Nil {
|
||||||
|
// Cache miss
|
||||||
|
missingIDs = append(missingIDs, userID)
|
||||||
|
} else if err != nil {
|
||||||
|
// Redis error - treat as cache miss
|
||||||
|
missingIDs = append(missingIDs, userID)
|
||||||
|
} else {
|
||||||
|
// Cache hit
|
||||||
|
var user CachedUser
|
||||||
|
if err := json.Unmarshal([]byte(cached), &user); err == nil && user.Name != "" {
|
||||||
|
result[userID] = user.Name
|
||||||
|
} else {
|
||||||
|
missingIDs = append(missingIDs, userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all were cached, return early
|
||||||
|
if len(missingIDs) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch fetch missing usernames from Roblox API
|
||||||
|
// Split into batches of 100 (Roblox API limit)
|
||||||
|
for i := 0; i < len(missingIDs); i += 100 {
|
||||||
|
end := i + 100
|
||||||
|
if end > len(missingIDs) {
|
||||||
|
end = len(missingIDs)
|
||||||
|
}
|
||||||
|
batch := missingIDs[i:end]
|
||||||
|
|
||||||
|
var users []roblox.UserData
|
||||||
|
var err error
|
||||||
|
users, err = s.robloxClient.GetUsernames(batch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch usernames: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process results and cache them
|
||||||
|
for _, user := range users {
|
||||||
|
cached := CachedUser{
|
||||||
|
Name: user.Name,
|
||||||
|
DisplayName: user.DisplayName,
|
||||||
|
CachedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Name != "" {
|
||||||
|
result[user.ID] = user.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
cacheKey := fmt.Sprintf("user:name:%d", user.ID)
|
||||||
|
cachedJSON, _ := json.Marshal(cached)
|
||||||
|
|
||||||
|
// Cache usernames for a long time (7 days) since they rarely change
|
||||||
|
s.redisClient.Set(ctx, cacheKey, cachedJSON, 7*24*time.Hour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSingleUsername is a convenience method for fetching a single username
|
||||||
|
func (s *ThumbnailService) GetSingleUsername(ctx context.Context, userID uint64) (string, error) {
|
||||||
|
results, err := s.GetUsernames(ctx, []uint64{userID})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if name, ok := results[userID]; ok {
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("username not available for user %d", userID)
|
||||||
|
}
|
||||||
@@ -327,6 +327,48 @@ func (svc *Service) UpdateMapfixModel(ctx context.Context, params api.UpdateMapf
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateMapfixDescription implements updateMapfixDescription operation.
|
||||||
|
//
|
||||||
|
// Update description (submitter only, status ChangesRequested or UnderConstruction).
|
||||||
|
//
|
||||||
|
// PATCH /mapfixes/{MapfixID}/description
|
||||||
|
func (svc *Service) UpdateMapfixDescription(ctx context.Context, req api.UpdateMapfixDescriptionReq, params api.UpdateMapfixDescriptionParams) error {
|
||||||
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||||
|
if !ok {
|
||||||
|
return ErrUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// read mapfix
|
||||||
|
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := userInfo.GetUserID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if caller is the submitter
|
||||||
|
if userId != mapfix.Submitter {
|
||||||
|
return ErrPermissionDeniedNotSubmitter
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the new description from request body
|
||||||
|
data, err := io.ReadAll(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newDescription := string(data)
|
||||||
|
|
||||||
|
// check if Status is ChangesRequested or UnderConstruction
|
||||||
|
update := service.NewMapfixUpdate()
|
||||||
|
update.SetDescription(newDescription)
|
||||||
|
allow_statuses := []model.MapfixStatus{model.MapfixStatusChangesRequested, model.MapfixStatusUnderConstruction}
|
||||||
|
return svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
|
||||||
|
}
|
||||||
|
|
||||||
// ActionMapfixReject invokes actionMapfixReject operation.
|
// ActionMapfixReject invokes actionMapfixReject operation.
|
||||||
//
|
//
|
||||||
// Role Reviewer changes status from Submitted -> Rejected.
|
// Role Reviewer changes status from Submitted -> Rejected.
|
||||||
@@ -406,7 +448,12 @@ func (svc *Service) ActionMapfixRequestChanges(ctx context.Context, params api.A
|
|||||||
target_status := model.MapfixStatusChangesRequested
|
target_status := model.MapfixStatusChangesRequested
|
||||||
update := service.NewMapfixUpdate()
|
update := service.NewMapfixUpdate()
|
||||||
update.SetStatusID(target_status)
|
update.SetStatusID(target_status)
|
||||||
allow_statuses := []model.MapfixStatus{model.MapfixStatusValidated, model.MapfixStatusAcceptedUnvalidated, model.MapfixStatusSubmitted}
|
allow_statuses := []model.MapfixStatus{
|
||||||
|
model.MapfixStatusUploaded,
|
||||||
|
model.MapfixStatusValidated,
|
||||||
|
model.MapfixStatusAcceptedUnvalidated,
|
||||||
|
model.MapfixStatusSubmitted,
|
||||||
|
}
|
||||||
err = svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
|
err = svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -491,13 +538,13 @@ func (svc *Service) ActionMapfixTriggerSubmit(ctx context.Context, params api.Ac
|
|||||||
return ErrUserInfo
|
return ErrUserInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// read mapfix (this could be done with a transaction WHERE clause)
|
userId, err := userInfo.GetUserID()
|
||||||
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
userId, err := userInfo.GetUserID()
|
// read mapfix (this could be done with a transaction WHERE clause)
|
||||||
|
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -508,6 +555,12 @@ func (svc *Service) ActionMapfixTriggerSubmit(ctx context.Context, params api.Ac
|
|||||||
return ErrPermissionDeniedNotSubmitter
|
return ErrPermissionDeniedNotSubmitter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// read map to get current DisplayName and such
|
||||||
|
target_map, err := svc.inner.GetMap(ctx, int64(mapfix.TargetAssetID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// transaction
|
// transaction
|
||||||
target_status := model.MapfixStatusSubmitting
|
target_status := model.MapfixStatusSubmitting
|
||||||
update := service.NewMapfixUpdate()
|
update := service.NewMapfixUpdate()
|
||||||
@@ -522,6 +575,9 @@ func (svc *Service) ActionMapfixTriggerSubmit(ctx context.Context, params api.Ac
|
|||||||
mapfix.ID,
|
mapfix.ID,
|
||||||
mapfix.AssetID,
|
mapfix.AssetID,
|
||||||
false,
|
false,
|
||||||
|
target_map.DisplayName,
|
||||||
|
target_map.Creator,
|
||||||
|
target_map.GameID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -553,13 +609,13 @@ func (svc *Service) ActionMapfixTriggerSubmitUnchecked(ctx context.Context, para
|
|||||||
return ErrUserInfo
|
return ErrUserInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// read mapfix (this could be done with a transaction WHERE clause)
|
userId, err := userInfo.GetUserID()
|
||||||
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
userId, err := userInfo.GetUserID()
|
// read mapfix (this could be done with a transaction WHERE clause)
|
||||||
|
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -579,6 +635,12 @@ func (svc *Service) ActionMapfixTriggerSubmitUnchecked(ctx context.Context, para
|
|||||||
return ErrPermissionDeniedNeedRoleMapfixReview
|
return ErrPermissionDeniedNeedRoleMapfixReview
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// read map to get current DisplayName and such
|
||||||
|
target_map, err := svc.inner.GetMap(ctx, int64(mapfix.TargetAssetID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// transaction
|
// transaction
|
||||||
target_status := model.MapfixStatusSubmitting
|
target_status := model.MapfixStatusSubmitting
|
||||||
update := service.NewMapfixUpdate()
|
update := service.NewMapfixUpdate()
|
||||||
@@ -593,6 +655,9 @@ func (svc *Service) ActionMapfixTriggerSubmitUnchecked(ctx context.Context, para
|
|||||||
mapfix.ID,
|
mapfix.ID,
|
||||||
mapfix.AssetID,
|
mapfix.AssetID,
|
||||||
true,
|
true,
|
||||||
|
target_map.DisplayName,
|
||||||
|
target_map.Creator,
|
||||||
|
target_map.GameID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -36,10 +36,28 @@ func (svc *Service) CreateScript(ctx context.Context, req *api.ScriptCreate) (*a
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hash := int64(model.HashSource(req.Source))
|
||||||
|
|
||||||
|
// Check if a script with this hash already exists
|
||||||
|
filter := service.NewScriptFilter()
|
||||||
|
filter.SetHash(hash)
|
||||||
|
existingScripts, err := svc.inner.ListScripts(ctx, filter, model.Page{Number: 1, Size: 1})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If script with this hash exists, return existing script ID
|
||||||
|
if len(existingScripts) > 0 {
|
||||||
|
return &api.ScriptID{
|
||||||
|
ScriptID: existingScripts[0].ID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new script
|
||||||
script, err := svc.inner.CreateScript(ctx, model.Script{
|
script, err := svc.inner.CreateScript(ctx, model.Script{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Hash: int64(model.HashSource(req.Source)),
|
Hash: hash,
|
||||||
Source: req.Source,
|
Source: req.Source,
|
||||||
ResourceType: model.ResourceType(req.ResourceType),
|
ResourceType: model.ResourceType(req.ResourceType),
|
||||||
ResourceID: req.ResourceID.Or(0),
|
ResourceID: req.ResourceID.Or(0),
|
||||||
|
|||||||
105
pkg/web_api/stats.go
Normal file
105
pkg/web_api/stats.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package web_api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.itzana.me/strafesnet/maps-service/pkg/api"
|
||||||
|
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||||
|
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||||
|
"git.itzana.me/strafesnet/maps-service/pkg/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /stats
|
||||||
|
func (svc *Service) GetStats(ctx context.Context) (*api.Stats, error) {
|
||||||
|
// Get total submissions count
|
||||||
|
totalSubmissions, _, err := svc.inner.ListSubmissionsWithTotal(ctx, service.NewSubmissionFilter(), model.Page{
|
||||||
|
Number: 1,
|
||||||
|
Size: 0, // We only want the count, not the items
|
||||||
|
}, datastore.ListSortDisabled)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total mapfixes count
|
||||||
|
totalMapfixes, _, err := svc.inner.ListMapfixesWithTotal(ctx, service.NewMapfixFilter(), model.Page{
|
||||||
|
Number: 1,
|
||||||
|
Size: 0, // We only want the count, not the items
|
||||||
|
}, datastore.ListSortDisabled)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get released submissions count
|
||||||
|
releasedSubmissionsFilter := service.NewSubmissionFilter()
|
||||||
|
releasedSubmissionsFilter.SetStatuses([]model.SubmissionStatus{model.SubmissionStatusReleased})
|
||||||
|
releasedSubmissions, _, err := svc.inner.ListSubmissionsWithTotal(ctx, releasedSubmissionsFilter, model.Page{
|
||||||
|
Number: 1,
|
||||||
|
Size: 0,
|
||||||
|
}, datastore.ListSortDisabled)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get released mapfixes count
|
||||||
|
releasedMapfixesFilter := service.NewMapfixFilter()
|
||||||
|
releasedMapfixesFilter.SetStatuses([]model.MapfixStatus{model.MapfixStatusReleased})
|
||||||
|
releasedMapfixes, _, err := svc.inner.ListMapfixesWithTotal(ctx, releasedMapfixesFilter, model.Page{
|
||||||
|
Number: 1,
|
||||||
|
Size: 0,
|
||||||
|
}, datastore.ListSortDisabled)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get submitted submissions count (under review)
|
||||||
|
submittedSubmissionsFilter := service.NewSubmissionFilter()
|
||||||
|
submittedSubmissionsFilter.SetStatuses([]model.SubmissionStatus{
|
||||||
|
model.SubmissionStatusUnderConstruction,
|
||||||
|
model.SubmissionStatusChangesRequested,
|
||||||
|
model.SubmissionStatusSubmitting,
|
||||||
|
model.SubmissionStatusSubmitted,
|
||||||
|
model.SubmissionStatusAcceptedUnvalidated,
|
||||||
|
model.SubmissionStatusValidating,
|
||||||
|
model.SubmissionStatusValidated,
|
||||||
|
model.SubmissionStatusUploading,
|
||||||
|
model.SubmissionStatusUploaded,
|
||||||
|
})
|
||||||
|
submittedSubmissions, _, err := svc.inner.ListSubmissionsWithTotal(ctx, submittedSubmissionsFilter, model.Page{
|
||||||
|
Number: 1,
|
||||||
|
Size: 0,
|
||||||
|
}, datastore.ListSortDisabled)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get submitted mapfixes count (under review)
|
||||||
|
submittedMapfixesFilter := service.NewMapfixFilter()
|
||||||
|
submittedMapfixesFilter.SetStatuses([]model.MapfixStatus{
|
||||||
|
model.MapfixStatusUnderConstruction,
|
||||||
|
model.MapfixStatusChangesRequested,
|
||||||
|
model.MapfixStatusSubmitting,
|
||||||
|
model.MapfixStatusSubmitted,
|
||||||
|
model.MapfixStatusAcceptedUnvalidated,
|
||||||
|
model.MapfixStatusValidating,
|
||||||
|
model.MapfixStatusValidated,
|
||||||
|
model.MapfixStatusUploading,
|
||||||
|
model.MapfixStatusUploaded,
|
||||||
|
model.MapfixStatusReleasing,
|
||||||
|
})
|
||||||
|
submittedMapfixes, _, err := svc.inner.ListMapfixesWithTotal(ctx, submittedMapfixesFilter, model.Page{
|
||||||
|
Number: 1,
|
||||||
|
Size: 0,
|
||||||
|
}, datastore.ListSortDisabled)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &api.Stats{
|
||||||
|
TotalSubmissions: totalSubmissions,
|
||||||
|
TotalMapfixes: totalMapfixes,
|
||||||
|
ReleasedSubmissions: releasedSubmissions,
|
||||||
|
ReleasedMapfixes: releasedMapfixes,
|
||||||
|
SubmittedSubmissions: submittedSubmissions,
|
||||||
|
SubmittedMapfixes: submittedMapfixes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -437,7 +437,12 @@ func (svc *Service) ActionSubmissionRequestChanges(ctx context.Context, params a
|
|||||||
target_status := model.SubmissionStatusChangesRequested
|
target_status := model.SubmissionStatusChangesRequested
|
||||||
update := service.NewSubmissionUpdate()
|
update := service.NewSubmissionUpdate()
|
||||||
update.SetStatusID(target_status)
|
update.SetStatusID(target_status)
|
||||||
allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusValidated, model.SubmissionStatusAcceptedUnvalidated, model.SubmissionStatusSubmitted}
|
allowed_statuses := []model.SubmissionStatus{
|
||||||
|
model.SubmissionStatusUploaded,
|
||||||
|
model.SubmissionStatusValidated,
|
||||||
|
model.SubmissionStatusAcceptedUnvalidated,
|
||||||
|
model.SubmissionStatusSubmitted,
|
||||||
|
}
|
||||||
err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update)
|
err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
135
pkg/web_api/thumbnails.go
Normal file
135
pkg/web_api/thumbnails.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package web_api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.itzana.me/strafesnet/maps-service/pkg/api"
|
||||||
|
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BatchAssetThumbnails handles batch fetching of asset thumbnails
|
||||||
|
func (svc *Service) BatchAssetThumbnails(ctx context.Context, req *api.BatchAssetThumbnailsReq) (*api.BatchAssetThumbnailsOK, error) {
|
||||||
|
if len(req.AssetIds) == 0 {
|
||||||
|
return &api.BatchAssetThumbnailsOK{
|
||||||
|
Thumbnails: api.NewOptBatchAssetThumbnailsOKThumbnails(map[string]string{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert size string to enum
|
||||||
|
size := roblox.Size420x420
|
||||||
|
if req.Size.IsSet() {
|
||||||
|
sizeStr := req.Size.Value
|
||||||
|
switch api.BatchAssetThumbnailsReqSize(sizeStr) {
|
||||||
|
case api.BatchAssetThumbnailsReqSize150x150:
|
||||||
|
size = roblox.Size150x150
|
||||||
|
case api.BatchAssetThumbnailsReqSize768x432:
|
||||||
|
size = roblox.Size768x432
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch thumbnails from service
|
||||||
|
thumbnails, err := svc.inner.GetAssetThumbnails(ctx, req.AssetIds, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map[uint64]string to map[string]string for JSON
|
||||||
|
result := make(map[string]string, len(thumbnails))
|
||||||
|
for assetID, url := range thumbnails {
|
||||||
|
result[strconv.FormatUint(assetID, 10)] = url
|
||||||
|
}
|
||||||
|
|
||||||
|
return &api.BatchAssetThumbnailsOK{
|
||||||
|
Thumbnails: api.NewOptBatchAssetThumbnailsOKThumbnails(result),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAssetThumbnail handles single asset thumbnail fetch (with redirect)
|
||||||
|
func (svc *Service) GetAssetThumbnail(ctx context.Context, params api.GetAssetThumbnailParams) (*api.GetAssetThumbnailFound, error) {
|
||||||
|
// Convert size string to enum
|
||||||
|
size := roblox.Size420x420
|
||||||
|
if params.Size.IsSet() {
|
||||||
|
sizeStr := params.Size.Value
|
||||||
|
switch api.GetAssetThumbnailSize(sizeStr) {
|
||||||
|
case api.GetAssetThumbnailSize150x150:
|
||||||
|
size = roblox.Size150x150
|
||||||
|
case api.GetAssetThumbnailSize768x432:
|
||||||
|
size = roblox.Size768x432
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch thumbnail
|
||||||
|
thumbnailURL, err := svc.inner.GetSingleAssetThumbnail(ctx, params.AssetID, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return redirect response
|
||||||
|
return &api.GetAssetThumbnailFound{
|
||||||
|
Location: api.NewOptString(thumbnailURL),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUserThumbnails handles batch fetching of user avatar thumbnails
|
||||||
|
func (svc *Service) BatchUserThumbnails(ctx context.Context, req *api.BatchUserThumbnailsReq) (*api.BatchUserThumbnailsOK, error) {
|
||||||
|
if len(req.UserIds) == 0 {
|
||||||
|
return &api.BatchUserThumbnailsOK{
|
||||||
|
Thumbnails: api.NewOptBatchUserThumbnailsOKThumbnails(map[string]string{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert size string to enum
|
||||||
|
size := roblox.Size150x150
|
||||||
|
if req.Size.IsSet() {
|
||||||
|
sizeStr := req.Size.Value
|
||||||
|
switch api.BatchUserThumbnailsReqSize(sizeStr) {
|
||||||
|
case api.BatchUserThumbnailsReqSize420x420:
|
||||||
|
size = roblox.Size420x420
|
||||||
|
case api.BatchUserThumbnailsReqSize768x432:
|
||||||
|
size = roblox.Size768x432
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch thumbnails from service
|
||||||
|
thumbnails, err := svc.inner.GetUserAvatarThumbnails(ctx, req.UserIds, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map[uint64]string to map[string]string for JSON
|
||||||
|
result := make(map[string]string, len(thumbnails))
|
||||||
|
for userID, url := range thumbnails {
|
||||||
|
result[strconv.FormatUint(userID, 10)] = url
|
||||||
|
}
|
||||||
|
|
||||||
|
return &api.BatchUserThumbnailsOK{
|
||||||
|
Thumbnails: api.NewOptBatchUserThumbnailsOKThumbnails(result),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserThumbnail handles single user avatar thumbnail fetch (with redirect)
|
||||||
|
func (svc *Service) GetUserThumbnail(ctx context.Context, params api.GetUserThumbnailParams) (*api.GetUserThumbnailFound, error) {
|
||||||
|
// Convert size string to enum
|
||||||
|
size := roblox.Size150x150
|
||||||
|
if params.Size.IsSet() {
|
||||||
|
sizeStr := params.Size.Value
|
||||||
|
switch api.GetUserThumbnailSize(sizeStr) {
|
||||||
|
case api.GetUserThumbnailSize420x420:
|
||||||
|
size = roblox.Size420x420
|
||||||
|
case api.GetUserThumbnailSize768x432:
|
||||||
|
size = roblox.Size768x432
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch thumbnail
|
||||||
|
thumbnailURL, err := svc.inner.GetSingleUserAvatarThumbnail(ctx, params.UserID, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return redirect response
|
||||||
|
return &api.GetUserThumbnailFound{
|
||||||
|
Location: api.NewOptString(thumbnailURL),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
33
pkg/web_api/users.go
Normal file
33
pkg/web_api/users.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package web_api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.itzana.me/strafesnet/maps-service/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BatchUsernames handles batch fetching of usernames
|
||||||
|
func (svc *Service) BatchUsernames(ctx context.Context, req *api.BatchUsernamesReq) (*api.BatchUsernamesOK, error) {
|
||||||
|
if len(req.UserIds) == 0 {
|
||||||
|
return &api.BatchUsernamesOK{
|
||||||
|
Usernames: api.NewOptBatchUsernamesOKUsernames(map[string]string{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch usernames from service
|
||||||
|
usernames, err := svc.inner.GetUsernames(ctx, req.UserIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map[uint64]string to map[string]string for JSON
|
||||||
|
result := make(map[string]string, len(usernames))
|
||||||
|
for userID, username := range usernames {
|
||||||
|
result[strconv.FormatUint(userID, 10)] = username
|
||||||
|
}
|
||||||
|
|
||||||
|
return &api.BatchUsernamesOK{
|
||||||
|
Usernames: api.NewOptBatchUsernamesOKUsernames(result),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -30,7 +30,6 @@ impl<Items> std::error::Error for SingleItemError<Items> where Items:std::fmt::D
|
|||||||
pub type ScriptSingleItemError=SingleItemError<Vec<ScriptID>>;
|
pub type ScriptSingleItemError=SingleItemError<Vec<ScriptID>>;
|
||||||
pub type ScriptPolicySingleItemError=SingleItemError<Vec<ScriptPolicyID>>;
|
pub type ScriptPolicySingleItemError=SingleItemError<Vec<ScriptPolicyID>>;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct UrlAndBody{
|
pub struct UrlAndBody{
|
||||||
pub url:url::Url,
|
pub url:url::Url,
|
||||||
@@ -76,7 +75,7 @@ pub enum GameID{
|
|||||||
FlyTrials=5,
|
FlyTrials=5,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Serialize)]
|
#[derive(Clone,Debug,serde::Serialize)]
|
||||||
pub struct CreateMapfixRequest<'a>{
|
pub struct CreateMapfixRequest<'a>{
|
||||||
pub OperationID:OperationID,
|
pub OperationID:OperationID,
|
||||||
@@ -89,13 +88,13 @@ pub struct CreateMapfixRequest<'a>{
|
|||||||
pub TargetAssetID:u64,
|
pub TargetAssetID:u64,
|
||||||
pub Description:&'a str,
|
pub Description:&'a str,
|
||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Deserialize)]
|
||||||
pub struct MapfixIDResponse{
|
pub struct MapfixIDResponse{
|
||||||
pub MapfixID:MapfixID,
|
pub MapfixID:MapfixID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Serialize)]
|
#[derive(Clone,Debug,serde::Serialize)]
|
||||||
pub struct CreateSubmissionRequest<'a>{
|
pub struct CreateSubmissionRequest<'a>{
|
||||||
pub OperationID:OperationID,
|
pub OperationID:OperationID,
|
||||||
@@ -108,7 +107,7 @@ pub struct CreateSubmissionRequest<'a>{
|
|||||||
pub Status:u32,
|
pub Status:u32,
|
||||||
pub Roles:u32,
|
pub Roles:u32,
|
||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Deserialize)]
|
||||||
pub struct SubmissionIDResponse{
|
pub struct SubmissionIDResponse{
|
||||||
pub SubmissionID:SubmissionID,
|
pub SubmissionID:SubmissionID,
|
||||||
@@ -127,11 +126,11 @@ pub enum ResourceType{
|
|||||||
Submission=2,
|
Submission=2,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
pub struct GetScriptRequest{
|
pub struct GetScriptRequest{
|
||||||
pub ScriptID:ScriptID,
|
pub ScriptID:ScriptID,
|
||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Serialize)]
|
#[derive(Clone,Debug,serde::Serialize)]
|
||||||
pub struct GetScriptsRequest<'a>{
|
pub struct GetScriptsRequest<'a>{
|
||||||
pub Page:u32,
|
pub Page:u32,
|
||||||
@@ -151,7 +150,7 @@ pub struct GetScriptsRequest<'a>{
|
|||||||
pub struct HashRequest<'a>{
|
pub struct HashRequest<'a>{
|
||||||
pub hash:&'a str,
|
pub hash:&'a str,
|
||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Deserialize)]
|
||||||
pub struct ScriptResponse{
|
pub struct ScriptResponse{
|
||||||
pub ID:ScriptID,
|
pub ID:ScriptID,
|
||||||
@@ -161,7 +160,7 @@ pub struct ScriptResponse{
|
|||||||
pub ResourceType:ResourceType,
|
pub ResourceType:ResourceType,
|
||||||
pub ResourceID:ResourceID,
|
pub ResourceID:ResourceID,
|
||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Serialize)]
|
#[derive(Clone,Debug,serde::Serialize)]
|
||||||
pub struct CreateScriptRequest<'a>{
|
pub struct CreateScriptRequest<'a>{
|
||||||
pub Name:&'a str,
|
pub Name:&'a str,
|
||||||
@@ -170,7 +169,7 @@ pub struct CreateScriptRequest<'a>{
|
|||||||
#[serde(skip_serializing_if="Option::is_none")]
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
pub ResourceID:Option<ResourceID>,
|
pub ResourceID:Option<ResourceID>,
|
||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Deserialize)]
|
||||||
pub struct ScriptIDResponse{
|
pub struct ScriptIDResponse{
|
||||||
pub ScriptID:ScriptID,
|
pub ScriptID:ScriptID,
|
||||||
@@ -186,11 +185,11 @@ pub enum Policy{
|
|||||||
Replace=4,
|
Replace=4,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
pub struct GetScriptPolicyRequest{
|
pub struct GetScriptPolicyRequest{
|
||||||
pub ScriptPolicyID:ScriptPolicyID,
|
pub ScriptPolicyID:ScriptPolicyID,
|
||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Serialize)]
|
#[derive(Clone,Debug,serde::Serialize)]
|
||||||
pub struct GetScriptPoliciesRequest<'a>{
|
pub struct GetScriptPoliciesRequest<'a>{
|
||||||
pub Page:u32,
|
pub Page:u32,
|
||||||
@@ -202,7 +201,7 @@ pub struct GetScriptPoliciesRequest<'a>{
|
|||||||
#[serde(skip_serializing_if="Option::is_none")]
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
pub Policy:Option<Policy>,
|
pub Policy:Option<Policy>,
|
||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Deserialize)]
|
||||||
pub struct ScriptPolicyResponse{
|
pub struct ScriptPolicyResponse{
|
||||||
pub ID:ScriptPolicyID,
|
pub ID:ScriptPolicyID,
|
||||||
@@ -210,20 +209,20 @@ pub struct ScriptPolicyResponse{
|
|||||||
pub ToScriptID:ScriptID,
|
pub ToScriptID:ScriptID,
|
||||||
pub Policy:Policy
|
pub Policy:Policy
|
||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Serialize)]
|
#[derive(Clone,Debug,serde::Serialize)]
|
||||||
pub struct CreateScriptPolicyRequest{
|
pub struct CreateScriptPolicyRequest{
|
||||||
pub FromScriptID:ScriptID,
|
pub FromScriptID:ScriptID,
|
||||||
pub ToScriptID:ScriptID,
|
pub ToScriptID:ScriptID,
|
||||||
pub Policy:Policy,
|
pub Policy:Policy,
|
||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Deserialize)]
|
||||||
pub struct ScriptPolicyIDResponse{
|
pub struct ScriptPolicyIDResponse{
|
||||||
pub ScriptPolicyID:ScriptPolicyID,
|
pub ScriptPolicyID:ScriptPolicyID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Serialize)]
|
#[derive(Clone,Debug,serde::Serialize)]
|
||||||
pub struct UpdateScriptPolicyRequest{
|
pub struct UpdateScriptPolicyRequest{
|
||||||
pub ID:ScriptPolicyID,
|
pub ID:ScriptPolicyID,
|
||||||
@@ -235,7 +234,7 @@ pub struct UpdateScriptPolicyRequest{
|
|||||||
pub Policy:Option<Policy>,
|
pub Policy:Option<Policy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct UpdateSubmissionModelRequest{
|
pub struct UpdateSubmissionModelRequest{
|
||||||
pub SubmissionID:SubmissionID,
|
pub SubmissionID:SubmissionID,
|
||||||
@@ -276,7 +275,7 @@ pub enum MapfixStatus{
|
|||||||
Released=10,
|
Released=10,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct GetMapfixesRequest<'a>{
|
pub struct GetMapfixesRequest<'a>{
|
||||||
pub Page:u32,
|
pub Page:u32,
|
||||||
@@ -292,7 +291,7 @@ pub struct GetMapfixesRequest<'a>{
|
|||||||
pub StatusID:Option<MapfixStatus>,
|
pub StatusID:Option<MapfixStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Serialize,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Serialize,serde::Deserialize)]
|
||||||
pub struct MapfixResponse{
|
pub struct MapfixResponse{
|
||||||
pub ID:MapfixID,
|
pub ID:MapfixID,
|
||||||
@@ -312,7 +311,7 @@ pub struct MapfixResponse{
|
|||||||
pub Description:String,
|
pub Description:String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Deserialize)]
|
||||||
pub struct MapfixesResponse{
|
pub struct MapfixesResponse{
|
||||||
pub Total:u64,
|
pub Total:u64,
|
||||||
@@ -342,7 +341,7 @@ pub enum SubmissionStatus{
|
|||||||
Released=10,
|
Released=10,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct GetSubmissionsRequest<'a>{
|
pub struct GetSubmissionsRequest<'a>{
|
||||||
pub Page:u32,
|
pub Page:u32,
|
||||||
@@ -358,7 +357,7 @@ pub struct GetSubmissionsRequest<'a>{
|
|||||||
pub StatusID:Option<SubmissionStatus>,
|
pub StatusID:Option<SubmissionStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Deserialize)]
|
||||||
pub struct SubmissionResponse{
|
pub struct SubmissionResponse{
|
||||||
pub ID:SubmissionID,
|
pub ID:SubmissionID,
|
||||||
@@ -376,14 +375,14 @@ pub struct SubmissionResponse{
|
|||||||
pub StatusID:SubmissionStatus,
|
pub StatusID:SubmissionStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Deserialize)]
|
||||||
pub struct SubmissionsResponse{
|
pub struct SubmissionsResponse{
|
||||||
pub Total:u64,
|
pub Total:u64,
|
||||||
pub Submissions:Vec<SubmissionResponse>,
|
pub Submissions:Vec<SubmissionResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct GetMapsRequest<'a>{
|
pub struct GetMapsRequest<'a>{
|
||||||
pub Page:u32,
|
pub Page:u32,
|
||||||
@@ -394,7 +393,7 @@ pub struct GetMapsRequest<'a>{
|
|||||||
pub GameID:Option<GameID>,
|
pub GameID:Option<GameID>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Deserialize)]
|
||||||
pub struct MapResponse{
|
pub struct MapResponse{
|
||||||
pub ID:i64,
|
pub ID:i64,
|
||||||
@@ -404,7 +403,7 @@ pub struct MapResponse{
|
|||||||
pub Date:i64,
|
pub Date:i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct GetMapfixAuditEventsRequest{
|
pub struct GetMapfixAuditEventsRequest{
|
||||||
pub Page:u32,
|
pub Page:u32,
|
||||||
@@ -412,7 +411,7 @@ pub struct GetMapfixAuditEventsRequest{
|
|||||||
pub MapfixID:i64,
|
pub MapfixID:i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct GetSubmissionAuditEventsRequest{
|
pub struct GetSubmissionAuditEventsRequest{
|
||||||
pub Page:u32,
|
pub Page:u32,
|
||||||
@@ -420,7 +419,6 @@ pub struct GetSubmissionAuditEventsRequest{
|
|||||||
pub SubmissionID:i64,
|
pub SubmissionID:i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
|
||||||
#[derive(Clone,Debug,serde_repr::Deserialize_repr)]
|
#[derive(Clone,Debug,serde_repr::Deserialize_repr)]
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
pub enum AuditEventType{
|
pub enum AuditEventType{
|
||||||
@@ -475,7 +473,6 @@ pub struct AuditEventCheckList{
|
|||||||
pub check_list:Vec<AuditEventCheck>,
|
pub check_list:Vec<AuditEventCheck>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub enum AuditEventData{
|
pub enum AuditEventData{
|
||||||
Action(AuditEventAction),
|
Action(AuditEventAction),
|
||||||
@@ -491,7 +488,7 @@ pub enum AuditEventData{
|
|||||||
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,serde::Serialize,serde::Deserialize)]
|
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,serde::Serialize,serde::Deserialize)]
|
||||||
pub struct AuditEventID(pub(crate)i64);
|
pub struct AuditEventID(pub(crate)i64);
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Deserialize)]
|
||||||
pub struct AuditEventReponse{
|
pub struct AuditEventReponse{
|
||||||
pub ID:AuditEventID,
|
pub ID:AuditEventID,
|
||||||
@@ -518,7 +515,7 @@ impl AuditEventReponse{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Serialize)]
|
#[derive(Clone,Debug,serde::Serialize)]
|
||||||
pub struct Check{
|
pub struct Check{
|
||||||
pub Name:&'static str,
|
pub Name:&'static str,
|
||||||
@@ -526,7 +523,7 @@ pub struct Check{
|
|||||||
pub Passed:bool,
|
pub Passed:bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct ActionSubmissionSubmittedRequest{
|
pub struct ActionSubmissionSubmittedRequest{
|
||||||
pub SubmissionID:SubmissionID,
|
pub SubmissionID:SubmissionID,
|
||||||
@@ -536,33 +533,33 @@ pub struct ActionSubmissionSubmittedRequest{
|
|||||||
pub GameID:GameID,
|
pub GameID:GameID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct ActionSubmissionRequestChangesRequest{
|
pub struct ActionSubmissionRequestChangesRequest{
|
||||||
pub SubmissionID:SubmissionID,
|
pub SubmissionID:SubmissionID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct ActionSubmissionUploadedRequest{
|
pub struct ActionSubmissionUploadedRequest{
|
||||||
pub SubmissionID:SubmissionID,
|
pub SubmissionID:SubmissionID,
|
||||||
pub UploadedAssetID:u64,
|
pub UploadedAssetID:u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct ActionSubmissionAcceptedRequest{
|
pub struct ActionSubmissionAcceptedRequest{
|
||||||
pub SubmissionID:SubmissionID,
|
pub SubmissionID:SubmissionID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct CreateSubmissionAuditErrorRequest{
|
pub struct CreateSubmissionAuditErrorRequest{
|
||||||
pub SubmissionID:SubmissionID,
|
pub SubmissionID:SubmissionID,
|
||||||
pub ErrorMessage:String,
|
pub ErrorMessage:String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct CreateSubmissionAuditCheckListRequest<'a>{
|
pub struct CreateSubmissionAuditCheckListRequest<'a>{
|
||||||
pub SubmissionID:SubmissionID,
|
pub SubmissionID:SubmissionID,
|
||||||
@@ -580,7 +577,7 @@ impl SubmissionID{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct UpdateMapfixModelRequest{
|
pub struct UpdateMapfixModelRequest{
|
||||||
pub MapfixID:MapfixID,
|
pub MapfixID:MapfixID,
|
||||||
@@ -588,7 +585,7 @@ pub struct UpdateMapfixModelRequest{
|
|||||||
pub ModelVersion:u64,
|
pub ModelVersion:u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct ActionMapfixSubmittedRequest{
|
pub struct ActionMapfixSubmittedRequest{
|
||||||
pub MapfixID:MapfixID,
|
pub MapfixID:MapfixID,
|
||||||
@@ -598,32 +595,32 @@ pub struct ActionMapfixSubmittedRequest{
|
|||||||
pub GameID:GameID,
|
pub GameID:GameID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct ActionMapfixRequestChangesRequest{
|
pub struct ActionMapfixRequestChangesRequest{
|
||||||
pub MapfixID:MapfixID,
|
pub MapfixID:MapfixID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct ActionMapfixUploadedRequest{
|
pub struct ActionMapfixUploadedRequest{
|
||||||
pub MapfixID:MapfixID,
|
pub MapfixID:MapfixID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct ActionMapfixAcceptedRequest{
|
pub struct ActionMapfixAcceptedRequest{
|
||||||
pub MapfixID:MapfixID,
|
pub MapfixID:MapfixID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct CreateMapfixAuditErrorRequest{
|
pub struct CreateMapfixAuditErrorRequest{
|
||||||
pub MapfixID:MapfixID,
|
pub MapfixID:MapfixID,
|
||||||
pub ErrorMessage:String,
|
pub ErrorMessage:String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct CreateMapfixAuditCheckListRequest<'a>{
|
pub struct CreateMapfixAuditCheckListRequest<'a>{
|
||||||
pub MapfixID:MapfixID,
|
pub MapfixID:MapfixID,
|
||||||
@@ -641,7 +638,7 @@ impl MapfixID{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone,Debug)]
|
||||||
pub struct ActionOperationFailedRequest{
|
pub struct ActionOperationFailedRequest{
|
||||||
pub OperationID:OperationID,
|
pub OperationID:OperationID,
|
||||||
@@ -668,7 +665,7 @@ impl Resource{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Serialize)]
|
#[derive(Clone,Debug,serde::Serialize)]
|
||||||
pub struct ReleaseInfo{
|
pub struct ReleaseInfo{
|
||||||
pub SubmissionID:SubmissionID,
|
pub SubmissionID:SubmissionID,
|
||||||
@@ -678,7 +675,7 @@ pub struct ReleaseInfo{
|
|||||||
pub struct ReleaseRequest<'a>{
|
pub struct ReleaseRequest<'a>{
|
||||||
pub schedule:&'a [ReleaseInfo],
|
pub schedule:&'a [ReleaseInfo],
|
||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(Clone,Debug,serde::Deserialize)]
|
#[derive(Clone,Debug,serde::Deserialize)]
|
||||||
pub struct OperationIDResponse{
|
pub struct OperationIDResponse{
|
||||||
pub OperationID:OperationID,
|
pub OperationID:OperationID,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ rbx_xml = "2.0.0"
|
|||||||
regex = { version = "1.11.3", default-features = false }
|
regex = { version = "1.11.3", default-features = false }
|
||||||
serde = { version = "1.0.215", features = ["derive"] }
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
serde_json = "1.0.133"
|
serde_json = "1.0.133"
|
||||||
|
serde_repr = "0.1.19"
|
||||||
siphasher = "1.0.1"
|
siphasher = "1.0.1"
|
||||||
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "signal"] }
|
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "signal"] }
|
||||||
heck = "0.5.0"
|
heck = "0.5.0"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use heck::{ToSnakeCase,ToTitleCase};
|
|||||||
use rbx_dom_weak::Instance;
|
use rbx_dom_weak::Instance;
|
||||||
use rust_grpc::validator::Check;
|
use rust_grpc::validator::Check;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
ModelInfoDownload(rbx_asset::cloud::GetError),
|
ModelInfoDownload(rbx_asset::cloud::GetError),
|
||||||
@@ -33,29 +33,6 @@ macro_rules! lazy_regex{
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
|
||||||
pub struct CheckRequest{
|
|
||||||
ModelID:u64,
|
|
||||||
SkipChecks:bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::nats_types::CheckMapfixRequest> for CheckRequest{
|
|
||||||
fn from(value:crate::nats_types::CheckMapfixRequest)->Self{
|
|
||||||
Self{
|
|
||||||
ModelID:value.ModelID,
|
|
||||||
SkipChecks:value.SkipChecks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<crate::nats_types::CheckSubmissionRequest> for CheckRequest{
|
|
||||||
fn from(value:crate::nats_types::CheckSubmissionRequest)->Self{
|
|
||||||
Self{
|
|
||||||
ModelID:value.ModelID,
|
|
||||||
SkipChecks:value.SkipChecks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,Ord,PartialOrd)]
|
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,Ord,PartialOrd)]
|
||||||
struct ModeID(u64);
|
struct ModeID(u64);
|
||||||
impl ModeID{
|
impl ModeID{
|
||||||
@@ -79,7 +56,7 @@ struct ModeElement{
|
|||||||
zone:Zone,
|
zone:Zone,
|
||||||
mode_id:ModeID,
|
mode_id:ModeID,
|
||||||
}
|
}
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
pub enum IDParseError{
|
pub enum IDParseError{
|
||||||
NoCaptures,
|
NoCaptures,
|
||||||
ParseInt(core::num::ParseIntError),
|
ParseInt(core::num::ParseIntError),
|
||||||
@@ -323,26 +300,25 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if an observed string matches an expected string
|
// check if an observed value matches an expected value
|
||||||
pub struct StringCheck<'a,T,Str>(Result<T,StringCheckContext<'a,Str>>);
|
pub struct EqualityCheck<Obs,Exp>{
|
||||||
pub struct StringCheckContext<'a,Str>{
|
observed:Obs,
|
||||||
observed:&'a str,
|
expected:Exp,
|
||||||
expected:Str,
|
|
||||||
}
|
}
|
||||||
impl<'a,Str> StringCheckContext<'a,Str>
|
impl<Obs,Exp> EqualityCheck<Obs,Exp>
|
||||||
where
|
where
|
||||||
&'a str:PartialEq<Str>,
|
Obs:PartialEq<Exp>,
|
||||||
{
|
{
|
||||||
/// Compute the StringCheck, passing through the provided value on success.
|
/// Compute the StringCheck, passing through the provided value on success.
|
||||||
fn check<T>(self,value:T)->StringCheck<'a,T,Str>{
|
fn check<T>(self,value:T)->Result<T,Self>{
|
||||||
if self.observed==self.expected{
|
if self.observed==self.expected{
|
||||||
StringCheck(Ok(value))
|
Ok(value)
|
||||||
}else{
|
}else{
|
||||||
StringCheck(Err(self))
|
Err(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<Str:std::fmt::Display> std::fmt::Display for StringCheckContext<'_,Str>{
|
impl<Obs:std::fmt::Display,Exp:std::fmt::Display> std::fmt::Display for EqualityCheck<Obs,Exp>{
|
||||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||||
write!(f,"expected: {}, observed: {}",self.expected,self.observed)
|
write!(f,"expected: {}, observed: {}",self.expected,self.observed)
|
||||||
}
|
}
|
||||||
@@ -442,7 +418,7 @@ pub struct MapInfoOwned{
|
|||||||
pub creator:String,
|
pub creator:String,
|
||||||
pub game_id:GameID,
|
pub game_id:GameID,
|
||||||
}
|
}
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum IntoMapInfoOwnedError{
|
pub enum IntoMapInfoOwnedError{
|
||||||
DisplayName(StringValueError),
|
DisplayName(StringValueError),
|
||||||
@@ -464,22 +440,164 @@ impl TryFrom<MapInfo<'_>> for MapInfoOwned{
|
|||||||
struct Exists;
|
struct Exists;
|
||||||
struct Absent;
|
struct Absent;
|
||||||
|
|
||||||
|
enum DisplayNameError<'a>{
|
||||||
|
TitleCase(EqualityCheck<&'a str,String>),
|
||||||
|
CannotChange(EqualityCheck<&'a str,String>),
|
||||||
|
Empty(StringEmpty),
|
||||||
|
TooLong(usize),
|
||||||
|
StringValue(StringValueError),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CreatorError<'a>{
|
||||||
|
CannotChange(EqualityCheck<&'a str,String>),
|
||||||
|
Empty(StringEmpty),
|
||||||
|
TooLong(usize),
|
||||||
|
StringValue(StringValueError),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GameIDError{
|
||||||
|
CannotChange(EqualityCheck<GameID,GameID>),
|
||||||
|
Parse(ParseGameIDError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CheckedMapInfo<'a>{
|
||||||
|
display_name:Result<&'a str,DisplayNameError<'a>>,
|
||||||
|
creator:Result<&'a str,CreatorError<'a>>,
|
||||||
|
game_id:Result<GameID,GameIDError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NoMapInfo;
|
||||||
|
impl CheckModelInfo for NoMapInfo{
|
||||||
|
fn check<'a>(self,map_info:MapInfo<'a>)->CheckedMapInfo<'a>{
|
||||||
|
fn check_display_name(display_name:Result<&str,StringValueError>)->Result<&str,DisplayNameError<'_>>{
|
||||||
|
// DisplayName StringValue can be missing or whatever
|
||||||
|
let display_name=display_name.map_err(DisplayNameError::StringValue)?;
|
||||||
|
|
||||||
|
// DisplayName cannot be ""
|
||||||
|
let display_name=check_empty(display_name).map_err(DisplayNameError::Empty)?;
|
||||||
|
|
||||||
|
// DisplayName cannot exceed 50 characters
|
||||||
|
if 50<display_name.len(){
|
||||||
|
return Err(DisplayNameError::TooLong(display_name.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check title case
|
||||||
|
let display_name=EqualityCheck{
|
||||||
|
observed:display_name,
|
||||||
|
expected:display_name.to_title_case(),
|
||||||
|
}.check(display_name).map_err(DisplayNameError::TitleCase)?;
|
||||||
|
|
||||||
|
Ok(display_name)
|
||||||
|
}
|
||||||
|
fn check_creator(creator:Result<&str,StringValueError>)->Result<&str,CreatorError<'_>>{
|
||||||
|
// Creator StringValue can be missing or whatever
|
||||||
|
let creator=creator.map_err(CreatorError::StringValue)?;
|
||||||
|
|
||||||
|
// Creator cannot be ""
|
||||||
|
let creator=check_empty(creator).map_err(CreatorError::Empty)?;
|
||||||
|
|
||||||
|
// Creator cannot exceed 50 characters
|
||||||
|
if 50<creator.len(){
|
||||||
|
return Err(CreatorError::TooLong(creator.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(creator)
|
||||||
|
}
|
||||||
|
fn check_game_id(game_id:Result<GameID,ParseGameIDError>)->Result<GameID,GameIDError>{
|
||||||
|
// Creator StringValue can be missing or whatever
|
||||||
|
let game_id=game_id.map_err(GameIDError::Parse)?;
|
||||||
|
|
||||||
|
Ok(game_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check display name is not empty and has title case
|
||||||
|
let display_name=check_display_name(map_info.display_name);
|
||||||
|
|
||||||
|
// Check Creator is not empty
|
||||||
|
let creator=check_creator(map_info.creator);
|
||||||
|
|
||||||
|
// Check GameID (model name was prefixed with bhop_ surf_ etc)
|
||||||
|
let game_id=check_game_id(map_info.game_id);
|
||||||
|
|
||||||
|
CheckedMapInfo{
|
||||||
|
display_name,
|
||||||
|
creator,
|
||||||
|
game_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl CheckModelInfo for MapInfoOwned{
|
||||||
|
fn check<'a>(self,map_info:MapInfo<'a>)->CheckedMapInfo<'a>{
|
||||||
|
fn check_display_name(display_name:Result<&str,StringValueError>,target_display_name:String)->Result<&str,DisplayNameError<'_>>{
|
||||||
|
// DisplayName StringValue can be missing or whatever
|
||||||
|
let display_name=display_name.map_err(DisplayNameError::StringValue)?;
|
||||||
|
|
||||||
|
// Mapfix cannot change display name
|
||||||
|
let display_name=EqualityCheck{
|
||||||
|
observed:display_name,
|
||||||
|
expected:target_display_name,
|
||||||
|
}.check(display_name).map_err(DisplayNameError::CannotChange)?;
|
||||||
|
|
||||||
|
Ok(display_name)
|
||||||
|
}
|
||||||
|
fn check_creator(creator:Result<&str,StringValueError>,target_creator:String)->Result<&str,CreatorError<'_>>{
|
||||||
|
// Creator StringValue can be missing or whatever
|
||||||
|
let creator=creator.map_err(CreatorError::StringValue)?;
|
||||||
|
|
||||||
|
// Mapfix cannot change creator
|
||||||
|
let creator=EqualityCheck{
|
||||||
|
observed:creator,
|
||||||
|
expected:target_creator,
|
||||||
|
}.check(creator).map_err(CreatorError::CannotChange)?;
|
||||||
|
|
||||||
|
Ok(creator)
|
||||||
|
}
|
||||||
|
fn check_game_id(game_id:Result<GameID,ParseGameIDError>,target_game_id:GameID)->Result<GameID,GameIDError>{
|
||||||
|
// Creator StringValue can be missing or whatever
|
||||||
|
let game_id=game_id.map_err(GameIDError::Parse)?;
|
||||||
|
|
||||||
|
// Mapfix cannot change game_id
|
||||||
|
let game_id=EqualityCheck{
|
||||||
|
observed:game_id,
|
||||||
|
expected:target_game_id,
|
||||||
|
}.check(game_id).map_err(GameIDError::CannotChange)?;
|
||||||
|
|
||||||
|
Ok(game_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check display name is not empty and has title case
|
||||||
|
let display_name=check_display_name(map_info.display_name,self.display_name);
|
||||||
|
|
||||||
|
// Check Creator is not empty
|
||||||
|
let creator=check_creator(map_info.creator,self.creator);
|
||||||
|
|
||||||
|
// Check GameID (model name was prefixed with bhop_ surf_ etc)
|
||||||
|
let game_id=check_game_id(map_info.game_id,self.game_id);
|
||||||
|
|
||||||
|
CheckedMapInfo{
|
||||||
|
display_name,
|
||||||
|
creator,
|
||||||
|
game_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The result of every map check.
|
/// The result of every map check.
|
||||||
struct MapCheck<'a>{
|
struct MapCheck<'a>{
|
||||||
// === METADATA CHECKS ===
|
// === METADATA CHECKS ===
|
||||||
// The root must be of class Model
|
// The root must be of class Model
|
||||||
model_class:StringCheck<'a,(),&'static str>,
|
model_class:Result<(),EqualityCheck<&'a str,&'static str>>,
|
||||||
// Model's name must be in snake case
|
// Model's name must be in snake case
|
||||||
model_name:StringCheck<'a,(),String>,
|
model_name:Result<(),EqualityCheck<&'a str,String>>,
|
||||||
// Map must have a StringValue named DisplayName.
|
// Map must have a StringValue named DisplayName.
|
||||||
// Value must not be empty, must be in title case.
|
// Value must not be empty, must be in title case.
|
||||||
display_name:Result<Result<StringCheck<'a,&'a str,String>,StringEmpty>,StringValueError>,
|
display_name:Result<&'a str,DisplayNameError<'a>>,
|
||||||
// Map must have a StringValue named Creator.
|
// Map must have a StringValue named Creator.
|
||||||
// Value must not be empty.
|
// Value must not be empty.
|
||||||
creator:Result<Result<&'a str,StringEmpty>,StringValueError>,
|
creator:Result<&'a str,CreatorError<'a>>,
|
||||||
// The prefix of the model's name must match the game it was submitted for.
|
// The prefix of the model's name must match the game it was submitted for.
|
||||||
// bhop_ for bhop, and surf_ for surf
|
// bhop_ for bhop, and surf_ for surf
|
||||||
game_id:Result<GameID,ParseGameIDError>,
|
game_id:Result<GameID,GameIDError>,
|
||||||
|
|
||||||
// === MODE CHECKS ===
|
// === MODE CHECKS ===
|
||||||
// MapStart must exist
|
// MapStart must exist
|
||||||
@@ -509,32 +627,24 @@ struct MapCheck<'a>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ModelInfo<'a>{
|
impl<'a> ModelInfo<'a>{
|
||||||
fn check(self)->MapCheck<'a>{
|
fn check<I:CheckModelInfo>(self,model_info:I)->MapCheck<'a>{
|
||||||
// Check class is exactly "Model"
|
// Check class is exactly "Model"
|
||||||
let model_class=StringCheckContext{
|
let model_class=EqualityCheck{
|
||||||
observed:self.model_class,
|
observed:self.model_class,
|
||||||
expected:"Model",
|
expected:"Model",
|
||||||
}.check(());
|
}.check(());
|
||||||
|
|
||||||
// Check model name is snake case
|
// Check model name is snake case
|
||||||
let model_name=StringCheckContext{
|
let model_name=EqualityCheck{
|
||||||
observed:self.model_name,
|
observed:self.model_name,
|
||||||
expected:self.model_name.to_snake_case(),
|
expected:self.model_name.to_snake_case(),
|
||||||
}.check(());
|
}.check(());
|
||||||
|
|
||||||
// Check display name is not empty and has title case
|
let CheckedMapInfo{
|
||||||
let display_name=self.map_info.display_name.map(|display_name|{
|
display_name,
|
||||||
check_empty(display_name).map(|display_name|StringCheckContext{
|
creator,
|
||||||
observed:display_name,
|
game_id,
|
||||||
expected:display_name.to_title_case(),
|
}=model_info.check(self.map_info);
|
||||||
}.check(display_name))
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check Creator is not empty
|
|
||||||
let creator=self.map_info.creator.map(check_empty);
|
|
||||||
|
|
||||||
// Check GameID (model name was prefixed with bhop_ surf_ etc)
|
|
||||||
let game_id=self.map_info.game_id;
|
|
||||||
|
|
||||||
// MapStart must exist
|
// MapStart must exist
|
||||||
let mapstart=if self.counts.mode_start_counts.contains_key(&ModeID::MAIN){
|
let mapstart=if self.counts.mode_start_counts.contains_key(&ModeID::MAIN){
|
||||||
@@ -630,10 +740,10 @@ impl MapCheck<'_>{
|
|||||||
fn result(self)->Result<MapInfoOwned,Result<MapCheckList,serde_json::Error>>{
|
fn result(self)->Result<MapInfoOwned,Result<MapCheckList,serde_json::Error>>{
|
||||||
match self{
|
match self{
|
||||||
MapCheck{
|
MapCheck{
|
||||||
model_class:StringCheck(Ok(())),
|
model_class:Ok(()),
|
||||||
model_name:StringCheck(Ok(())),
|
model_name:Ok(()),
|
||||||
display_name:Ok(Ok(StringCheck(Ok(display_name)))),
|
display_name:Ok(display_name),
|
||||||
creator:Ok(Ok(creator)),
|
creator:Ok(creator),
|
||||||
game_id:Ok(game_id),
|
game_id:Ok(game_id),
|
||||||
mapstart:Ok(Exists),
|
mapstart:Ok(Exists),
|
||||||
mode_start_counts:DuplicateCheck(Ok(())),
|
mode_start_counts:DuplicateCheck(Ok(())),
|
||||||
@@ -737,31 +847,32 @@ macro_rules! summary_format{
|
|||||||
impl MapCheck<'_>{
|
impl MapCheck<'_>{
|
||||||
fn itemize(&self)->Result<MapCheckList,serde_json::Error>{
|
fn itemize(&self)->Result<MapCheckList,serde_json::Error>{
|
||||||
let model_class=match &self.model_class{
|
let model_class=match &self.model_class{
|
||||||
StringCheck(Ok(()))=>passed!("ModelClass"),
|
Ok(())=>passed!("ModelClass"),
|
||||||
StringCheck(Err(context))=>summary_format!("ModelClass","Invalid model class: {context}"),
|
Err(context)=>summary_format!("ModelClass","Invalid model class: {context}"),
|
||||||
};
|
};
|
||||||
let model_name=match &self.model_name{
|
let model_name=match &self.model_name{
|
||||||
StringCheck(Ok(()))=>passed!("ModelName"),
|
Ok(())=>passed!("ModelName"),
|
||||||
StringCheck(Err(context))=>summary_format!("ModelName","Model name must have snake_case: {context}"),
|
Err(context)=>summary_format!("ModelName","Model name must have snake_case: {context}"),
|
||||||
};
|
};
|
||||||
let display_name=match &self.display_name{
|
let display_name=match &self.display_name{
|
||||||
Ok(Ok(StringCheck(Ok(_))))=>passed!("DisplayName"),
|
Ok(_)=>passed!("DisplayName"),
|
||||||
Ok(Ok(StringCheck(Err(context))))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"),
|
Err(DisplayNameError::TitleCase(context))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"),
|
||||||
Ok(Err(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"),
|
Err(DisplayNameError::CannotChange(context))=>summary_format!("DisplayName","DisplayName cannot be changed: {context}"),
|
||||||
Err(StringValueError::ObjectNotFound)=>summary!("DisplayName","Missing DisplayName StringValue".to_owned()),
|
Err(DisplayNameError::Empty(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"),
|
||||||
Err(StringValueError::ValueNotSet)=>summary!("DisplayName","DisplayName Value not set".to_owned()),
|
Err(DisplayNameError::TooLong(context))=>summary_format!("DisplayName","DisplayName is too long: {context} characters (50 characters max)"),
|
||||||
Err(StringValueError::NonStringValue)=>summary!("DisplayName","DisplayName Value is not a String".to_owned()),
|
Err(DisplayNameError::StringValue(context))=>summary_format!("DisplayName","DisplayName StringValue: {context}"),
|
||||||
};
|
};
|
||||||
let creator=match &self.creator{
|
let creator=match &self.creator{
|
||||||
Ok(Ok(_))=>passed!("Creator"),
|
Ok(_)=>passed!("Creator"),
|
||||||
Ok(Err(context))=>summary_format!("Creator","Invalid Creator: {context}"),
|
Err(CreatorError::CannotChange(context))=>summary_format!("Creator","Creator cannot be changed: {context}"),
|
||||||
Err(StringValueError::ObjectNotFound)=>summary!("Creator","Missing Creator StringValue".to_owned()),
|
Err(CreatorError::Empty(context))=>summary_format!("Creator","Invalid Creator: {context}"),
|
||||||
Err(StringValueError::ValueNotSet)=>summary!("Creator","Creator Value not set".to_owned()),
|
Err(CreatorError::TooLong(context))=>summary_format!("Creator","Creator is too long: {context} characters (50 characters max)"),
|
||||||
Err(StringValueError::NonStringValue)=>summary!("Creator","Creator Value is not a String".to_owned()),
|
Err(CreatorError::StringValue(context))=>summary_format!("Creator","Creator StringValue: {context}"),
|
||||||
};
|
};
|
||||||
let game_id=match &self.game_id{
|
let game_id=match &self.game_id{
|
||||||
Ok(_)=>passed!("GameID"),
|
Ok(_)=>passed!("GameID"),
|
||||||
Err(ParseGameIDError)=>summary!("GameID","Model name must be prefixed with bhop_ surf_ or flytrials_".to_owned()),
|
Err(GameIDError::CannotChange(context))=>summary_format!("GameID","GameID cannot be changed: {context}"),
|
||||||
|
Err(GameIDError::Parse(ParseGameIDError))=>summary!("GameID","Model name must be prefixed with bhop_ surf_ or flytrials_".to_owned()),
|
||||||
};
|
};
|
||||||
let mapstart=match &self.mapstart{
|
let mapstart=match &self.mapstart{
|
||||||
Ok(Exists)=>passed!("MapStart"),
|
Ok(Exists)=>passed!("MapStart"),
|
||||||
@@ -922,8 +1033,55 @@ pub struct CheckListAndVersion{
|
|||||||
pub version:u64,
|
pub version:u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait CheckModelInfo{
|
||||||
|
fn check<'a>(self,map_info:MapInfo<'a>)->CheckedMapInfo<'a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CheckSpecialization{
|
||||||
|
type ModelInfo:CheckModelInfo;
|
||||||
|
fn info(self)->(CheckRequest,Self::ModelInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[expect(nonstandard_style)]
|
||||||
|
pub struct CheckRequest{
|
||||||
|
ModelID:u64,
|
||||||
|
SkipChecks:bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CheckSpecialization for crate::nats_types::CheckMapfixRequest{
|
||||||
|
type ModelInfo=MapInfoOwned;
|
||||||
|
fn info(self)->(CheckRequest,Self::ModelInfo) {
|
||||||
|
(
|
||||||
|
CheckRequest{
|
||||||
|
ModelID:self.ModelID,
|
||||||
|
SkipChecks:self.SkipChecks,
|
||||||
|
},
|
||||||
|
MapInfoOwned{
|
||||||
|
display_name:self.DisplayName,
|
||||||
|
creator:self.Creator,
|
||||||
|
game_id:self.GameID,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl CheckSpecialization for crate::nats_types::CheckSubmissionRequest{
|
||||||
|
type ModelInfo=NoMapInfo;
|
||||||
|
fn info(self)->(CheckRequest,Self::ModelInfo) {
|
||||||
|
(
|
||||||
|
CheckRequest{
|
||||||
|
ModelID:self.ModelID,
|
||||||
|
SkipChecks:self.SkipChecks,
|
||||||
|
},
|
||||||
|
NoMapInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl crate::message_handler::MessageHandler{
|
impl crate::message_handler::MessageHandler{
|
||||||
pub async fn check_inner(&self,check_info:CheckRequest)->Result<CheckListAndVersion,Error>{
|
pub async fn check_inner<R:CheckSpecialization>(&self,check_info:R)->Result<CheckListAndVersion,Error>{
|
||||||
|
let (check_info,target_model_info)=check_info.info();
|
||||||
|
|
||||||
// discover asset creator and latest version
|
// discover asset creator and latest version
|
||||||
let info=self.cloud_context.get_asset_info(
|
let info=self.cloud_context.get_asset_info(
|
||||||
rbx_asset::cloud::GetAssetLatestRequest{asset_id:check_info.ModelID}
|
rbx_asset::cloud::GetAssetLatestRequest{asset_id:check_info.ModelID}
|
||||||
@@ -963,7 +1121,7 @@ impl crate::message_handler::MessageHandler{
|
|||||||
let model_info=get_model_info(&dom,model_instance);
|
let model_info=get_model_info(&dom,model_instance);
|
||||||
|
|
||||||
// convert the model information into a structured report
|
// convert the model information into a structured report
|
||||||
let map_check=model_info.check();
|
let map_check=model_info.check(target_model_info);
|
||||||
|
|
||||||
// check the report, generate an error message if it fails the check
|
// check the report, generate an error message if it fails the check
|
||||||
let status=match map_check.result(){
|
let status=match map_check.result(){
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::check::CheckListAndVersion;
|
use crate::check::CheckListAndVersion;
|
||||||
use crate::nats_types::CheckMapfixRequest;
|
use crate::nats_types::CheckMapfixRequest;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
Check(crate::check::Error),
|
Check(crate::check::Error),
|
||||||
@@ -17,7 +17,7 @@ impl std::error::Error for Error{}
|
|||||||
impl crate::message_handler::MessageHandler{
|
impl crate::message_handler::MessageHandler{
|
||||||
pub async fn check_mapfix(&self,check_info:CheckMapfixRequest)->Result<(),Error>{
|
pub async fn check_mapfix(&self,check_info:CheckMapfixRequest)->Result<(),Error>{
|
||||||
let mapfix_id=check_info.MapfixID;
|
let mapfix_id=check_info.MapfixID;
|
||||||
let check_result=self.check_inner(check_info.into()).await;
|
let check_result=self.check_inner(check_info).await;
|
||||||
|
|
||||||
// update the mapfix depending on the result
|
// update the mapfix depending on the result
|
||||||
match check_result{
|
match check_result{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::check::CheckListAndVersion;
|
use crate::check::CheckListAndVersion;
|
||||||
use crate::nats_types::CheckSubmissionRequest;
|
use crate::nats_types::CheckSubmissionRequest;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
Check(crate::check::Error),
|
Check(crate::check::Error),
|
||||||
@@ -17,7 +17,7 @@ impl std::error::Error for Error{}
|
|||||||
impl crate::message_handler::MessageHandler{
|
impl crate::message_handler::MessageHandler{
|
||||||
pub async fn check_submission(&self,check_info:CheckSubmissionRequest)->Result<(),Error>{
|
pub async fn check_submission(&self,check_info:CheckSubmissionRequest)->Result<(),Error>{
|
||||||
let submission_id=check_info.SubmissionID;
|
let submission_id=check_info.SubmissionID;
|
||||||
let check_result=self.check_inner(check_info.into()).await;
|
let check_result=self.check_inner(check_info).await;
|
||||||
|
|
||||||
// update the submission depending on the result
|
// update the submission depending on the result
|
||||||
match check_result{
|
match check_result{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::download::download_asset_version;
|
use crate::download::download_asset_version;
|
||||||
use crate::rbx_util::{get_root_instance,get_mapinfo,read_dom,MapInfo,ReadDomError,GetRootInstanceError,GameID};
|
use crate::rbx_util::{get_root_instance,get_mapinfo,read_dom,MapInfo,ReadDomError,GetRootInstanceError,GameID};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
CreatorTypeMustBeUser,
|
CreatorTypeMustBeUser,
|
||||||
@@ -17,11 +17,11 @@ impl std::fmt::Display for Error{
|
|||||||
}
|
}
|
||||||
impl std::error::Error for Error{}
|
impl std::error::Error for Error{}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
pub struct CreateRequest{
|
pub struct CreateRequest{
|
||||||
pub ModelID:u64,
|
pub ModelID:u64,
|
||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
pub struct CreateResult{
|
pub struct CreateResult{
|
||||||
pub AssetOwner:u64,
|
pub AssetOwner:u64,
|
||||||
pub DisplayName:Option<String>,
|
pub DisplayName:Option<String>,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::nats_types::CreateMapfixRequest;
|
use crate::nats_types::CreateMapfixRequest;
|
||||||
use crate::create::CreateRequest;
|
use crate::create::CreateRequest;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
Create(crate::create::Error),
|
Create(crate::create::Error),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::nats_types::CreateSubmissionRequest;
|
|||||||
use crate::create::CreateRequest;
|
use crate::create::CreateRequest;
|
||||||
use crate::rbx_util::GameID;
|
use crate::rbx_util::GameID;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
Create(crate::create::Error),
|
Create(crate::create::Error),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
ModelLocationDownload(rbx_asset::cloud::GetError),
|
ModelLocationDownload(rbx_asset::cloud::GetError),
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ mod validator;
|
|||||||
mod validate_mapfix;
|
mod validate_mapfix;
|
||||||
mod validate_submission;
|
mod validate_submission;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum StartupError{
|
pub enum StartupError{
|
||||||
API(tonic::transport::Error),
|
API(tonic::transport::Error),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum HandleMessageError{
|
pub enum HandleMessageError{
|
||||||
Messages(async_nats::jetstream::consumer::pull::MessagesError),
|
Messages(async_nats::jetstream::consumer::pull::MessagesError),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// Requests are sent from maps-service to validator
|
// Requests are sent from maps-service to validator
|
||||||
// Validation invokes the REST api to update the submissions
|
// Validation invokes the REST api to update the submissions
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct CreateSubmissionRequest{
|
pub struct CreateSubmissionRequest{
|
||||||
// operation_id is passed back in the response message
|
// operation_id is passed back in the response message
|
||||||
@@ -18,7 +18,7 @@ pub struct CreateSubmissionRequest{
|
|||||||
pub Roles:u32,
|
pub Roles:u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct CreateMapfixRequest{
|
pub struct CreateMapfixRequest{
|
||||||
pub OperationID:u32,
|
pub OperationID:u32,
|
||||||
@@ -27,7 +27,7 @@ pub struct CreateMapfixRequest{
|
|||||||
pub Description:String,
|
pub Description:String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct CheckSubmissionRequest{
|
pub struct CheckSubmissionRequest{
|
||||||
pub SubmissionID:u64,
|
pub SubmissionID:u64,
|
||||||
@@ -35,15 +35,19 @@ pub struct CheckSubmissionRequest{
|
|||||||
pub SkipChecks:bool,
|
pub SkipChecks:bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct CheckMapfixRequest{
|
pub struct CheckMapfixRequest{
|
||||||
pub MapfixID:u64,
|
pub MapfixID:u64,
|
||||||
pub ModelID:u64,
|
pub ModelID:u64,
|
||||||
pub SkipChecks:bool,
|
pub SkipChecks:bool,
|
||||||
|
// target map info
|
||||||
|
pub DisplayName:String,
|
||||||
|
pub Creator:String,
|
||||||
|
pub GameID:crate::rbx_util::GameID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct ValidateSubmissionRequest{
|
pub struct ValidateSubmissionRequest{
|
||||||
// submission_id is passed back in the response message
|
// submission_id is passed back in the response message
|
||||||
@@ -53,7 +57,7 @@ pub struct ValidateSubmissionRequest{
|
|||||||
pub ValidatedModelID:Option<u64>,
|
pub ValidatedModelID:Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct ValidateMapfixRequest{
|
pub struct ValidateMapfixRequest{
|
||||||
// submission_id is passed back in the response message
|
// submission_id is passed back in the response message
|
||||||
@@ -64,7 +68,7 @@ pub struct ValidateMapfixRequest{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new map
|
// Create a new map
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct UploadSubmissionRequest{
|
pub struct UploadSubmissionRequest{
|
||||||
pub SubmissionID:u64,
|
pub SubmissionID:u64,
|
||||||
@@ -73,7 +77,7 @@ pub struct UploadSubmissionRequest{
|
|||||||
pub ModelName:String,
|
pub ModelName:String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct UploadMapfixRequest{
|
pub struct UploadMapfixRequest{
|
||||||
pub MapfixID:u64,
|
pub MapfixID:u64,
|
||||||
@@ -83,7 +87,7 @@ pub struct UploadMapfixRequest{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Release a new map
|
// Release a new map
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct ReleaseSubmissionRequest{
|
pub struct ReleaseSubmissionRequest{
|
||||||
pub SubmissionID:u64,
|
pub SubmissionID:u64,
|
||||||
@@ -97,14 +101,14 @@ pub struct ReleaseSubmissionRequest{
|
|||||||
pub Submitter:u64,
|
pub Submitter:u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct ReleaseSubmissionsBatchRequest{
|
pub struct ReleaseSubmissionsBatchRequest{
|
||||||
pub Submissions:Vec<ReleaseSubmissionRequest>,
|
pub Submissions:Vec<ReleaseSubmissionRequest>,
|
||||||
pub OperationID:u32,
|
pub OperationID:u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct ReleaseMapfixRequest{
|
pub struct ReleaseMapfixRequest{
|
||||||
pub MapfixID:u64,
|
pub MapfixID:u64,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ReadDomError{
|
pub enum ReadDomError{
|
||||||
Binary(rbx_binary::DecodeError),
|
Binary(rbx_binary::DecodeError),
|
||||||
@@ -31,6 +31,9 @@ fn find_first_child_name_and_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&r
|
|||||||
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.name==name&&inst.class==class)
|
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.name==name&&inst.class==class)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde_repr::Deserialize_repr)]
|
||||||
|
#[repr(u32)]
|
||||||
|
#[derive(Clone,Copy,PartialEq)]
|
||||||
pub enum GameID{
|
pub enum GameID{
|
||||||
Bhop=1,
|
Bhop=1,
|
||||||
Surf=2,
|
Surf=2,
|
||||||
@@ -66,6 +69,15 @@ impl TryFrom<u32> for GameID{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl std::fmt::Display for GameID{
|
||||||
|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||||
|
match self{
|
||||||
|
GameID::Bhop=>write!(f,"Bhop"),
|
||||||
|
GameID::Surf=>write!(f,"Surf"),
|
||||||
|
GameID::FlyTrials=>write!(f,"FlyTrials"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct MapInfo<'a>{
|
pub struct MapInfo<'a>{
|
||||||
pub display_name:Result<&'a str,StringValueError>,
|
pub display_name:Result<&'a str,StringValueError>,
|
||||||
@@ -79,6 +91,15 @@ pub enum StringValueError{
|
|||||||
ValueNotSet,
|
ValueNotSet,
|
||||||
NonStringValue,
|
NonStringValue,
|
||||||
}
|
}
|
||||||
|
impl std::fmt::Display for StringValueError{
|
||||||
|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||||
|
match self{
|
||||||
|
StringValueError::ObjectNotFound=>write!(f,"Missing StringValue"),
|
||||||
|
StringValueError::ValueNotSet=>write!(f,"Value not set"),
|
||||||
|
StringValueError::NonStringValue=>write!(f,"Value is not a String"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn string_value(instance:Option<&rbx_dom_weak::Instance>)->Result<&str,StringValueError>{
|
fn string_value(instance:Option<&rbx_dom_weak::Instance>)->Result<&str,StringValueError>{
|
||||||
let instance=instance.ok_or(StringValueError::ObjectNotFound)?;
|
let instance=instance.ok_or(StringValueError::ObjectNotFound)?;
|
||||||
|
|||||||
@@ -75,25 +75,27 @@ async fn release_inner(
|
|||||||
cloud_context:&rbx_asset::cloud::Context,
|
cloud_context:&rbx_asset::cloud::Context,
|
||||||
cloud_context_luau_execution:&rbx_asset::cloud::Context,
|
cloud_context_luau_execution:&rbx_asset::cloud::Context,
|
||||||
load_asset_version_runtime:&rbx_asset::cloud::LuauSessionLatestRequest,
|
load_asset_version_runtime:&rbx_asset::cloud::LuauSessionLatestRequest,
|
||||||
submissions_service:&crate::grpc::submissions::Service,
|
submissions:&crate::grpc::submissions::Service,
|
||||||
)->Result<(),InnerError>{
|
)->Result<(),InnerError>{
|
||||||
let available_parallelism=std::thread::available_parallelism().map_err(InnerError::Io)?.get();
|
let available_parallelism=std::thread::available_parallelism().map_err(InnerError::Io)?.get();
|
||||||
// set up futures
|
// set up futures
|
||||||
|
|
||||||
let submissions=&release_info.Submissions;
|
// unnecessary allocation :(
|
||||||
|
let asset_versions:Vec<_> =release_info
|
||||||
|
.Submissions
|
||||||
|
.iter()
|
||||||
|
.map(|submission|rbx_asset::cloud::GetAssetVersionRequest{
|
||||||
|
asset_id:submission.ModelID,
|
||||||
|
version:submission.ModelVersion,
|
||||||
|
})
|
||||||
|
.enumerate()
|
||||||
|
.collect();
|
||||||
|
|
||||||
// fut_download
|
// fut_download
|
||||||
let fut_download=futures::stream::iter(submissions)
|
let fut_download=futures::stream::iter(asset_versions)
|
||||||
.enumerate()
|
.map(|(index,asset_version)|async move{
|
||||||
.map(|(index,submission)|{
|
let modes=download_fut(cloud_context,asset_version).await;
|
||||||
let asset_version=rbx_asset::cloud::GetAssetVersionRequest{
|
(index,modes)
|
||||||
asset_id:submission.ModelID,
|
|
||||||
version:submission.ModelVersion,
|
|
||||||
};
|
|
||||||
async move{
|
|
||||||
let modes=download_fut(cloud_context,asset_version).await;
|
|
||||||
(index,modes)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.buffer_unordered(available_parallelism.min(MAX_PARALLEL_DECODE))
|
.buffer_unordered(available_parallelism.min(MAX_PARALLEL_DECODE))
|
||||||
.collect::<Vec<(usize,Result<_,DownloadFutError>)>>();
|
.collect::<Vec<(usize,Result<_,DownloadFutError>)>>();
|
||||||
@@ -102,7 +104,7 @@ async fn release_inner(
|
|||||||
let fut_load_asset_versions=load_asset_versions(
|
let fut_load_asset_versions=load_asset_versions(
|
||||||
cloud_context_luau_execution,
|
cloud_context_luau_execution,
|
||||||
load_asset_version_runtime,
|
load_asset_version_runtime,
|
||||||
submissions.iter().map(|submission|submission.UploadedAssetID),
|
release_info.Submissions.iter().map(|submission|submission.UploadedAssetID),
|
||||||
);
|
);
|
||||||
|
|
||||||
// execute futures
|
// execute futures
|
||||||
@@ -111,7 +113,7 @@ async fn release_inner(
|
|||||||
let load_asset_versions=load_asset_versions_result.map_err(InnerError::LoadAssetVersions)?;
|
let load_asset_versions=load_asset_versions_result.map_err(InnerError::LoadAssetVersions)?;
|
||||||
|
|
||||||
// sanity check roblox output
|
// sanity check roblox output
|
||||||
if load_asset_versions.len()!=submissions.len(){
|
if load_asset_versions.len()!=release_info.Submissions.len(){
|
||||||
return Err(InnerError::LoadAssetVersionsListLength);
|
return Err(InnerError::LoadAssetVersionsListLength);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,7 +127,7 @@ async fn release_inner(
|
|||||||
match result{
|
match result{
|
||||||
Ok(value)=>modes.push(value),
|
Ok(value)=>modes.push(value),
|
||||||
Err(error)=>errors.push(ErrorContext{
|
Err(error)=>errors.push(ErrorContext{
|
||||||
submission_id:submissions[index].SubmissionID,
|
submission_id:release_info.Submissions[index].SubmissionID,
|
||||||
error:error,
|
error:error,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -142,7 +144,7 @@ async fn release_inner(
|
|||||||
.zip(modes)
|
.zip(modes)
|
||||||
.zip(load_asset_versions)
|
.zip(load_asset_versions)
|
||||||
.map(|((submission,modes),asset_version)|async move{
|
.map(|((submission,modes),asset_version)|async move{
|
||||||
let result=submissions_service.set_status_released(rust_grpc::validator::SubmissionReleaseRequest{
|
let result=submissions.set_status_released(rust_grpc::validator::SubmissionReleaseRequest{
|
||||||
submission_id:submission.SubmissionID,
|
submission_id:submission.SubmissionID,
|
||||||
map_create:Some(rust_grpc::maps_extended::MapCreate{
|
map_create:Some(rust_grpc::maps_extended::MapCreate{
|
||||||
id:submission.UploadedAssetID as i64,
|
id:submission.UploadedAssetID as i64,
|
||||||
@@ -181,7 +183,7 @@ async fn release_inner(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
UpdateOperation(tonic::Status),
|
UpdateOperation(tonic::Status),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::download::download_asset_version;
|
use crate::download::download_asset_version;
|
||||||
use crate::nats_types::UploadMapfixRequest;
|
use crate::nats_types::UploadMapfixRequest;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum InnerError{
|
pub enum InnerError{
|
||||||
Download(crate::download::Error),
|
Download(crate::download::Error),
|
||||||
@@ -43,7 +43,7 @@ async fn upload_inner(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
ApiActionMapfixUploaded(tonic::Status),
|
ApiActionMapfixUploaded(tonic::Status),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::download::download_asset_version;
|
use crate::download::download_asset_version;
|
||||||
use crate::nats_types::UploadSubmissionRequest;
|
use crate::nats_types::UploadSubmissionRequest;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum InnerError{
|
pub enum InnerError{
|
||||||
Download(crate::download::Error),
|
Download(crate::download::Error),
|
||||||
@@ -44,7 +44,7 @@ async fn upload_inner(
|
|||||||
Ok(upload_response.AssetId)
|
Ok(upload_response.AssetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
ApiActionSubmissionUploaded(tonic::Status),
|
ApiActionSubmissionUploaded(tonic::Status),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::nats_types::ValidateMapfixRequest;
|
use crate::nats_types::ValidateMapfixRequest;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
ApiActionMapfixValidate(tonic::Status),
|
ApiActionMapfixValidate(tonic::Status),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::nats_types::ValidateSubmissionRequest;
|
use crate::nats_types::ValidateSubmissionRequest;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
ApiActionSubmissionValidate(tonic::Status),
|
ApiActionSubmissionValidate(tonic::Status),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ fn hash_source(source:&str)->u64{
|
|||||||
std::hash::Hasher::finish(&hasher)
|
std::hash::Hasher::finish(&hasher)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[expect(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error{
|
pub enum Error{
|
||||||
ModelInfoDownload(rbx_asset::cloud::GetError),
|
ModelInfoDownload(rbx_asset::cloud::GetError),
|
||||||
@@ -52,7 +52,7 @@ impl std::fmt::Display for Error{
|
|||||||
}
|
}
|
||||||
impl std::error::Error for Error{}
|
impl std::error::Error for Error{}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[expect(nonstandard_style)]
|
||||||
pub struct ValidateRequest{
|
pub struct ValidateRequest{
|
||||||
pub ModelID:u64,
|
pub ModelID:u64,
|
||||||
pub ModelVersion:u64,
|
pub ModelVersion:u64,
|
||||||
|
|||||||
34
web/.gitignore
vendored
34
web/.gitignore
vendored
@@ -1,24 +1,12 @@
|
|||||||
bun.lockb
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
|
||||||
.pnp.*
|
|
||||||
.yarn/*
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
/dist
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -29,12 +17,22 @@ npm-debug.log*
|
|||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files
|
||||||
.env*
|
.env*
|
||||||
|
.env.local
|
||||||
# vercel
|
.env.development.local
|
||||||
.vercel
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
|
||||||
|
# editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
FROM registry.itzana.me/docker-proxy/oven/bun:1.3.3
|
# Build stage
|
||||||
|
FROM registry.itzana.me/docker-proxy/oven/bun:1.3.3 AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json bun.lockb* ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
# Release
|
||||||
|
FROM registry.itzana.me/docker-proxy/nginx:alpine
|
||||||
|
|
||||||
|
COPY --from=builder /app/build /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Add nginx configuration for SPA routing
|
||||||
|
RUN echo 'server { \
|
||||||
|
listen 3000; \
|
||||||
|
location / { \
|
||||||
|
root /usr/share/nginx/html; \
|
||||||
|
index index.html; \
|
||||||
|
try_files $uri $uri/ /index.html; \
|
||||||
|
} \
|
||||||
|
}' > /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
||||||
RUN bun install
|
|
||||||
RUN bun run build
|
|
||||||
ENTRYPOINT ["bun", "run", "start"]
|
|
||||||
|
|||||||
677
web/bun.lock
677
web/bun.lock
File diff suppressed because it is too large
Load Diff
13
web/index.html
Normal file
13
web/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Maps Service</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import type { NextConfig } from "next";
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
|
||||||
distDir: "build",
|
|
||||||
output: "standalone",
|
|
||||||
images: {
|
|
||||||
remotePatterns: [
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "**.rbxcdn.com",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
4142
web/package-lock.json
generated
Normal file
4142
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,21 +2,24 @@
|
|||||||
"name": "map-service-web",
|
"name": "map-service-web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3000 --turbopack",
|
"dev": "vite",
|
||||||
"build": "next build",
|
"build": "tsc && vite build",
|
||||||
"start": "next start -p 3000",
|
"preview": "vite preview",
|
||||||
"lint": "next lint"
|
"lint": "eslint src --ext ts,tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@mui/icons-material": "^7.3.6",
|
"@mui/icons-material": "^7.3.6",
|
||||||
"@mui/material": "^7.3.6",
|
"@mui/material": "^7.3.6",
|
||||||
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"next": "^16.0.7",
|
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
|
"react-router-dom": "^7.1.3",
|
||||||
"sass": "^1.94.2"
|
"sass": "^1.94.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -24,8 +27,9 @@
|
|||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-config-next": "16.0.7",
|
"typescript": "^5.9.3",
|
||||||
"typescript": "^5.9.3"
|
"vite": "^6.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
46
web/src/App.tsx
Normal file
46
web/src/App.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Routes, Route } from 'react-router-dom'
|
||||||
|
import { ThemeProvider } from '@mui/material'
|
||||||
|
import { theme } from '@/app/lib/theme'
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
import Home from '@/app/page'
|
||||||
|
import MapsPage from '@/app/maps/page'
|
||||||
|
import MapDetailPage from '@/app/maps/[mapId]/page'
|
||||||
|
import MapFixCreatePage from '@/app/maps/[mapId]/fix/page'
|
||||||
|
import MapfixesPage from '@/app/mapfixes/page'
|
||||||
|
import MapfixDetailPage from '@/app/mapfixes/[mapfixId]/page'
|
||||||
|
import SubmissionsPage from '@/app/submissions/page'
|
||||||
|
import SubmissionDetailPage from '@/app/submissions/[submissionId]/page'
|
||||||
|
import SubmitPage from '@/app/submit/page'
|
||||||
|
import AdminSubmitPage from '@/app/admin-submit/page'
|
||||||
|
import OperationPage from '@/app/operations/[operationId]/page'
|
||||||
|
import ReviewerDashboardPage from '@/app/reviewer-dashboard/page'
|
||||||
|
import UserDashboardPage from '@/app/user-dashboard/page'
|
||||||
|
import ScriptReviewPage from '@/app/script-review/page'
|
||||||
|
import NotFound from '@/app/not-found/page'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="/maps" element={<MapsPage />} />
|
||||||
|
<Route path="/maps/:mapId" element={<MapDetailPage />} />
|
||||||
|
<Route path="/maps/:mapId/fix" element={<MapFixCreatePage />} />
|
||||||
|
<Route path="/mapfixes" element={<MapfixesPage />} />
|
||||||
|
<Route path="/mapfixes/:mapfixId" element={<MapfixDetailPage />} />
|
||||||
|
<Route path="/submissions" element={<SubmissionsPage />} />
|
||||||
|
<Route path="/submissions/:submissionId" element={<SubmissionDetailPage />} />
|
||||||
|
<Route path="/submit" element={<SubmitPage />} />
|
||||||
|
<Route path="/admin-submit" element={<AdminSubmitPage />} />
|
||||||
|
<Route path="/operations/:operationId" element={<OperationPage />} />
|
||||||
|
<Route path="/review" element={<ReviewerDashboardPage />} />
|
||||||
|
<Route path="/dashboard" element={<UserDashboardPage />} />
|
||||||
|
<Route path="/script-review" element={<ScriptReviewPage />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Box, IconButton, Typography} from "@mui/material";
|
import {Box, Button, IconButton, Typography} from "@mui/material";
|
||||||
import {useEffect, useRef, useState} from "react";
|
import {useEffect, useRef, useState} from "react";
|
||||||
import Link from "next/link";
|
import { Link } from "react-router-dom";
|
||||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
||||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
||||||
import {SubmissionInfo} from "@/app/ts/Submission";
|
import {SubmissionInfo} from "@/app/ts/Submission";
|
||||||
@@ -65,14 +65,22 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box mb={6}>
|
<Box mb={6}>
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||||
<Typography variant="h4" component="h2" fontWeight="bold">
|
<Typography variant="h4" component="h2" fontWeight="bold">
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Link href={viewAllLink} style={{textDecoration: 'none'}}>
|
<Link to={viewAllLink} style={{textDecoration: 'none'}}>
|
||||||
<Typography component="span" color="primary">
|
<Button
|
||||||
View All →
|
endIcon={<ArrowForwardIosIcon sx={{ fontSize: '0.875rem' }} />}
|
||||||
</Typography>
|
sx={{
|
||||||
|
color: 'primary.main',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -85,9 +93,12 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
|||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
backgroundColor: 'background.paper',
|
backgroundColor: 'background.paper',
|
||||||
boxShadow: 2,
|
border: '1px solid rgba(99, 102, 241, 0.2)',
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: 'action.hover',
|
backgroundColor: 'background.paper',
|
||||||
|
borderColor: 'rgba(99, 102, 241, 0.4)',
|
||||||
|
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
|
||||||
},
|
},
|
||||||
visibility: scrollPosition <= 5 ? 'hidden' : 'visible',
|
visibility: scrollPosition <= 5 ? 'hidden' : 'visible',
|
||||||
}}
|
}}
|
||||||
@@ -106,7 +117,7 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
|||||||
'&::-webkit-scrollbar': {
|
'&::-webkit-scrollbar': {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
},
|
},
|
||||||
gap: '16px', // Fixed 16px gap - using string with px unit to ensure it's absolute
|
gap: '20px',
|
||||||
padding: '8px 4px',
|
padding: '8px 4px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -116,7 +127,7 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
|||||||
sx={{
|
sx={{
|
||||||
flex: '0 0 auto',
|
flex: '0 0 auto',
|
||||||
width: {
|
width: {
|
||||||
xs: '260px', // Fixed width at different breakpoints
|
xs: '260px',
|
||||||
sm: '280px',
|
sm: '280px',
|
||||||
md: '300px'
|
md: '300px'
|
||||||
}
|
}
|
||||||
@@ -135,9 +146,12 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
|||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
backgroundColor: 'background.paper',
|
backgroundColor: 'background.paper',
|
||||||
boxShadow: 2,
|
border: '1px solid rgba(99, 102, 241, 0.2)',
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: 'action.hover',
|
backgroundColor: 'background.paper',
|
||||||
|
borderColor: 'rgba(99, 102, 241, 0.4)',
|
||||||
|
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
|
||||||
},
|
},
|
||||||
visibility: scrollPosition >= maxScroll - 5 ? 'hidden' : 'visible',
|
visibility: scrollPosition >= maxScroll - 5 ? 'hidden' : 'visible',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Avatar,
|
Avatar,
|
||||||
Typography,
|
Typography,
|
||||||
Tooltip
|
Tooltip,
|
||||||
|
Skeleton
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import PersonIcon from '@mui/icons-material/Person';
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
import { formatDistanceToNow, format } from "date-fns";
|
import { formatDistanceToNow, format } from "date-fns";
|
||||||
import { AuditEvent, decodeAuditEvent as auditEventMessage } from "@/app/ts/AuditEvent";
|
import { AuditEvent, decodeAuditEvent as auditEventMessage } from "@/app/ts/AuditEvent";
|
||||||
|
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
|
||||||
|
|
||||||
interface AuditEventItemProps {
|
interface AuditEventItemProps {
|
||||||
event: AuditEvent;
|
event: AuditEvent;
|
||||||
@@ -15,17 +16,44 @@ interface AuditEventItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AuditEventItem({ event, validatorUser }: AuditEventItemProps) {
|
export default function AuditEventItem({ event, validatorUser }: AuditEventItemProps) {
|
||||||
|
const isValidator = event.User === validatorUser;
|
||||||
|
const { thumbnailUrl, isLoading } = useUserThumbnail(isValidator ? undefined : event.User, '150x150');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
<Box sx={{
|
||||||
<Avatar
|
display: 'flex',
|
||||||
src={event.User === validatorUser ? undefined : `/thumbnails/user/${event.User}`}
|
gap: 2,
|
||||||
>
|
p: 2,
|
||||||
<PersonIcon />
|
borderRadius: 1
|
||||||
</Avatar>
|
}}>
|
||||||
|
<Box sx={{ position: 'relative', width: 40, height: 40 }}>
|
||||||
|
<Skeleton
|
||||||
|
variant="circular"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
opacity: isLoading ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
animation="wave"
|
||||||
|
/>
|
||||||
|
<Avatar
|
||||||
|
src={isValidator ? undefined : (thumbnailUrl || undefined)}
|
||||||
|
sx={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
opacity: isLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PersonIcon />
|
||||||
|
</Avatar>
|
||||||
|
</Box>
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Typography variant="subtitle2">
|
<Typography variant="subtitle2">
|
||||||
{event.User === validatorUser ? "Validator" : event.Username || "Unknown"}
|
{isValidator ? "Validator" : event.Username || "Unknown"}
|
||||||
</Typography>
|
</Typography>
|
||||||
<DateDisplay date={event.Date} />
|
<DateDisplay date={event.Date} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -22,18 +21,21 @@ export default function AuditEventsTabPanel({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box role="tabpanel" hidden={activeTab !== 1}>
|
<Box
|
||||||
{activeTab === 1 && (
|
role="tabpanel"
|
||||||
<Stack spacing={2}>
|
sx={{
|
||||||
{filteredEvents.map((event, index) => (
|
display: activeTab === 1 ? 'block' : 'none'
|
||||||
<AuditEventItem
|
}}
|
||||||
key={index}
|
>
|
||||||
event={event}
|
<Stack spacing={2}>
|
||||||
validatorUser={validatorUser}
|
{filteredEvents.map((event, index) => (
|
||||||
/>
|
<AuditEventItem
|
||||||
))}
|
key={index}
|
||||||
</Stack>
|
event={event}
|
||||||
)}
|
validatorUser={validatorUser}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Avatar,
|
Avatar,
|
||||||
Typography,
|
Typography,
|
||||||
Tooltip
|
Tooltip,
|
||||||
|
Skeleton
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import PersonIcon from '@mui/icons-material/Person';
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
import { formatDistanceToNow, format } from "date-fns";
|
import { formatDistanceToNow, format } from "date-fns";
|
||||||
import { AuditEvent, decodeAuditEvent } from "@/app/ts/AuditEvent";
|
import { AuditEvent, decodeAuditEvent } from "@/app/ts/AuditEvent";
|
||||||
|
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
|
||||||
|
|
||||||
interface CommentItemProps {
|
interface CommentItemProps {
|
||||||
event: AuditEvent;
|
event: AuditEvent;
|
||||||
@@ -15,21 +16,43 @@ interface CommentItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CommentItem({ event, validatorUser }: CommentItemProps) {
|
export default function CommentItem({ event, validatorUser }: CommentItemProps) {
|
||||||
|
const isValidator = event.User === validatorUser;
|
||||||
|
const { thumbnailUrl, isLoading } = useUserThumbnail(isValidator ? undefined : event.User, '150x150');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
<Avatar
|
<Box sx={{ position: 'relative', width: 40, height: 40 }}>
|
||||||
src={event.User === validatorUser ? undefined : `/thumbnails/user/${event.User}`}
|
<Skeleton
|
||||||
>
|
variant="circular"
|
||||||
<PersonIcon />
|
sx={{
|
||||||
</Avatar>
|
position: 'absolute',
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
opacity: isLoading ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
animation="wave"
|
||||||
|
/>
|
||||||
|
<Avatar
|
||||||
|
src={isValidator ? undefined : (thumbnailUrl || undefined)}
|
||||||
|
sx={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
opacity: isLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PersonIcon />
|
||||||
|
</Avatar>
|
||||||
|
</Box>
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Typography variant="subtitle2">
|
<Typography variant="subtitle2">
|
||||||
{event.User === validatorUser ? "Validator" : event.Username || "Unknown"}
|
{isValidator ? "Validator" : event.Username || "Unknown"}
|
||||||
</Typography>
|
</Typography>
|
||||||
<DateDisplay date={event.Date} />
|
<DateDisplay date={event.Date} />
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="body2">{decodeAuditEvent(event)}</Typography>
|
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>{decodeAuditEvent(event)}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,10 +4,22 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
|
keyframes
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import CommentsTabPanel from './CommentsTabPanel';
|
import CommentsTabPanel from './CommentsTabPanel';
|
||||||
import AuditEventsTabPanel from './AuditEventsTabPanel';
|
import AuditEventsTabPanel from './AuditEventsTabPanel';
|
||||||
import { AuditEvent } from "@/app/ts/AuditEvent";
|
import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent";
|
||||||
|
|
||||||
|
const pulse = keyframes`
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
interface CommentsAndAuditSectionProps {
|
interface CommentsAndAuditSectionProps {
|
||||||
auditEvents: AuditEvent[];
|
auditEvents: AuditEvent[];
|
||||||
@@ -16,6 +28,7 @@ interface CommentsAndAuditSectionProps {
|
|||||||
handleCommentSubmit: () => void;
|
handleCommentSubmit: () => void;
|
||||||
validatorUser: number;
|
validatorUser: number;
|
||||||
userId: number | null;
|
userId: number | null;
|
||||||
|
currentStatus?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CommentsAndAuditSection({
|
export default function CommentsAndAuditSection({
|
||||||
@@ -25,13 +38,24 @@ export default function CommentsAndAuditSection({
|
|||||||
handleCommentSubmit,
|
handleCommentSubmit,
|
||||||
validatorUser,
|
validatorUser,
|
||||||
userId,
|
userId,
|
||||||
|
currentStatus,
|
||||||
}: CommentsAndAuditSectionProps) {
|
}: CommentsAndAuditSectionProps) {
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||||
setActiveTab(newValue);
|
setActiveTab(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if there's validator feedback for changes requested status
|
||||||
|
// Show badge if status is ChangesRequested and there are validator events
|
||||||
|
const hasValidatorFeedback = currentStatus === 1 && auditEvents.some(event =>
|
||||||
|
event.User === validatorUser &&
|
||||||
|
(
|
||||||
|
event.EventType === AuditEventType.Error ||
|
||||||
|
event.EventType === AuditEventType.CheckList
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ p: 3, mt: 3 }}>
|
<Paper sx={{ p: 3, mt: 3 }}>
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||||
@@ -41,7 +65,24 @@ export default function CommentsAndAuditSection({
|
|||||||
aria-label="comments and audit tabs"
|
aria-label="comments and audit tabs"
|
||||||
>
|
>
|
||||||
<Tab label="Comments" />
|
<Tab label="Comments" />
|
||||||
<Tab label="Audit Events" />
|
<Tab
|
||||||
|
label={
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
Audit Events
|
||||||
|
{hasValidatorFeedback && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: '#ff9800',
|
||||||
|
animation: `${pulse} 2s ease-in-out infinite`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Stack,
|
Stack,
|
||||||
Avatar,
|
Avatar,
|
||||||
TextField,
|
TextField,
|
||||||
IconButton
|
IconButton,
|
||||||
|
Skeleton
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import SendIcon from '@mui/icons-material/Send';
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent";
|
import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent";
|
||||||
import CommentItem from './CommentItem';
|
import CommentItem from './CommentItem';
|
||||||
|
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
|
||||||
|
|
||||||
interface CommentsTabPanelProps {
|
interface CommentsTabPanelProps {
|
||||||
activeTab: number;
|
activeTab: number;
|
||||||
@@ -34,34 +35,35 @@ export default function CommentsTabPanel({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box role="tabpanel" hidden={activeTab !== 0}>
|
<Box
|
||||||
{activeTab === 0 && (
|
role="tabpanel"
|
||||||
<>
|
sx={{
|
||||||
<Stack spacing={2} sx={{ mb: 3 }}>
|
display: activeTab === 0 ? 'block' : 'none'
|
||||||
{commentEvents.length > 0 ? (
|
}}
|
||||||
commentEvents.map((event, index) => (
|
>
|
||||||
<CommentItem
|
<Stack spacing={2} sx={{ mb: 3 }}>
|
||||||
key={index}
|
{commentEvents.length > 0 ? (
|
||||||
event={event}
|
commentEvents.map((event, index) => (
|
||||||
validatorUser={validatorUser}
|
<CommentItem
|
||||||
/>
|
key={index}
|
||||||
))
|
event={event}
|
||||||
) : (
|
validatorUser={validatorUser}
|
||||||
<Box sx={{ textAlign: 'center', py: 2, color: 'text.secondary' }}>
|
|
||||||
No Comments
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{userId !== null && (
|
|
||||||
<CommentInput
|
|
||||||
newComment={newComment}
|
|
||||||
setNewComment={setNewComment}
|
|
||||||
handleCommentSubmit={handleCommentSubmit}
|
|
||||||
userId={userId}
|
|
||||||
/>
|
/>
|
||||||
)}
|
))
|
||||||
</>
|
) : (
|
||||||
|
<Box sx={{ textAlign: 'center', py: 2, color: 'text.secondary' }}>
|
||||||
|
No Comments
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{userId !== null && (
|
||||||
|
<CommentInput
|
||||||
|
newComment={newComment}
|
||||||
|
setNewComment={setNewComment}
|
||||||
|
handleCommentSubmit={handleCommentSubmit}
|
||||||
|
userId={userId}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -75,11 +77,32 @@ interface CommentInputProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CommentInput({ newComment, setNewComment, handleCommentSubmit, userId }: CommentInputProps) {
|
function CommentInput({ newComment, setNewComment, handleCommentSubmit, userId }: CommentInputProps) {
|
||||||
|
const { thumbnailUrl, isLoading } = useUserThumbnail(userId || undefined, '150x150');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
|
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
|
||||||
<Avatar
|
<Box sx={{ position: 'relative', width: 40, height: 40 }}>
|
||||||
src={`/thumbnails/user/${userId}`}
|
<Skeleton
|
||||||
/>
|
variant="circular"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
opacity: isLoading ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
animation="wave"
|
||||||
|
/>
|
||||||
|
<Avatar
|
||||||
|
src={thumbnailUrl || undefined}
|
||||||
|
sx={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
opacity: isLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
multiline
|
multiline
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
"use client"
|
import { Link } from "react-router-dom"
|
||||||
|
import { useState, useRef } from "react";
|
||||||
import Link from "next/link"
|
import { useUser } from "@/app/hooks/useUser";
|
||||||
import Image from "next/image";
|
|
||||||
import { UserInfo } from "@/app/ts/User";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
|
|
||||||
import AppBar from "@mui/material/AppBar";
|
import AppBar from "@mui/material/AppBar";
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
@@ -37,7 +34,7 @@ const navItems: HeaderButton[] = [
|
|||||||
|
|
||||||
function HeaderButton(header: HeaderButton) {
|
function HeaderButton(header: HeaderButton) {
|
||||||
return (
|
return (
|
||||||
<Button color="inherit" component={Link} href={header.href}>
|
<Button color="inherit" component={Link} to={header.href}>
|
||||||
{header.name}
|
{header.name}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -47,14 +44,26 @@ export default function Header() {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
const hasAnimated = useRef(false);
|
||||||
|
|
||||||
const handleLoginClick = () => {
|
const getAuthUrl = () => {
|
||||||
window.location.href =
|
const hostname = window.location.hostname;
|
||||||
"/auth/oauth2/login?redirect=" + window.location.href;
|
|
||||||
|
// Production only
|
||||||
|
if (hostname === 'maps.strafes.net') {
|
||||||
|
return 'https://auth.strafes.net';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to staging (works for staging.strafes.net and localhost)
|
||||||
|
return 'https://auth.staging.strafes.net';
|
||||||
};
|
};
|
||||||
|
|
||||||
const [valid, setValid] = useState<boolean>(false);
|
const handleLoginClick = () => {
|
||||||
const [user, setUser] = useState<UserInfo | null>(null);
|
const authUrl = getAuthUrl();
|
||||||
|
window.location.href = `${authUrl}/oauth2/login?redirect=${window.location.href}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { user, isLoggedIn } = useUser();
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const [quickLinksAnchor, setQuickLinksAnchor] = useState<null | HTMLElement>(null);
|
const [quickLinksAnchor, setQuickLinksAnchor] = useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
@@ -77,60 +86,34 @@ export default function Header() {
|
|||||||
setQuickLinksAnchor(null);
|
setQuickLinksAnchor(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getLoginInfo() {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/session/user");
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
setValid(false);
|
|
||||||
setUser(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = await response.json();
|
|
||||||
const isLoggedIn = userData && 'UserID' in userData;
|
|
||||||
|
|
||||||
setValid(isLoggedIn);
|
|
||||||
setUser(isLoggedIn ? userData : null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching user data:", error);
|
|
||||||
setValid(false);
|
|
||||||
setUser(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getLoginInfo();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Mobile navigation drawer content
|
// Mobile navigation drawer content
|
||||||
const drawer = (
|
const drawer = (
|
||||||
<Box onClick={handleDrawerToggle} sx={{ textAlign: 'center' }}>
|
<Box onClick={handleDrawerToggle} sx={{ textAlign: 'center' }}>
|
||||||
<List>
|
<List>
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<ListItem key={item.name} disablePadding>
|
<ListItem key={item.name} disablePadding>
|
||||||
<ListItemButton component={Link} href={item.href} sx={{ textAlign: 'center' }}>
|
<ListItemButton component={Link} to={item.href} sx={{ textAlign: 'center' }}>
|
||||||
<ListItemText primary={item.name} />
|
<ListItemText primary={item.name} />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
{valid && user && (
|
{isLoggedIn && user && (
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemButton component={Link} href="/submit" sx={{ textAlign: 'center' }}>
|
<ListItemButton component={Link} to="/submit" sx={{ textAlign: 'center' }}>
|
||||||
<ListItemText primary="Submit Map" sx={{ color: 'success.main' }} />
|
<ListItemText primary="Submit Map" sx={{ color: 'success.main' }} />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
{!valid && (
|
{!isLoggedIn && (
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemButton onClick={handleLoginClick} sx={{ textAlign: 'center' }}>
|
<ListItemButton onClick={handleLoginClick} sx={{ textAlign: 'center' }}>
|
||||||
<ListItemText primary="Login" />
|
<ListItemText primary="Login" />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
{valid && user && (
|
{isLoggedIn && user && (
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemButton component={Link} href="/auth" sx={{ textAlign: 'center' }}>
|
<ListItemButton component="a" href={getAuthUrl()} sx={{ textAlign: 'center' }}>
|
||||||
<ListItemText primary="Manage Account" />
|
<ListItemText primary="Manage Account" />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -150,7 +133,7 @@ export default function Header() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position="static">
|
<AppBar position="static">
|
||||||
<Toolbar>
|
<Toolbar sx={{ py: 1 }}>
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@@ -165,20 +148,144 @@ export default function Header() {
|
|||||||
|
|
||||||
{/* Desktop navigation */}
|
{/* Desktop navigation */}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<Box display="flex" flexGrow={1} gap={2} alignItems="center">
|
<Box display="flex" flexGrow={1} gap={1} alignItems="center">
|
||||||
|
{/* Logo/Brand */}
|
||||||
|
<Box
|
||||||
|
component={Link}
|
||||||
|
to="/"
|
||||||
|
sx={{
|
||||||
|
mr: 4,
|
||||||
|
textDecoration: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'@keyframes speedLine': {
|
||||||
|
'0%': {
|
||||||
|
transform: 'translateX(-50px) scaleX(0.5)',
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
'40%': {
|
||||||
|
opacity: 0.8,
|
||||||
|
transform: 'translateX(0px) scaleX(1)',
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translateX(30px) scaleX(0.7)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'@keyframes logoReveal': {
|
||||||
|
'0%': {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translateX(-10px)',
|
||||||
|
filter: 'blur(2px)',
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'translateX(0px)',
|
||||||
|
filter: 'blur(0px)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'&::before, &::after': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '2px',
|
||||||
|
background: 'linear-gradient(90deg, transparent 10%, rgba(59, 130, 246, 0.8) 50%, transparent 90%)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
animation: !hasAnimated.current ? 'speedLine 0.6s ease-out forwards' : 'none',
|
||||||
|
opacity: !hasAnimated.current ? 0 : undefined,
|
||||||
|
},
|
||||||
|
'&::before': {
|
||||||
|
top: '35%',
|
||||||
|
animationDelay: !hasAnimated.current ? '0s' : undefined,
|
||||||
|
},
|
||||||
|
'&::after': {
|
||||||
|
top: '65%',
|
||||||
|
animationDelay: !hasAnimated.current ? '0.08s' : undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '1px',
|
||||||
|
background: 'linear-gradient(90deg, transparent 10%, rgba(139, 92, 246, 0.6) 50%, transparent 90%)',
|
||||||
|
animation: !hasAnimated.current ? 'speedLine 0.6s ease-out forwards' : 'none',
|
||||||
|
animationDelay: !hasAnimated.current ? '0.04s' : '0s',
|
||||||
|
opacity: !hasAnimated.current ? 0 : undefined,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
color: 'text.primary',
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: '-0.01em',
|
||||||
|
fontSize: '1.125rem',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
opacity: !hasAnimated.current ? 0 : 1,
|
||||||
|
animation: !hasAnimated.current ? 'logoReveal 0.5s ease-out forwards' : 'none',
|
||||||
|
animationDelay: !hasAnimated.current ? '0.5s' : '0s',
|
||||||
|
}}
|
||||||
|
onAnimationEnd={() => {
|
||||||
|
hasAnimated.current = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
StrafesNET
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<HeaderButton key={item.name} name={item.name} href={item.href} />
|
<Button
|
||||||
|
key={item.name}
|
||||||
|
color="inherit"
|
||||||
|
component={Link}
|
||||||
|
to={item.href}
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
py: 1,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
color: 'text.secondary',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||||
|
color: 'text.primary',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Button>
|
||||||
))}
|
))}
|
||||||
<Box sx={{ flexGrow: 1 }} /> {/* Push quick links to the right */}
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
{/* Quick Links Dropdown */}
|
{/* Quick Links Dropdown */}
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
color="inherit"
|
color="inherit"
|
||||||
endIcon={<ArrowDropDownIcon />}
|
endIcon={<ArrowDropDownIcon />}
|
||||||
onClick={handleQuickLinksOpen}
|
onClick={handleQuickLinksOpen}
|
||||||
sx={{ textTransform: 'none', fontSize: '0.95rem', px: 1 }}
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
mr: 1,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
color: 'text.secondary',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||||
|
color: 'text.primary',
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
QUICK LINKS
|
Quick Links
|
||||||
</Button>
|
</Button>
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={quickLinksAnchor}
|
anchorEl={quickLinksAnchor}
|
||||||
@@ -186,12 +293,20 @@ export default function Header() {
|
|||||||
onClose={handleQuickLinksClose}
|
onClose={handleQuickLinksClose}
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||||
|
sx={{
|
||||||
|
'& .MuiMenu-paper': {
|
||||||
|
mt: 1.5,
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{quickLinks.map(link => (
|
{quickLinks.map(link => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={link.name}
|
key={link.name}
|
||||||
onClick={handleQuickLinksClose}
|
onClick={handleQuickLinksClose}
|
||||||
sx={{ minWidth: 180 }}
|
sx={{
|
||||||
|
minWidth: 200,
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
}}
|
||||||
component="a"
|
component="a"
|
||||||
href={link.href}
|
href={link.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -209,30 +324,53 @@ export default function Header() {
|
|||||||
{isMobile && <Box sx={{ flexGrow: 1 }} />}
|
{isMobile && <Box sx={{ flexGrow: 1 }} />}
|
||||||
|
|
||||||
{/* Right side of nav */}
|
{/* Right side of nav */}
|
||||||
<Box display="flex" gap={2}>
|
<Box display="flex" gap={2} alignItems="center">
|
||||||
{!isMobile && valid && user && (
|
{!isMobile && isLoggedIn && user && (
|
||||||
<Button variant="outlined" color="success" component={Link} href="/submit">
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
component={Link}
|
||||||
|
to="/submit"
|
||||||
|
sx={{
|
||||||
|
px: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
Submit Map
|
Submit Map
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!isMobile && valid && user ? (
|
{!isMobile && isLoggedIn && user ? (
|
||||||
<Box display="flex" alignItems="center">
|
<Box display="flex" alignItems="center">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleMenuOpen}
|
onClick={handleMenuOpen}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
size="small"
|
size="small"
|
||||||
style={{ textTransform: "none" }}
|
sx={{
|
||||||
|
textTransform: "none",
|
||||||
|
borderRadius: 1.5,
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.75,
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||||
|
borderColor: 'rgba(59, 130, 246, 0.3)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<img
|
||||||
className="avatar"
|
className="avatar"
|
||||||
width={28}
|
width={28}
|
||||||
height={28}
|
height={28}
|
||||||
priority={true}
|
|
||||||
src={user.AvatarURL}
|
src={user.AvatarURL}
|
||||||
alt={user.Username}
|
alt={user.Username}
|
||||||
style={{ marginRight: 8 }}
|
style={{
|
||||||
|
marginRight: 8,
|
||||||
|
borderRadius: '50%',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography variant="body1">{user.Username}</Typography>
|
<Typography variant="body2" sx={{ fontSize: '0.875rem', fontWeight: 500 }}>
|
||||||
|
{user.Username}
|
||||||
|
</Typography>
|
||||||
</Button>
|
</Button>
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
@@ -240,38 +378,58 @@ export default function Header() {
|
|||||||
onClose={handleMenuClose}
|
onClose={handleMenuClose}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
vertical: "bottom",
|
vertical: "bottom",
|
||||||
horizontal: "left",
|
horizontal: "right",
|
||||||
}}
|
}}
|
||||||
transformOrigin={{
|
transformOrigin={{
|
||||||
vertical: "top",
|
vertical: "top",
|
||||||
horizontal: "left",
|
horizontal: "right",
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
'& .MuiMenu-paper': {
|
||||||
|
mt: 1.5,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem component={Link} href="/auth">
|
<MenuItem
|
||||||
Manage
|
component="a"
|
||||||
|
href={getAuthUrl()}
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Manage Account
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
) : !isMobile && (
|
) : !isMobile && (
|
||||||
<Button color="inherit" onClick={handleLoginClick}>
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={handleLoginClick}
|
||||||
|
sx={{
|
||||||
|
px: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* In mobile view, display just the avatar if logged in */}
|
{/* In mobile view, display just the avatar if logged in */}
|
||||||
{isMobile && valid && user && (
|
{isMobile && isLoggedIn && user && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleMenuOpen}
|
onClick={handleMenuOpen}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
<Image
|
<img
|
||||||
className="avatar"
|
className="avatar"
|
||||||
width={28}
|
width={32}
|
||||||
height={28}
|
height={32}
|
||||||
priority={true}
|
|
||||||
src={user.AvatarURL}
|
src={user.AvatarURL}
|
||||||
alt={user.Username}
|
alt={user.Username}
|
||||||
|
style={{
|
||||||
|
borderRadius: '50%',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
@@ -284,10 +442,13 @@ export default function Header() {
|
|||||||
open={mobileOpen}
|
open={mobileOpen}
|
||||||
onClose={handleDrawerToggle}
|
onClose={handleDrawerToggle}
|
||||||
ModalProps={{
|
ModalProps={{
|
||||||
keepMounted: true, // Better open performance on mobile
|
keepMounted: true,
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: 240 },
|
'& .MuiDrawer-paper': {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
width: 240,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{drawer}
|
{drawer}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import React from "react";
|
import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Divider, Typography, Skeleton} from "@mui/material";
|
||||||
import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Divider, Grid, Typography} from "@mui/material";
|
import {Explore, Person2, Assignment, Build} from "@mui/icons-material";
|
||||||
import {Explore, Person2} from "@mui/icons-material";
|
|
||||||
import {StatusChip} from "@/app/_components/statusChip";
|
import {StatusChip} from "@/app/_components/statusChip";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
import {useAssetThumbnail, useUserThumbnail} from "@/app/hooks/useThumbnails";
|
||||||
|
import {useUsername} from "@/app/hooks/useUsername";
|
||||||
|
import { getGameName } from "@/app/utils/games";
|
||||||
|
|
||||||
interface MapCardProps {
|
interface MapCardProps {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@@ -14,173 +17,176 @@ interface MapCardProps {
|
|||||||
gameID: number;
|
gameID: number;
|
||||||
created: number;
|
created: number;
|
||||||
type: 'mapfix' | 'submission';
|
type: 'mapfix' | 'submission';
|
||||||
|
showTypeBadge?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CARD_WIDTH = 270;
|
|
||||||
|
|
||||||
export function MapCard(props: MapCardProps) {
|
export function MapCard(props: MapCardProps) {
|
||||||
|
const { thumbnailUrl: assetThumbnail, isLoading: assetLoading } = useAssetThumbnail(props.assetId);
|
||||||
|
const { thumbnailUrl: userThumbnail, isLoading: userLoading } = useUserThumbnail(props.authorId);
|
||||||
|
const { username, isLoading: usernameLoading } = useUsername(props.type === 'mapfix' ? props.authorId : undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 3 }} key={props.assetId}>
|
<Card sx={{ height: '100%' }}>
|
||||||
<Box sx={{
|
<CardActionArea
|
||||||
width: CARD_WIDTH,
|
component={Link}
|
||||||
mx: 'auto', // Center the card in its grid cell
|
to={`/${props.type === 'submission' ? 'submissions' : 'mapfixes'}/${props.id}`}>
|
||||||
}}>
|
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
|
||||||
<Card sx={{
|
<Skeleton
|
||||||
width: CARD_WIDTH,
|
variant="rectangular"
|
||||||
height: 340, // Fixed height for all cards
|
height={180}
|
||||||
display: 'flex',
|
animation="wave"
|
||||||
flexDirection: 'column',
|
|
||||||
}}>
|
|
||||||
<CardActionArea
|
|
||||||
sx={{
|
|
||||||
height: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'stretch'
|
|
||||||
}}
|
|
||||||
href={`/${props.type === 'submission' ? 'submissions' : 'mapfixes'}/${props.id}`}>
|
|
||||||
<Box sx={{ position: 'relative' }}>
|
|
||||||
<CardMedia
|
|
||||||
component="img"
|
|
||||||
image={`/thumbnails/asset/${props.assetId}`}
|
|
||||||
alt={props.displayName}
|
|
||||||
sx={{
|
sx={{
|
||||||
height: 160, // Fixed height for all images
|
position: 'absolute',
|
||||||
objectFit: 'cover',
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
opacity: assetLoading ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
image={assetThumbnail || '/placeholder-map.png'}
|
||||||
|
alt={props.displayName}
|
||||||
|
sx={{
|
||||||
|
height: 180,
|
||||||
|
objectFit: 'cover',
|
||||||
|
opacity: assetLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.3s ease-in-out, transform 0.3s',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'scale(1.05)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{props.showTypeBadge && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 12,
|
||||||
|
left: 12,
|
||||||
|
opacity: assetLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.3s ease-in-out 0.1s',
|
||||||
|
bgcolor: props.type === 'submission' ? 'primary.main' : 'secondary.main',
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.type === 'submission' ? <Assignment sx={{ fontSize: '1.1rem' }} /> : <Build sx={{ fontSize: '1.1rem' }} />}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 12,
|
top: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
|
opacity: assetLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.3s ease-in-out 0.1s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StatusChip status={props.statusID}/>
|
<StatusChip status={props.statusID}/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<CardContent sx={{
|
<CardContent>
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
p: 2,
|
|
||||||
width: '100%',
|
|
||||||
}}>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Typography
|
<Typography
|
||||||
variant="subtitle1"
|
variant="h6"
|
||||||
component="div"
|
component="div"
|
||||||
sx={{
|
sx={{
|
||||||
mb: 1,
|
mb: 1.5,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#fff',
|
lineHeight: '1.4',
|
||||||
lineHeight: '1.3',
|
|
||||||
// Allow text to wrap
|
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
WebkitLineClamp: 2,
|
WebkitLineClamp: 2,
|
||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
|
minHeight: '2.8em',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.displayName}
|
{props.displayName}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
mb: 1.5,
|
gap: 2,
|
||||||
|
mb: 2,
|
||||||
|
flexWrap: 'wrap',
|
||||||
}}>
|
}}>
|
||||||
<Explore sx={{
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
mr: 0.75,
|
<Explore sx={{ fontSize: '1rem', color: '#6366f1' }} />
|
||||||
mt: 0.25,
|
<Typography variant="body2" color="text.secondary" fontSize="0.875rem">
|
||||||
color: 'text.secondary',
|
{getGameName(props.gameID)}
|
||||||
fontSize: '0.9rem',
|
</Typography>
|
||||||
flexShrink: 0,
|
</Box>
|
||||||
}} />
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
<Typography
|
<Person2 sx={{ fontSize: '1rem', color: '#8b5cf6' }} />
|
||||||
variant="body2"
|
{props.type === 'mapfix' && usernameLoading ? (
|
||||||
color="text.secondary"
|
<Skeleton variant="text" width={80} />
|
||||||
sx={{
|
) : (
|
||||||
fontWeight: 500,
|
<Typography
|
||||||
// Allow text to wrap
|
variant="body2"
|
||||||
overflow: 'hidden',
|
color="text.secondary"
|
||||||
textOverflow: 'ellipsis',
|
fontSize="0.875rem"
|
||||||
display: '-webkit-box',
|
sx={{
|
||||||
WebkitLineClamp: 2,
|
overflow: 'hidden',
|
||||||
WebkitBoxOrient: 'vertical',
|
textOverflow: 'ellipsis',
|
||||||
lineHeight: '1.2',
|
whiteSpace: 'nowrap',
|
||||||
wordBreak: 'break-word',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{props.type === 'mapfix' && username ? `@${username}` : props.author}
|
||||||
{props.gameID === 1 ? 'Bhop' : props.gameID === 2 ? 'Surf' : props.gameID === 5 ? 'Fly Trials' : props.gameID === 4 ? 'Deathrun' : 'Unknown'}
|
</Typography>
|
||||||
</Typography>
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{
|
|
||||||
display: 'flex',
|
|
||||||
mb: 1.5,
|
|
||||||
}}>
|
|
||||||
<Person2 sx={{
|
|
||||||
mr: 0.75,
|
|
||||||
mt: 0.25,
|
|
||||||
color: 'text.secondary',
|
|
||||||
fontSize: '0.9rem',
|
|
||||||
flexShrink: 0,
|
|
||||||
}} />
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 500,
|
|
||||||
// Allow text to wrap
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitLineClamp: 2,
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
lineHeight: '1.2',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.author}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Divider sx={{ my: 1.5 }} />
|
<Divider sx={{ my: 1.5 }} />
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Skeleton
|
||||||
|
variant="circular"
|
||||||
|
width={28}
|
||||||
|
height={28}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
opacity: userLoading ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={`/thumbnails/user/${props.authorId}`}
|
src={userThumbnail || undefined}
|
||||||
alt={props.author}
|
alt={props.author}
|
||||||
sx={{
|
sx={{
|
||||||
width: 24,
|
width: 28,
|
||||||
height: 24,
|
height: 28,
|
||||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
opacity: userLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
sx={{
|
sx={{
|
||||||
ml: 1,
|
|
||||||
color: 'text.secondary',
|
|
||||||
fontWeight: 500,
|
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/*In the future author should be the username of the submitter not the info from the map*/}
|
{new Date(props.created * 1000).toLocaleDateString('en-US', {
|
||||||
{props.author} - {new Date(props.created * 1000).toLocaleDateString('en-US', {
|
month: 'short',
|
||||||
year: 'numeric',
|
day: 'numeric',
|
||||||
month: 'long',
|
year: 'numeric'
|
||||||
day: 'numeric'
|
|
||||||
})}
|
})}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</CardActionArea>
|
</CardActionArea>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Button, Stack } from '@mui/material';
|
import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, Typography, Box } from '@mui/material';
|
||||||
import {MapfixInfo } from "@/app/ts/Mapfix";
|
import {MapfixInfo } from "@/app/ts/Mapfix";
|
||||||
import {hasRole, Roles, RolesConstants} from "@/app/ts/Roles";
|
import {hasRole, Roles, RolesConstants} from "@/app/ts/Roles";
|
||||||
import {SubmissionInfo} from "@/app/ts/Submission";
|
import {SubmissionInfo} from "@/app/ts/Submission";
|
||||||
import {Status, StatusMatches} from "@/app/ts/Status";
|
import {Status, StatusMatches} from "@/app/ts/Status";
|
||||||
|
|
||||||
interface ReviewAction {
|
interface ReviewAction {
|
||||||
name: string,
|
name: string;
|
||||||
action: string,
|
action: string;
|
||||||
|
confirmTitle?: string;
|
||||||
|
confirmMessage?: string;
|
||||||
|
requiresConfirmation: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReviewButtonsProps {
|
interface ReviewButtonsProps {
|
||||||
@@ -19,20 +22,102 @@ interface ReviewButtonsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ReviewActions = {
|
const ReviewActions = {
|
||||||
Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction,
|
Submit: {
|
||||||
AdminSubmit: {name:"Admin Submit",action:"trigger-submit"} as ReviewAction,
|
name: "Submit for Review",
|
||||||
SubmitUnchecked: {name:"Submit Unchecked", action:"trigger-submit-unchecked"} as ReviewAction,
|
action: "trigger-submit",
|
||||||
ResetSubmitting: {name:"Reset Submitting",action:"reset-submitting"} as ReviewAction,
|
confirmTitle: "Submit for Review",
|
||||||
Revoke: {name:"Revoke",action:"revoke"} as ReviewAction,
|
confirmMessage: "Are you ready to submit this for review? The model version is locked in once submitted, but you can revoke it later if needed.",
|
||||||
Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction,
|
requiresConfirmation: true
|
||||||
Reject: {name:"Reject",action:"reject"} as ReviewAction,
|
} as ReviewAction,
|
||||||
Validate: {name:"Validate",action:"retry-validate"} as ReviewAction,
|
AdminSubmit: {
|
||||||
ResetValidating: {name:"Reset Validating",action:"reset-validating"} as ReviewAction,
|
name: "Submit on Behalf of User",
|
||||||
RequestChanges: {name:"Request Changes",action:"request-changes"} as ReviewAction,
|
action: "trigger-submit",
|
||||||
Upload: {name:"Upload",action:"trigger-upload"} as ReviewAction,
|
confirmTitle: "Admin Submit",
|
||||||
ResetUploading: {name:"Reset Uploading",action:"reset-uploading"} as ReviewAction,
|
confirmMessage: "This will submit the work as if the original user did it. Continue?",
|
||||||
Release: {name:"Release",action:"trigger-release"} as ReviewAction,
|
requiresConfirmation: true
|
||||||
ResetReleasing: {name:"Reset Releasing",action:"reset-releasing"} as ReviewAction,
|
} as ReviewAction,
|
||||||
|
SubmitUnchecked: {
|
||||||
|
name: "Approve Without Validation",
|
||||||
|
action: "trigger-submit-unchecked",
|
||||||
|
confirmTitle: "Skip Validation",
|
||||||
|
confirmMessage: "This will approve without running validation checks. Only use this if you're certain the work is correct.",
|
||||||
|
requiresConfirmation: true
|
||||||
|
} as ReviewAction,
|
||||||
|
ResetSubmitting: {
|
||||||
|
name: "Reset Submit Process",
|
||||||
|
action: "reset-submitting",
|
||||||
|
confirmTitle: "Reset Submit",
|
||||||
|
confirmMessage: "This will force-cancel the submission process and return to 'Under Construction' status. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
|
||||||
|
requiresConfirmation: true
|
||||||
|
} as ReviewAction,
|
||||||
|
Revoke: {
|
||||||
|
name: "Revoke",
|
||||||
|
action: "revoke",
|
||||||
|
confirmTitle: "Revoke",
|
||||||
|
confirmMessage: "This will withdraw from review and return to 'Under Construction' status.",
|
||||||
|
requiresConfirmation: true
|
||||||
|
} as ReviewAction,
|
||||||
|
Accept: {
|
||||||
|
name: "Accept & Validate",
|
||||||
|
action: "trigger-validate",
|
||||||
|
confirmTitle: "Accept",
|
||||||
|
confirmMessage: "This will accept and trigger validation. The work will proceed to the next stage.",
|
||||||
|
requiresConfirmation: true
|
||||||
|
} as ReviewAction,
|
||||||
|
Reject: {
|
||||||
|
name: "Reject",
|
||||||
|
action: "reject",
|
||||||
|
confirmTitle: "Reject",
|
||||||
|
confirmMessage: "This will permanently reject. The user will need to create a new one. Are you sure?",
|
||||||
|
requiresConfirmation: true
|
||||||
|
} as ReviewAction,
|
||||||
|
Validate: {
|
||||||
|
name: "Run Validation",
|
||||||
|
action: "retry-validate",
|
||||||
|
requiresConfirmation: false
|
||||||
|
} as ReviewAction,
|
||||||
|
ResetValidating: {
|
||||||
|
name: "Reset Validation Process",
|
||||||
|
action: "reset-validating",
|
||||||
|
confirmTitle: "Reset Validation",
|
||||||
|
confirmMessage: "This will force-abort the validation process so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
|
||||||
|
requiresConfirmation: true
|
||||||
|
} as ReviewAction,
|
||||||
|
RequestChanges: {
|
||||||
|
name: "Request Changes",
|
||||||
|
action: "request-changes",
|
||||||
|
confirmTitle: "Request Changes",
|
||||||
|
confirmMessage: "Request that the submitter make changes. Make sure you've explained which changes are requested in a comment.",
|
||||||
|
requiresConfirmation: true
|
||||||
|
} as ReviewAction,
|
||||||
|
Upload: {
|
||||||
|
name: "Upload to Roblox",
|
||||||
|
action: "trigger-upload",
|
||||||
|
confirmTitle: "Upload to Roblox Group",
|
||||||
|
confirmMessage: "This will upload the validated work to the Roblox group. Continue?",
|
||||||
|
requiresConfirmation: true
|
||||||
|
} as ReviewAction,
|
||||||
|
ResetUploading: {
|
||||||
|
name: "Reset Upload Process",
|
||||||
|
action: "reset-uploading",
|
||||||
|
confirmTitle: "Reset Upload",
|
||||||
|
confirmMessage: "This will force-abort the upload to Roblox so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
|
||||||
|
requiresConfirmation: true
|
||||||
|
} as ReviewAction,
|
||||||
|
Release: {
|
||||||
|
name: "Release to Game",
|
||||||
|
action: "trigger-release",
|
||||||
|
confirmTitle: "Release to Game",
|
||||||
|
confirmMessage: "This will make the work available in game. This is the final step!",
|
||||||
|
requiresConfirmation: true
|
||||||
|
} as ReviewAction,
|
||||||
|
ResetReleasing: {
|
||||||
|
name: "Reset Release Process",
|
||||||
|
action: "reset-releasing",
|
||||||
|
confirmTitle: "Reset Release",
|
||||||
|
confirmMessage: "This will force-abort the release to the game so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
|
||||||
|
requiresConfirmation: true
|
||||||
|
} as ReviewAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
||||||
@@ -42,16 +127,46 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
|||||||
roles,
|
roles,
|
||||||
type,
|
type,
|
||||||
}) => {
|
}) => {
|
||||||
const getVisibleButtons = () => {
|
const [confirmDialog, setConfirmDialog] = useState<{
|
||||||
if (!item || userId === null) return [];
|
open: boolean;
|
||||||
|
action: ReviewAction | null;
|
||||||
|
}>({ open: false, action: null });
|
||||||
|
|
||||||
|
const handleButtonClick = (action: ReviewAction) => {
|
||||||
|
if (action.requiresConfirmation) {
|
||||||
|
setConfirmDialog({ open: true, action });
|
||||||
|
} else {
|
||||||
|
onClick(action.action, item.ID);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (confirmDialog.action) {
|
||||||
|
onClick(confirmDialog.action.action, item.ID);
|
||||||
|
}
|
||||||
|
setConfirmDialog({ open: false, action: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setConfirmDialog({ open: false, action: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVisibleButtons = () => {
|
||||||
|
if (!item || userId === null) return { primary: [], secondary: [], submitter: [], reviewer: [], admin: [] };
|
||||||
|
|
||||||
// Define a type for the button
|
|
||||||
type ReviewButton = {
|
type ReviewButton = {
|
||||||
action: ReviewAction;
|
action: ReviewAction;
|
||||||
color: "primary" | "error" | "success" | "info" | "warning";
|
color: "primary" | "error" | "success" | "info" | "warning";
|
||||||
|
variant?: "contained" | "outlined";
|
||||||
|
isPrimary?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttons: ReviewButton[] = [];
|
const primaryButtons: ReviewButton[] = [];
|
||||||
|
const secondaryButtons: ReviewButton[] = [];
|
||||||
|
const submitterButtons: ReviewButton[] = [];
|
||||||
|
const reviewerButtons: ReviewButton[] = [];
|
||||||
|
const adminButtons: ReviewButton[] = [];
|
||||||
|
|
||||||
const is_submitter = userId === item.Submitter;
|
const is_submitter = userId === item.Submitter;
|
||||||
const status = item.StatusID;
|
const status = item.StatusID;
|
||||||
|
|
||||||
@@ -59,133 +174,215 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
|||||||
const uploadRole = type === "submission" ? RolesConstants.SubmissionUpload : RolesConstants.MapfixUpload;
|
const uploadRole = type === "submission" ? RolesConstants.SubmissionUpload : RolesConstants.MapfixUpload;
|
||||||
const releaseRole = type === "submission" ? RolesConstants.SubmissionRelease : RolesConstants.MapfixRelease;
|
const releaseRole = type === "submission" ? RolesConstants.SubmissionRelease : RolesConstants.MapfixRelease;
|
||||||
|
|
||||||
|
// Submitter actions
|
||||||
if (is_submitter) {
|
if (is_submitter) {
|
||||||
if (StatusMatches(status, [Status.UnderConstruction, Status.ChangesRequested])) {
|
if (StatusMatches(status, [Status.UnderConstruction, Status.ChangesRequested])) {
|
||||||
buttons.push({
|
submitterButtons.push({
|
||||||
action: ReviewActions.Submit,
|
action: ReviewActions.Submit,
|
||||||
color: "primary"
|
color: "success"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StatusMatches(status, [Status.Submitted, Status.ChangesRequested])) {
|
if (StatusMatches(status, [Status.Submitted, Status.ChangesRequested])) {
|
||||||
buttons.push({
|
submitterButtons.push({
|
||||||
action: ReviewActions.Revoke,
|
action: ReviewActions.Revoke,
|
||||||
color: "error"
|
color: "warning",
|
||||||
|
variant: "outlined"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === Status.Submitting) {
|
if (status === Status.Submitting) {
|
||||||
buttons.push({
|
adminButtons.push({
|
||||||
action: ReviewActions.ResetSubmitting,
|
action: ReviewActions.ResetSubmitting,
|
||||||
color: "warning"
|
color: "error",
|
||||||
|
variant: "outlined"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buttons for review role
|
// Reviewer actions
|
||||||
if (hasRole(roles, reviewRole)) {
|
if (hasRole(roles, reviewRole)) {
|
||||||
if (status === Status.Submitted && !is_submitter) {
|
if (status === Status.Submitted && !is_submitter) {
|
||||||
buttons.push(
|
reviewerButtons.push({
|
||||||
{
|
action: ReviewActions.Accept,
|
||||||
action: ReviewActions.Accept,
|
color: "success"
|
||||||
color: "success"
|
});
|
||||||
},
|
reviewerButtons.push({
|
||||||
{
|
action: ReviewActions.Reject,
|
||||||
action: ReviewActions.Reject,
|
color: "error",
|
||||||
color: "error"
|
variant: "outlined"
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === Status.AcceptedUnvalidated) {
|
if (status === Status.AcceptedUnvalidated) {
|
||||||
buttons.push({
|
reviewerButtons.push({
|
||||||
action: ReviewActions.Validate,
|
action: ReviewActions.Validate,
|
||||||
color: "info"
|
color: "primary"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === Status.Validating) {
|
if (status === Status.Validating) {
|
||||||
buttons.push({
|
adminButtons.push({
|
||||||
action: ReviewActions.ResetValidating,
|
action: ReviewActions.ResetValidating,
|
||||||
color: "warning"
|
color: "error",
|
||||||
|
variant: "outlined"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StatusMatches(status, [Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) {
|
if (StatusMatches(status, [Status.Uploaded, Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) {
|
||||||
buttons.push({
|
reviewerButtons.push({
|
||||||
action: ReviewActions.RequestChanges,
|
action: ReviewActions.RequestChanges,
|
||||||
color: "warning"
|
color: "warning",
|
||||||
|
variant: "outlined"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === Status.ChangesRequested) {
|
if (status === Status.ChangesRequested) {
|
||||||
buttons.push({
|
adminButtons.push({
|
||||||
action: ReviewActions.SubmitUnchecked,
|
action: ReviewActions.SubmitUnchecked,
|
||||||
color: "warning"
|
color: "warning",
|
||||||
|
variant: "outlined"
|
||||||
});
|
});
|
||||||
// button only exists for submissions
|
|
||||||
// submitter has normal submit button
|
|
||||||
if (type === "submission" && !is_submitter) {
|
if (type === "submission" && !is_submitter) {
|
||||||
buttons.push({
|
adminButtons.push({
|
||||||
action: ReviewActions.AdminSubmit,
|
action: ReviewActions.AdminSubmit,
|
||||||
color: "primary"
|
color: "info",
|
||||||
|
variant: "outlined"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buttons for upload role
|
// Upload role actions
|
||||||
if (hasRole(roles, uploadRole)) {
|
if (hasRole(roles, uploadRole)) {
|
||||||
if (status === Status.Validated) {
|
if (status === Status.Validated) {
|
||||||
buttons.push({
|
reviewerButtons.push({
|
||||||
action: ReviewActions.Upload,
|
action: ReviewActions.Upload,
|
||||||
color: "success"
|
color: "success"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === Status.Uploading) {
|
if (status === Status.Uploading) {
|
||||||
buttons.push({
|
adminButtons.push({
|
||||||
action: ReviewActions.ResetUploading,
|
action: ReviewActions.ResetUploading,
|
||||||
color: "warning"
|
color: "error",
|
||||||
|
variant: "outlined"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buttons for release role
|
// Release role actions
|
||||||
if (hasRole(roles, releaseRole)) {
|
if (hasRole(roles, releaseRole)) {
|
||||||
// submissions do not have a release button
|
|
||||||
if (type === "mapfix" && status === Status.Uploaded) {
|
if (type === "mapfix" && status === Status.Uploaded) {
|
||||||
buttons.push({
|
reviewerButtons.push({
|
||||||
action: ReviewActions.Release,
|
action: ReviewActions.Release,
|
||||||
color: "success"
|
color: "success"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === Status.Releasing) {
|
if (status === Status.Releasing) {
|
||||||
buttons.push({
|
adminButtons.push({
|
||||||
action: ReviewActions.ResetReleasing,
|
action: ReviewActions.ResetReleasing,
|
||||||
color: "warning"
|
color: "error",
|
||||||
|
variant: "outlined"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return buttons;
|
return {
|
||||||
|
primary: primaryButtons,
|
||||||
|
secondary: secondaryButtons,
|
||||||
|
submitter: submitterButtons,
|
||||||
|
reviewer: reviewerButtons,
|
||||||
|
admin: adminButtons
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttons = getVisibleButtons();
|
||||||
|
const hasAnyButtons = buttons.submitter.length > 0 || buttons.reviewer.length > 0 || buttons.admin.length > 0;
|
||||||
|
|
||||||
|
if (!hasAnyButtons) return null;
|
||||||
|
|
||||||
|
const ActionCard = ({ title, actions, isFirst = false }: { title: string; actions: any[]; isFirst?: boolean }) => {
|
||||||
|
if (actions.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: isFirst ? 0 : 3 }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
fontWeight={600}
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
mb: 1.5,
|
||||||
|
display: 'block'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
{actions.map((button, index) => (
|
||||||
|
<Button
|
||||||
|
key={index}
|
||||||
|
variant="contained"
|
||||||
|
color={button.color}
|
||||||
|
fullWidth
|
||||||
|
size="large"
|
||||||
|
onClick={() => handleButtonClick(button.action)}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
py: 1.5
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{button.action.name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={2} sx={{ mb: 3 }}>
|
<>
|
||||||
{getVisibleButtons().map((button, index) => (
|
<Box sx={{ mb: 3 }}>
|
||||||
<Button
|
<ActionCard title="Your Actions" actions={buttons.submitter} isFirst={true} />
|
||||||
key={index}
|
<ActionCard title="Review Actions" actions={buttons.reviewer} isFirst={buttons.submitter.length === 0} />
|
||||||
variant="contained"
|
<ActionCard title="Admin Actions" actions={buttons.admin} isFirst={buttons.submitter.length === 0 && buttons.reviewer.length === 0} />
|
||||||
color={button.color}
|
</Box>
|
||||||
fullWidth
|
|
||||||
onClick={() => onClick(button.action.action, item.ID)}
|
{/* Confirmation Dialog */}
|
||||||
>
|
<Dialog
|
||||||
{button.action.name}
|
open={confirmDialog.open}
|
||||||
</Button>
|
onClose={handleCancel}
|
||||||
))}
|
maxWidth="xs"
|
||||||
</Stack>
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ pb: 1 }}>
|
||||||
|
{confirmDialog.action?.confirmTitle || confirmDialog.action?.name}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{confirmDialog.action?.confirmMessage || "Are you sure you want to proceed?"}
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||||
|
<Button onClick={handleCancel} color="inherit">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { Paper, Grid, Typography } from "@mui/material";
|
import { Paper, Grid, Typography, TextField, IconButton, Box } from "@mui/material";
|
||||||
import { ReviewItemHeader } from "./ReviewItemHeader";
|
import { ReviewItemHeader } from "./ReviewItemHeader";
|
||||||
import { CopyableField } from "@/app/_components/review/CopyableField";
|
import { CopyableField } from "@/app/_components/review/CopyableField";
|
||||||
|
import WorkflowStepper from "./WorkflowStepper";
|
||||||
import { SubmissionInfo } from "@/app/ts/Submission";
|
import { SubmissionInfo } from "@/app/ts/Submission";
|
||||||
import { MapfixInfo } from "@/app/ts/Mapfix";
|
import { MapfixInfo } from "@/app/ts/Mapfix";
|
||||||
|
import { getGameName } from "@/app/utils/games";
|
||||||
|
import { useState } from "react";
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import { Status, StatusMatches } from "@/app/ts/Status";
|
||||||
|
|
||||||
// Define a field configuration for specific types
|
// Define a field configuration for specific types
|
||||||
interface FieldConfig {
|
interface FieldConfig {
|
||||||
@@ -16,12 +23,24 @@ type ReviewItemType = SubmissionInfo | MapfixInfo;
|
|||||||
interface ReviewItemProps {
|
interface ReviewItemProps {
|
||||||
item: ReviewItemType;
|
item: ReviewItemType;
|
||||||
handleCopyValue: (value: string) => void;
|
handleCopyValue: (value: string) => void;
|
||||||
|
currentUserId?: number;
|
||||||
|
userId?: number | null;
|
||||||
|
onDescriptionUpdate?: () => Promise<void>;
|
||||||
|
showSnackbar?: (message: string, severity?: 'success' | 'error' | 'info' | 'warning') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReviewItem({
|
export function ReviewItem({
|
||||||
item,
|
item,
|
||||||
handleCopyValue
|
handleCopyValue,
|
||||||
|
currentUserId,
|
||||||
|
userId,
|
||||||
|
onDescriptionUpdate,
|
||||||
|
showSnackbar
|
||||||
}: ReviewItemProps) {
|
}: ReviewItemProps) {
|
||||||
|
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
||||||
|
const [editedDescription, setEditedDescription] = useState("");
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
// Type guard to check if item is valid
|
// Type guard to check if item is valid
|
||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
|
|
||||||
@@ -29,6 +48,57 @@ export function ReviewItem({
|
|||||||
const isSubmission = 'UploadedAssetID' in item;
|
const isSubmission = 'UploadedAssetID' in item;
|
||||||
const isMapfix = 'TargetAssetID' in item;
|
const isMapfix = 'TargetAssetID' in item;
|
||||||
|
|
||||||
|
// Check if current user is the submitter
|
||||||
|
const isSubmitter = userId !== null && userId === item.Submitter;
|
||||||
|
|
||||||
|
// Check if description can be edited (only in ChangesRequested or UnderConstruction status)
|
||||||
|
const canEditDescription = isSubmitter && isMapfix && StatusMatches(item.StatusID, [Status.ChangesRequested, Status.UnderConstruction]);
|
||||||
|
|
||||||
|
const handleEditClick = () => {
|
||||||
|
setEditedDescription(isMapfix ? (item.Description || "") : "");
|
||||||
|
setIsEditingDescription(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelEdit = () => {
|
||||||
|
setIsEditingDescription(false);
|
||||||
|
setEditedDescription("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveDescription = async () => {
|
||||||
|
if (!isMapfix) return;
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/v1/mapfixes/${item.ID}/description`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
},
|
||||||
|
body: editedDescription,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to update description: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsEditingDescription(false);
|
||||||
|
if (showSnackbar) {
|
||||||
|
showSnackbar("Description updated successfully", "success");
|
||||||
|
}
|
||||||
|
if (onDescriptionUpdate) {
|
||||||
|
await onDescriptionUpdate();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating description:", error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Failed to update description";
|
||||||
|
if (showSnackbar) {
|
||||||
|
showSnackbar(errorMessage, "error");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Define static fields based on item type
|
// Define static fields based on item type
|
||||||
let fields: FieldConfig[] = [];
|
let fields: FieldConfig[] = [];
|
||||||
if (isSubmission) {
|
if (isSubmission) {
|
||||||
@@ -46,17 +116,18 @@ export function ReviewItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4 }}>
|
<>
|
||||||
<ReviewItemHeader
|
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4 }}>
|
||||||
displayName={item.DisplayName}
|
<ReviewItemHeader
|
||||||
assetId={isMapfix ? item.TargetAssetID : undefined}
|
displayName={item.DisplayName}
|
||||||
statusId={item.StatusID}
|
assetId={isMapfix ? item.TargetAssetID : undefined}
|
||||||
creator={item.Creator}
|
statusId={item.StatusID}
|
||||||
submitterId={item.Submitter}
|
creator={item.Creator}
|
||||||
/>
|
submitterId={item.Submitter}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Item Details */}
|
{/* Item Details */}
|
||||||
<Grid container spacing={2} sx={{ mt: 2 }}>
|
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||||
{fields.map((field) => {
|
{fields.map((field) => {
|
||||||
const fieldValue = (item as never)[field.key];
|
const fieldValue = (item as never)[field.key];
|
||||||
const displayValue = fieldValue === 0 || fieldValue == null ? 'N/A' : fieldValue;
|
const displayValue = fieldValue === 0 || fieldValue == null ? 'N/A' : fieldValue;
|
||||||
@@ -74,19 +145,83 @@ export function ReviewItem({
|
|||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Grid>
|
<Grid size={{ xs: 12, sm: 6}}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
{/* Description Section */}
|
Game
|
||||||
{isMapfix && item.Description && (
|
|
||||||
<div style={{ marginTop: 24 }}>
|
|
||||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
|
||||||
Description
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
{item.Description}
|
{getGameName(item.GameID)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</Grid>
|
||||||
)}
|
</Grid>
|
||||||
</Paper>
|
|
||||||
|
{/* Description Section */}
|
||||||
|
{isMapfix && (
|
||||||
|
<div style={{ marginTop: 24 }}>
|
||||||
|
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold">
|
||||||
|
Description
|
||||||
|
</Typography>
|
||||||
|
{canEditDescription && !isEditingDescription && (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={handleEditClick}
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
aria-label="edit description"
|
||||||
|
>
|
||||||
|
<EditIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{isEditingDescription ? (
|
||||||
|
<Box>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
value={editedDescription}
|
||||||
|
onChange={(e) => setEditedDescription(e.target.value)}
|
||||||
|
placeholder="Describe the changes made in this mapfix"
|
||||||
|
slotProps={{ htmlInput: { maxLength: 256 } }}
|
||||||
|
helperText={`${editedDescription.length}/256 characters`}
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
<Box display="flex" gap={1} mt={1}>
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={handleSaveDescription}
|
||||||
|
disabled={isSaving}
|
||||||
|
aria-label="save description"
|
||||||
|
>
|
||||||
|
<SaveIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleCancelEdit}
|
||||||
|
disabled={isSaving}
|
||||||
|
aria-label="cancel edit"
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body1">
|
||||||
|
{item.Description || "No description provided"}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Workflow Progress Indicator */}
|
||||||
|
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4, display: { xs: 'none', md: 'block' } }}>
|
||||||
|
<WorkflowStepper
|
||||||
|
currentStatus={item.StatusID}
|
||||||
|
type={isMapfix ? 'mapfix' : 'submission'}
|
||||||
|
submitterId={item.Submitter}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,44 @@
|
|||||||
import {Typography, Box, Avatar, keyframes} from "@mui/material";
|
import {Typography, Box, Avatar, keyframes, Skeleton} from "@mui/material";
|
||||||
import { StatusChip } from "@/app/_components/statusChip";
|
import { StatusChip } from "@/app/_components/statusChip";
|
||||||
import { SubmissionStatus } from "@/app/ts/Submission";
|
import { SubmissionStatus } from "@/app/ts/Submission";
|
||||||
import { MapfixStatus } from "@/app/ts/Mapfix";
|
import { MapfixStatus } from "@/app/ts/Mapfix";
|
||||||
import {Status, StatusMatches} from "@/app/ts/Status";
|
import {Status, StatusMatches} from "@/app/ts/Status";
|
||||||
import { useState, useEffect } from "react";
|
import { Link } from "react-router-dom";
|
||||||
import Link from "next/link";
|
|
||||||
import LaunchIcon from '@mui/icons-material/Launch';
|
import LaunchIcon from '@mui/icons-material/Launch';
|
||||||
|
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
|
||||||
|
import { useUsername } from "@/app/hooks/useUsername";
|
||||||
|
|
||||||
function SubmitterName({ submitterId }: { submitterId: number }) {
|
function SubmitterName({ submitterId }: { submitterId: number }) {
|
||||||
const [name, setName] = useState<string | null>(null);
|
const { username, isLoading } = useUsername(submitterId);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const displayName = username ? `@${username}` : String(submitterId);
|
||||||
if (!submitterId) return;
|
|
||||||
const fetchUserName = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const response = await fetch(`/proxy/users/${submitterId}`);
|
|
||||||
if (!response.ok) throw new Error('Failed to fetch user');
|
|
||||||
const data = await response.json();
|
|
||||||
setName(`@${data.name}`);
|
|
||||||
} catch {
|
|
||||||
setName(String(submitterId));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchUserName();
|
|
||||||
}, [submitterId]);
|
|
||||||
|
|
||||||
if (loading) return <Typography variant="body1">Loading...</Typography>;
|
return <a href={`https://www.roblox.com/users/${submitterId}/profile`} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
return <Link href={`https://www.roblox.com/users/${submitterId}/profile`} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none', color: 'inherit' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, '&:hover': { textDecoration: 'underline' }, position: 'relative' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, '&:hover': { textDecoration: 'underline' } }}>
|
<Skeleton
|
||||||
<Typography>
|
variant="text"
|
||||||
{name || submitterId}
|
width={80}
|
||||||
</Typography>
|
sx={{
|
||||||
<LaunchIcon sx={{ fontSize: '1rem', color: 'text.secondary' }} />
|
opacity: isLoading ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
position: isLoading ? 'relative' : 'absolute',
|
||||||
|
}}
|
||||||
|
animation="wave"
|
||||||
|
/>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
opacity: isLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}>
|
||||||
|
<Typography>
|
||||||
|
{displayName}
|
||||||
|
</Typography>
|
||||||
|
<LaunchIcon sx={{ fontSize: '1rem', color: 'text.secondary' }} />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Link>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReviewItemHeaderProps {
|
interface ReviewItemHeaderProps {
|
||||||
@@ -49,7 +50,8 @@ interface ReviewItemHeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, submitterId }: ReviewItemHeaderProps) => {
|
export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, submitterId }: ReviewItemHeaderProps) => {
|
||||||
const isProcessing = StatusMatches(statusId, [Status.Validating, Status.Uploading, Status.Submitting]);
|
const isProcessing = StatusMatches(statusId, [Status.Validating, Status.Uploading, Status.Submitting, Status.Releasing]);
|
||||||
|
const { thumbnailUrl, isLoading } = useUserThumbnail(submitterId, '150x150');
|
||||||
const pulse = keyframes`
|
const pulse = keyframes`
|
||||||
0%, 100% { opacity: 0.2; transform: scale(0.8); }
|
0%, 100% { opacity: 0.2; transform: scale(0.8); }
|
||||||
50% { opacity: 1; transform: scale(1); }
|
50% { opacity: 1; transform: scale(1); }
|
||||||
@@ -59,7 +61,7 @@ export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, subm
|
|||||||
<>
|
<>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||||
{assetId != null ? (
|
{assetId != null ? (
|
||||||
<Link href={`/maps/${assetId}`} passHref legacyBehavior>
|
<Link to={`/maps/${assetId}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} title="View related map">
|
<Box sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} title="View related map">
|
||||||
<Typography
|
<Typography
|
||||||
variant="h4"
|
variant="h4"
|
||||||
@@ -111,10 +113,28 @@ export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, subm
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||||
<Avatar
|
<Box sx={{ position: 'relative', mr: 1, width: 24, height: 24 }}>
|
||||||
src={`/thumbnails/user/${submitterId}`}
|
<Skeleton
|
||||||
sx={{ mr: 1, width: 24, height: 24 }}
|
variant="circular"
|
||||||
/>
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
opacity: isLoading ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
animation="wave"
|
||||||
|
/>
|
||||||
|
<Avatar
|
||||||
|
src={thumbnailUrl || undefined}
|
||||||
|
sx={{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
opacity: isLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
<SubmitterName submitterId={submitterId} />
|
<SubmitterName submitterId={submitterId} />
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
|||||||
315
web/src/app/_components/review/WorkflowStepper.tsx
Normal file
315
web/src/app/_components/review/WorkflowStepper.tsx
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stepper, Step, StepLabel, Box, StepConnector, stepConnectorClasses, StepIconProps, styled, keyframes, Typography, Paper } from '@mui/material';
|
||||||
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import PendingIcon from '@mui/icons-material/Pending';
|
||||||
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
|
import { Status } from '@/app/ts/Status';
|
||||||
|
|
||||||
|
const pulse = keyframes`
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface WorkflowStepperProps {
|
||||||
|
currentStatus: number;
|
||||||
|
type: 'submission' | 'mapfix';
|
||||||
|
submitterId?: number;
|
||||||
|
currentUserId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the workflow steps
|
||||||
|
interface WorkflowStep {
|
||||||
|
label: string;
|
||||||
|
statuses: number[];
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transitional states that show as "in progress"
|
||||||
|
const transitionalStates = [
|
||||||
|
Status.Submitting,
|
||||||
|
Status.Validating,
|
||||||
|
Status.Uploading,
|
||||||
|
Status.Releasing
|
||||||
|
];
|
||||||
|
|
||||||
|
const submissionWorkflow: WorkflowStep[] = [
|
||||||
|
{
|
||||||
|
label: 'Draft',
|
||||||
|
statuses: [Status.UnderConstruction, Status.ChangesRequested],
|
||||||
|
description: 'Creating or revising'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Submitted',
|
||||||
|
statuses: [Status.Submitting, Status.Submitted],
|
||||||
|
description: 'Awaiting review'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accepted',
|
||||||
|
statuses: [Status.AcceptedUnvalidated],
|
||||||
|
description: 'Script review pending'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Validated',
|
||||||
|
statuses: [Status.Validating, Status.Validated],
|
||||||
|
description: 'Scripts approved'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Uploaded',
|
||||||
|
statuses: [Status.Uploading, Status.Uploaded],
|
||||||
|
description: 'Published to Roblox group'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Released',
|
||||||
|
statuses: [Status.Releasing, Status.Release],
|
||||||
|
description: 'Live in-game'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const mapfixWorkflow: WorkflowStep[] = [
|
||||||
|
{
|
||||||
|
label: 'Draft',
|
||||||
|
statuses: [Status.UnderConstruction, Status.ChangesRequested],
|
||||||
|
description: 'Creating or revising'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Submitted',
|
||||||
|
statuses: [Status.Submitting, Status.Submitted],
|
||||||
|
description: 'Awaiting review'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accepted',
|
||||||
|
statuses: [Status.AcceptedUnvalidated],
|
||||||
|
description: 'Script review pending'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Validated',
|
||||||
|
statuses: [Status.Validating, Status.Validated],
|
||||||
|
description: 'Scripts approved'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Uploaded',
|
||||||
|
statuses: [Status.Uploading, Status.Uploaded],
|
||||||
|
description: 'Published to Roblox group'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Released',
|
||||||
|
statuses: [Status.Releasing, Status.Release],
|
||||||
|
description: 'Live in-game'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const CustomConnector = styled(StepConnector)(({ theme }) => ({
|
||||||
|
[`&.${stepConnectorClasses.alternativeLabel}`]: {
|
||||||
|
top: 10,
|
||||||
|
left: 'calc(-50% + 16px)',
|
||||||
|
right: 'calc(50% + 16px)',
|
||||||
|
},
|
||||||
|
[`&.${stepConnectorClasses.active}`]: {
|
||||||
|
[`& .${stepConnectorClasses.line}`]: {
|
||||||
|
borderColor: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[`&.${stepConnectorClasses.completed}`]: {
|
||||||
|
[`& .${stepConnectorClasses.line}`]: {
|
||||||
|
borderColor: theme.palette.success.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[`& .${stepConnectorClasses.line}`]: {
|
||||||
|
borderColor: theme.palette.mode === 'dark' ? theme.palette.grey[800] : '#eaeaf0',
|
||||||
|
borderTopWidth: 3,
|
||||||
|
borderRadius: 1,
|
||||||
|
transition: 'border-color 0.4s ease-in-out',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const CustomStepIcon = (props: StepIconProps & { isRejected?: boolean; isChangesRequested?: boolean }) => {
|
||||||
|
const { active, completed, className, isRejected, isChangesRequested } = props;
|
||||||
|
|
||||||
|
const iconStyle = {
|
||||||
|
transition: 'color 0.4s ease-in-out, opacity 0.3s ease-in-out, transform 0.3s ease-in-out',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isRejected) {
|
||||||
|
return <CancelIcon className={className} sx={{ ...iconStyle, color: 'error.main' }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completed) {
|
||||||
|
return <CheckCircleIcon className={className} sx={{ ...iconStyle, color: 'success.main' }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active && isChangesRequested) {
|
||||||
|
return <WarningIcon className={className} sx={{ ...iconStyle, color: 'warning.main' }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
return <PendingIcon className={className} sx={{ ...iconStyle, color: 'primary.main', animation: `${pulse} 2s ease-in-out infinite` }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={className}
|
||||||
|
sx={{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: '50%',
|
||||||
|
border: 2,
|
||||||
|
borderColor: 'grey.400',
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
transition: 'all 0.4s ease-in-out',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkflowStepper: React.FC<WorkflowStepperProps> = ({ currentStatus, type, submitterId, currentUserId }) => {
|
||||||
|
const workflow = type === 'mapfix' ? mapfixWorkflow : submissionWorkflow;
|
||||||
|
|
||||||
|
// Check if rejected or released
|
||||||
|
const isRejected = currentStatus === Status.Rejected;
|
||||||
|
const isReleased = currentStatus === Status.Release || currentStatus === Status.Releasing;
|
||||||
|
const isChangesRequested = currentStatus === Status.ChangesRequested;
|
||||||
|
const isUnderConstruction = currentStatus === Status.UnderConstruction;
|
||||||
|
|
||||||
|
// Find the active step
|
||||||
|
const activeStep = workflow.findIndex(step =>
|
||||||
|
step.statuses.includes(currentStatus)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine nudge message
|
||||||
|
const getNudgeContent = () => {
|
||||||
|
if (isUnderConstruction) {
|
||||||
|
return {
|
||||||
|
icon: InfoOutlinedIcon,
|
||||||
|
title: 'Not Yet Submitted',
|
||||||
|
message: 'Your submission has been created but has not been submitted. Click "Submit" to submit it.',
|
||||||
|
color: '#2196f3',
|
||||||
|
bgColor: 'rgba(33, 150, 243, 0.08)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isChangesRequested) {
|
||||||
|
return {
|
||||||
|
icon: WarningIcon,
|
||||||
|
title: 'Changes Requested',
|
||||||
|
message: 'Review comments and audit events, make modifications, and submit again.',
|
||||||
|
color: '#ff9800',
|
||||||
|
bgColor: 'rgba(255, 152, 0, 0.08)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nudge = getNudgeContent();
|
||||||
|
|
||||||
|
// Only show nudge if current user is the submitter
|
||||||
|
const isSubmitter = submitterId !== undefined && currentUserId !== undefined && submitterId === currentUserId;
|
||||||
|
const shouldShowNudge = nudge && isSubmitter;
|
||||||
|
|
||||||
|
// If rejected, show all steps as incomplete with error state
|
||||||
|
if (isRejected) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '100%' }}>
|
||||||
|
<Stepper activeStep={-1} alternativeLabel connector={<CustomConnector />}>
|
||||||
|
{workflow.map((step) => (
|
||||||
|
<Step key={step.label} completed={false}>
|
||||||
|
<StepLabel
|
||||||
|
StepIconComponent={(props) => <CustomStepIcon {...props} isRejected={true} />}
|
||||||
|
error={true}
|
||||||
|
>
|
||||||
|
<Box sx={{ fontSize: '0.875rem', fontWeight: 500 }}>
|
||||||
|
{step.label}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ fontSize: '0.75rem', color: 'error.main', mt: 0.5 }}>
|
||||||
|
Rejected
|
||||||
|
</Box>
|
||||||
|
</StepLabel>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '100%' }}>
|
||||||
|
<Stepper activeStep={activeStep} alternativeLabel connector={<CustomConnector />}>
|
||||||
|
{workflow.map((step, index) => {
|
||||||
|
const stepIncludesCurrentStatus = index === activeStep;
|
||||||
|
const isTransitional = transitionalStates.includes(currentStatus);
|
||||||
|
|
||||||
|
// Show as active if in a transitional state OR if changes requested
|
||||||
|
const isActive = stepIncludesCurrentStatus && (isTransitional || isChangesRequested);
|
||||||
|
|
||||||
|
const isCompleted = isReleased
|
||||||
|
? true
|
||||||
|
: index < activeStep || (stepIncludesCurrentStatus && !isTransitional && !isChangesRequested);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Step key={step.label} completed={isCompleted}>
|
||||||
|
<StepLabel
|
||||||
|
StepIconComponent={(props) => <CustomStepIcon {...props} isChangesRequested={stepIncludesCurrentStatus && isChangesRequested} />}
|
||||||
|
>
|
||||||
|
<Box sx={{
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: isReleased ? 500 : (isActive ? 600 : 500),
|
||||||
|
transition: 'all 0.4s ease-in-out'
|
||||||
|
}}>
|
||||||
|
{step.label}
|
||||||
|
</Box>
|
||||||
|
{step.description && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
color: isReleased ? 'text.secondary' : (stepIncludesCurrentStatus && isChangesRequested ? 'warning.main' : (isActive ? 'primary.main' : 'text.secondary')),
|
||||||
|
mt: 0.5,
|
||||||
|
transition: 'color 0.4s ease-in-out'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{step.description}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</StepLabel>
|
||||||
|
</Step>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stepper>
|
||||||
|
|
||||||
|
{/* Action Nudge */}
|
||||||
|
{shouldShowNudge && (
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
mt: 3,
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 2,
|
||||||
|
borderLeft: 4,
|
||||||
|
borderColor: nudge.color,
|
||||||
|
backgroundColor: nudge.bgColor,
|
||||||
|
display: 'flex',
|
||||||
|
gap: 1.5,
|
||||||
|
alignItems: 'flex-start'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ color: nudge.color, display: 'flex', alignItems: 'center', pt: 0.25 }}>
|
||||||
|
<nudge.icon sx={{ fontSize: 24 }} />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Typography variant="body2" fontWeight={600} sx={{ color: nudge.color, mb: 0.5 }}>
|
||||||
|
{nudge.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: 'text.secondary', fontSize: '0.875rem' }}>
|
||||||
|
{nudge.message}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkflowStepper;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, {JSX} from "react";
|
import {JSX} from "react";
|
||||||
import {Cancel, CheckCircle, Pending} from "@mui/icons-material";
|
import {Cancel, CheckCircle, Pending} from "@mui/icons-material";
|
||||||
import {Chip} from "@mui/material";
|
import {Chip} from "@mui/material";
|
||||||
|
|
||||||
export const StatusChip = ({status}: { status: number }) => {
|
export const StatusChip = ({status}: { status: number }): JSX.Element => {
|
||||||
let color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default';
|
let color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default';
|
||||||
let icon: JSX.Element = <Pending fontSize="small"/>;
|
let icon: JSX.Element = <Pending fontSize="small"/>;
|
||||||
let label: string = 'Unknown';
|
let label: string = 'Unknown';
|
||||||
@@ -81,12 +81,6 @@ export const StatusChip = ({status}: { status: number }) => {
|
|||||||
label={label}
|
label={label}
|
||||||
color={color}
|
color={color}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
|
||||||
height: 24,
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import Header from "./header";
|
import Header from "./header";
|
||||||
|
|
||||||
export default function Webpage({children}: Readonly<{children?: React.ReactNode}>) {
|
export default function Webpage({children}: Readonly<{children?: React.ReactNode}>) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use "../../globals.scss";
|
@use "../globals.scss";
|
||||||
|
|
||||||
::placeholder {
|
::placeholder {
|
||||||
color: var(--placeholder-text)
|
color: var(--placeholder-text)
|
||||||
@@ -47,8 +47,4 @@ header h1 {
|
|||||||
form {
|
form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 25px;
|
gap: 25px;
|
||||||
|
|
||||||
fieldset {
|
|
||||||
border: blue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Button, TextField } from "@mui/material"
|
import { Button, TextField } from "@mui/material"
|
||||||
|
|
||||||
import GameSelection from "./_game";
|
import GameSelection from "./_game";
|
||||||
@@ -8,7 +6,7 @@ import Webpage from "@/app/_components/webpage"
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import {useTitle} from "@/app/hooks/useTitle";
|
import {useTitle} from "@/app/hooks/useTitle";
|
||||||
|
|
||||||
import "./(styles)/page.scss"
|
import "./page.scss"
|
||||||
|
|
||||||
interface SubmissionPayload {
|
interface SubmissionPayload {
|
||||||
AssetID: number;
|
AssetID: number;
|
||||||
@@ -43,7 +41,7 @@ export default function SubmissionInfoPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Send the POST request
|
// Send the POST request
|
||||||
const response = await fetch("/api/submissions-admin", {
|
const response = await fetch("/v1/submissions-admin", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
|
|||||||
@@ -8,39 +8,23 @@ $form-label-fontsize: 1.3rem;
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: dark;
|
||||||
|
|
||||||
--header-height: 45px;
|
--header-height: 45px;
|
||||||
|
|
||||||
--page: white;
|
--page: rgb(15,15,15);
|
||||||
--header-grad-left: #363b40;
|
--header-grad-left: #363b40;
|
||||||
--header-grad-right: #353a40;
|
--header-grad-right: #353a40;
|
||||||
--header-button-left: white;
|
--header-button-left: white;
|
||||||
--header-button-right: #b4b4b4;
|
--header-button-right: #b4b4b4;
|
||||||
--header-button-hover: white;
|
--header-button-hover: white;
|
||||||
--review-border: #c8c8c8;
|
--review-border: rgb(50,50,50);
|
||||||
--text-color: #1e1e1e;
|
--text-color: rgb(230,230,230);
|
||||||
--anchor-link-review: #008fd6;
|
--anchor-link-review: #008fd6;
|
||||||
--window-header: #f5f5f5;
|
--window-header: rgb(10,10,10);
|
||||||
--comment-highlighted: #ffffd7;
|
--comment-highlighted: #ffffd7;
|
||||||
--comment-area: white;
|
--comment-area: rgb(20,20,20);
|
||||||
--placeholder-text: rgb(150,150,150);
|
--placeholder-text: rgb(80,80,80);
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
--page: rgb(15,15,15);
|
|
||||||
--header-grad-left: #363b40;
|
|
||||||
--header-grad-right: #353a40;
|
|
||||||
--header-button-left: white;
|
|
||||||
--header-button-right: #b4b4b4;
|
|
||||||
--header-button-hover: white;
|
|
||||||
--review-border: rgb(50,50,50);
|
|
||||||
--text-color: rgb(230,230,230);
|
|
||||||
--anchor-link-review: #008fd6;
|
|
||||||
--window-header: rgb(10,10,10);
|
|
||||||
--comment-highlighted: #ffffd7;
|
|
||||||
--comment-area: rgb(20,20,20);
|
|
||||||
--placeholder-text: rgb(80,80,80);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [reviewData, auditData] = await Promise.all([
|
const [reviewData, auditData] = await Promise.all([
|
||||||
fetch(`/api/${itemType}/${itemId}`).then(res => {
|
fetch(`/v1/${itemType}/${itemId}`).then(res => {
|
||||||
if (!res.ok) throw new Error(`Failed to fetch ${itemType.slice(0, -1)}: ${res.status}`);
|
if (!res.ok) throw new Error(`Failed to fetch ${itemType.slice(0, -1)}: ${res.status}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
}),
|
}),
|
||||||
fetch(`/api/${itemType}/${itemId}/audit-events?Page=1&Limit=100`).then(res => {
|
fetch(`/v1/${itemType}/${itemId}/audit-events?Page=1&Limit=100`).then(res => {
|
||||||
if (!res.ok) throw new Error(`Failed to fetch audit events: ${res.status}`);
|
if (!res.ok) throw new Error(`Failed to fetch audit events: ${res.status}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
@@ -58,7 +58,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rolesResponse = await fetch("/api/session/roles");
|
const rolesResponse = await fetch("/v1/session/roles");
|
||||||
if (rolesResponse.ok) {
|
if (rolesResponse.ok) {
|
||||||
const rolesData = await rolesResponse.json();
|
const rolesData = await rolesResponse.json();
|
||||||
setRoles(rolesData.Roles);
|
setRoles(rolesData.Roles);
|
||||||
@@ -72,7 +72,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userResponse = await fetch("/api/session/user");
|
const userResponse = await fetch("/v1/session/user");
|
||||||
if (userResponse.ok) {
|
if (userResponse.ok) {
|
||||||
const userData = await userResponse.json();
|
const userData = await userResponse.json();
|
||||||
setUser(userData.UserID);
|
setUser(userData.UserID);
|
||||||
@@ -100,7 +100,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
if (StatusMatches(data.StatusID, [Status.Uploading, Status.Submitting, Status.Validating])) {
|
if (StatusMatches(data.StatusID, [Status.Uploading, Status.Submitting, Status.Validating, Status.Releasing])) {
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
fetchData(true);
|
fetchData(true);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|||||||
216
web/src/app/hooks/useThumbnails.ts
Normal file
216
web/src/app/hooks/useThumbnails.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type ThumbnailSize = '150x150' | '420x420' | '768x432';
|
||||||
|
|
||||||
|
interface ThumbnailBatchResponse {
|
||||||
|
thumbnails: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batching queue
|
||||||
|
class ThumbnailBatcher {
|
||||||
|
private assetQueue: Map<number, Set<(url: string | null) => void>> = new Map();
|
||||||
|
private userQueue: Map<number, Set<(url: string | null) => void>> = new Map();
|
||||||
|
private assetTimeoutId: NodeJS.Timeout | null = null;
|
||||||
|
private userTimeoutId: NodeJS.Timeout | null = null;
|
||||||
|
private batchDelay = 50; // 50ms delay to collect requests
|
||||||
|
|
||||||
|
async fetchAssetBatch(ids: number[], size: ThumbnailSize): Promise<Record<number, string>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/v1/thumbnails/assets', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ assetIds: ids, size }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch thumbnails: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ThumbnailBatchResponse = await response.json();
|
||||||
|
|
||||||
|
// Convert string keys back to numbers
|
||||||
|
const result: Record<number, string> = {};
|
||||||
|
Object.entries(data.thumbnails || {}).forEach(([key, value]) => {
|
||||||
|
result[parseInt(key, 10)] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching asset thumbnails:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchUserBatch(ids: number[], size: ThumbnailSize): Promise<Record<number, string>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/v1/thumbnails/users', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ userIds: ids, size }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch user thumbnails: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ThumbnailBatchResponse = await response.json();
|
||||||
|
|
||||||
|
// Convert string keys back to numbers
|
||||||
|
const result: Record<number, string> = {};
|
||||||
|
Object.entries(data.thumbnails || {}).forEach(([key, value]) => {
|
||||||
|
result[parseInt(key, 10)] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user thumbnails:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queueAssetRequest(id: number, size: ThumbnailSize, callback: (url: string | null) => void) {
|
||||||
|
if (!this.assetQueue.has(id)) {
|
||||||
|
this.assetQueue.set(id, new Set());
|
||||||
|
}
|
||||||
|
this.assetQueue.get(id)!.add(callback);
|
||||||
|
|
||||||
|
if (this.assetTimeoutId) {
|
||||||
|
clearTimeout(this.assetTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.assetTimeoutId = setTimeout(() => {
|
||||||
|
this.flushAssetQueue(size);
|
||||||
|
}, this.batchDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
queueUserRequest(id: number, size: ThumbnailSize, callback: (url: string | null) => void) {
|
||||||
|
if (!this.userQueue.has(id)) {
|
||||||
|
this.userQueue.set(id, new Set());
|
||||||
|
}
|
||||||
|
this.userQueue.get(id)!.add(callback);
|
||||||
|
|
||||||
|
if (this.userTimeoutId) {
|
||||||
|
clearTimeout(this.userTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.userTimeoutId = setTimeout(() => {
|
||||||
|
this.flushUserQueue(size);
|
||||||
|
}, this.batchDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async flushAssetQueue(size: ThumbnailSize) {
|
||||||
|
if (this.assetQueue.size === 0) return;
|
||||||
|
|
||||||
|
const ids = Array.from(this.assetQueue.keys());
|
||||||
|
const callbacks = new Map(this.assetQueue);
|
||||||
|
this.assetQueue.clear();
|
||||||
|
|
||||||
|
// Split into batches of 100 (API limit)
|
||||||
|
for (let i = 0; i < ids.length; i += 100) {
|
||||||
|
const batchIds = ids.slice(i, i + 100);
|
||||||
|
const results = await this.fetchAssetBatch(batchIds, size);
|
||||||
|
|
||||||
|
batchIds.forEach((id) => {
|
||||||
|
const url = results[id] || null;
|
||||||
|
const cbs = callbacks.get(id);
|
||||||
|
if (cbs) {
|
||||||
|
cbs.forEach((cb) => cb(url));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async flushUserQueue(size: ThumbnailSize) {
|
||||||
|
if (this.userQueue.size === 0) return;
|
||||||
|
|
||||||
|
const ids = Array.from(this.userQueue.keys());
|
||||||
|
const callbacks = new Map(this.userQueue);
|
||||||
|
this.userQueue.clear();
|
||||||
|
|
||||||
|
// Split into batches of 100 (API limit)
|
||||||
|
for (let i = 0; i < ids.length; i += 100) {
|
||||||
|
const batchIds = ids.slice(i, i + 100);
|
||||||
|
const results = await this.fetchUserBatch(batchIds, size);
|
||||||
|
|
||||||
|
batchIds.forEach((id) => {
|
||||||
|
const url = results[id] || null;
|
||||||
|
const cbs = callbacks.get(id);
|
||||||
|
if (cbs) {
|
||||||
|
cbs.forEach((cb) => cb(url));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton batcher instance
|
||||||
|
const batcher = new ThumbnailBatcher();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch a single asset thumbnail with automatic batching
|
||||||
|
*/
|
||||||
|
export function useAssetThumbnail(assetId: number | undefined, size: ThumbnailSize = '420x420') {
|
||||||
|
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!assetId) {
|
||||||
|
setThumbnailUrl(null);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
batcher.queueAssetRequest(assetId, size, (url) => {
|
||||||
|
setThumbnailUrl(url);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, [assetId, size]);
|
||||||
|
|
||||||
|
return { thumbnailUrl, isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch a single user avatar thumbnail with automatic batching
|
||||||
|
*/
|
||||||
|
export function useUserThumbnail(userId: number | undefined, size: ThumbnailSize = '150x150') {
|
||||||
|
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userId) {
|
||||||
|
setThumbnailUrl(null);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
batcher.queueUserRequest(userId, size, (url) => {
|
||||||
|
setThumbnailUrl(url);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, [userId, size]);
|
||||||
|
|
||||||
|
return { thumbnailUrl, isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to prefetch multiple thumbnails (useful for lists)
|
||||||
|
* This ensures they're batched together
|
||||||
|
*/
|
||||||
|
export function usePrefetchThumbnails(
|
||||||
|
items: Array<{ assetId?: number; userId?: number }>,
|
||||||
|
assetSize: ThumbnailSize = '420x420',
|
||||||
|
userSize: ThumbnailSize = '150x150'
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item.assetId) {
|
||||||
|
batcher.queueAssetRequest(item.assetId, assetSize, () => {});
|
||||||
|
}
|
||||||
|
if (item.userId) {
|
||||||
|
batcher.queueUserRequest(item.userId, userSize, () => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [items, assetSize, userSize]);
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export function useTitle(title: string) {
|
export function useTitle(title: string) {
|
||||||
|
|||||||
39
web/src/app/hooks/useUser.ts
Normal file
39
web/src/app/hooks/useUser.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { UserInfo } from '@/app/ts/User';
|
||||||
|
|
||||||
|
async function fetchUser(): Promise<UserInfo | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/v1/session/user');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await response.json();
|
||||||
|
|
||||||
|
if (userData && 'UserID' in userData) {
|
||||||
|
return userData as UserInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user data:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUser() {
|
||||||
|
const { data: user, isLoading, error } = useQuery({
|
||||||
|
queryKey: ['user'],
|
||||||
|
queryFn: fetchUser,
|
||||||
|
staleTime: Infinity, // User data won't go stale unless manually invalidated
|
||||||
|
gcTime: Infinity, // Keep in cache indefinitely
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
isLoggedIn: user !== null && user !== undefined,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
103
web/src/app/hooks/useUsername.ts
Normal file
103
web/src/app/hooks/useUsername.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface UserBatchResponse {
|
||||||
|
usernames: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batching queue
|
||||||
|
class UserBatcher {
|
||||||
|
private queue: Map<number, Set<(name: string | null) => void>> = new Map();
|
||||||
|
private timeoutId: NodeJS.Timeout | null = null;
|
||||||
|
private batchDelay = 50; // 50ms delay to collect requests
|
||||||
|
|
||||||
|
async fetchBatch(ids: number[]): Promise<Record<number, string>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/v1/usernames', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ userIds: ids }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch usernames: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: UserBatchResponse = await response.json();
|
||||||
|
|
||||||
|
// Convert string keys back to numbers
|
||||||
|
const result: Record<number, string> = {};
|
||||||
|
Object.entries(data.usernames || {}).forEach(([key, value]) => {
|
||||||
|
result[parseInt(key, 10)] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching usernames:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queueRequest(id: number, callback: (name: string | null) => void) {
|
||||||
|
if (!this.queue.has(id)) {
|
||||||
|
this.queue.set(id, new Set());
|
||||||
|
}
|
||||||
|
this.queue.get(id)!.add(callback);
|
||||||
|
|
||||||
|
if (this.timeoutId) {
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
this.flushQueue();
|
||||||
|
}, this.batchDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async flushQueue() {
|
||||||
|
if (this.queue.size === 0) return;
|
||||||
|
|
||||||
|
const ids = Array.from(this.queue.keys());
|
||||||
|
const callbacks = new Map(this.queue);
|
||||||
|
this.queue.clear();
|
||||||
|
|
||||||
|
// Split into batches of 100 (API limit)
|
||||||
|
for (let i = 0; i < ids.length; i += 100) {
|
||||||
|
const batchIds = ids.slice(i, i + 100);
|
||||||
|
const results = await this.fetchBatch(batchIds);
|
||||||
|
|
||||||
|
batchIds.forEach((id) => {
|
||||||
|
const name = results[id] || null;
|
||||||
|
const cbs = callbacks.get(id);
|
||||||
|
if (cbs) {
|
||||||
|
cbs.forEach((cb) => cb(name));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton batcher instance
|
||||||
|
const batcher = new UserBatcher();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch a single username with automatic batching
|
||||||
|
*/
|
||||||
|
export function useUsername(userId: number | undefined) {
|
||||||
|
const [username, setUsername] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userId) {
|
||||||
|
setUsername(null);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
batcher.queueRequest(userId, (name) => {
|
||||||
|
setUsername(name);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
return { username, isLoading };
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import "./globals.scss";
|
|
||||||
import {theme} from "@/app/lib/theme";
|
|
||||||
import {ThemeProvider} from "@mui/material";
|
|
||||||
|
|
||||||
export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body>
|
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
{children}
|
|
||||||
</ThemeProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
export async function errorImageResponse(
|
|
||||||
statusCode: number = 500,
|
|
||||||
options?: { message?: string }
|
|
||||||
): Promise<NextResponse> {
|
|
||||||
const file = `${statusCode}.png`;
|
|
||||||
const filePath = path.join(process.cwd(), 'public/errors', file);
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': 'image/png',
|
|
||||||
};
|
|
||||||
if (options?.message) {
|
|
||||||
headers['X-Error-Message'] = encodeURIComponent(options.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const buffer = await fs.readFile(filePath);
|
|
||||||
headers['Content-Length'] = buffer.length.toString();
|
|
||||||
return new NextResponse(buffer, {
|
|
||||||
status: statusCode,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
const fallback = path.join(process.cwd(), 'public/errors', '500.png');
|
|
||||||
const buffer = await fs.readFile(fallback);
|
|
||||||
headers['Content-Length'] = buffer.length.toString();
|
|
||||||
return new NextResponse(buffer, {
|
|
||||||
status: 500,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +1,116 @@
|
|||||||
import {createTheme} from "@mui/material";
|
import {createTheme} from "@mui/material";
|
||||||
|
|
||||||
export const theme = createTheme({
|
export const theme = createTheme({
|
||||||
|
cssVariables: {
|
||||||
|
colorSchemeSelector: 'class',
|
||||||
|
},
|
||||||
|
colorSchemes: {
|
||||||
|
dark: true,
|
||||||
|
},
|
||||||
|
defaultColorScheme: 'dark',
|
||||||
palette: {
|
palette: {
|
||||||
mode: 'dark',
|
mode: 'dark',
|
||||||
primary: {
|
primary: {
|
||||||
main: '#90caf9',
|
main: '#3b82f6',
|
||||||
|
dark: '#2563eb',
|
||||||
|
light: '#60a5fa',
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: '#f48fb1',
|
main: '#8b5cf6',
|
||||||
|
dark: '#7c3aed',
|
||||||
|
light: '#a78bfa',
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
default: '#121212',
|
default: '#0a0a0a',
|
||||||
paper: '#1e1e1e',
|
paper: '#171717',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#ffffff',
|
||||||
|
secondary: '#9ca3af',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
main: '#ef4444',
|
||||||
|
light: '#f87171',
|
||||||
|
dark: '#dc2626',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
main: '#f59e0b',
|
||||||
|
light: '#fbbf24',
|
||||||
|
dark: '#d97706',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
main: '#10b981',
|
||||||
|
light: '#34d399',
|
||||||
|
dark: '#059669',
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
main: '#3b82f6',
|
||||||
|
light: '#60a5fa',
|
||||||
|
dark: '#2563eb',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif',
|
||||||
|
h1: {
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: '-0.025em',
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: '-0.015em',
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: '-0.01em',
|
||||||
|
},
|
||||||
h5: {
|
h5: {
|
||||||
fontWeight: 500,
|
fontWeight: 600,
|
||||||
letterSpacing: '0.5px',
|
},
|
||||||
|
h6: {
|
||||||
|
fontWeight: 600,
|
||||||
},
|
},
|
||||||
subtitle1: {
|
subtitle1: {
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
fontSize: '0.95rem',
|
fontSize: '1rem',
|
||||||
|
},
|
||||||
|
body1: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
lineHeight: 1.7,
|
||||||
},
|
},
|
||||||
body2: {
|
body2: {
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
|
lineHeight: 1.6,
|
||||||
},
|
},
|
||||||
caption: {
|
caption: {
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
},
|
},
|
||||||
|
button: {
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: 'none',
|
||||||
|
letterSpacing: '0.01em',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
shape: {
|
shape: {
|
||||||
borderRadius: 8,
|
borderRadius: 12,
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
MuiCard: {
|
MuiCard: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
borderRadius: 8,
|
borderRadius: 12,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
backgroundColor: '#171717',
|
||||||
transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out',
|
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||||
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
|
||||||
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
transform: 'translateY(-4px)',
|
transform: 'translateY(-4px)',
|
||||||
boxShadow: '0 8px 16px rgba(0, 0, 0, 0.2)',
|
border: '1px solid rgba(59, 130, 246, 0.4)',
|
||||||
|
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(59, 130, 246, 0.2)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -52,7 +118,7 @@ export const theme = createTheme({
|
|||||||
MuiCardMedia: {
|
MuiCardMedia: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
transition: 'transform 0.3s',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -69,14 +135,48 @@ export const theme = createTheme({
|
|||||||
MuiChip: {
|
MuiChip: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
fontWeight: 500,
|
fontWeight: 600,
|
||||||
|
borderRadius: 6,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
marginLeft: '8px',
|
||||||
|
},
|
||||||
|
colorError: {
|
||||||
|
backgroundColor: '#ef4444',
|
||||||
|
color: '#ffffff',
|
||||||
|
'& .MuiChip-icon': {
|
||||||
|
color: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
colorWarning: {
|
||||||
|
backgroundColor: '#f59e0b',
|
||||||
|
color: '#ffffff',
|
||||||
|
'& .MuiChip-icon': {
|
||||||
|
color: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
colorSuccess: {
|
||||||
|
backgroundColor: '#10b981',
|
||||||
|
color: '#ffffff',
|
||||||
|
'& .MuiChip-icon': {
|
||||||
|
color: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
colorInfo: {
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
color: '#ffffff',
|
||||||
|
'& .MuiChip-icon': {
|
||||||
|
color: '#ffffff',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MuiDivider: {
|
MuiDivider: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
borderColor: 'rgba(148, 163, 184, 0.1)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -84,6 +184,126 @@ export const theme = createTheme({
|
|||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
backgroundImage: 'none',
|
backgroundImage: 'none',
|
||||||
|
backgroundColor: '#171717',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: 8,
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: 'none',
|
||||||
|
padding: '10px 24px',
|
||||||
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
},
|
||||||
|
contained: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||||
|
transform: 'translateY(-1px)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
containedPrimary: {
|
||||||
|
background: '#3b82f6',
|
||||||
|
'&:hover': {
|
||||||
|
background: '#2563eb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outlined: {
|
||||||
|
borderWidth: '1.5px',
|
||||||
|
'&:hover': {
|
||||||
|
borderWidth: '1.5px',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outlinedPrimary: {
|
||||||
|
borderColor: 'rgba(59, 130, 246, 0.5)',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outlinedSecondary: {
|
||||||
|
borderColor: 'rgba(139, 92, 246, 0.5)',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: '#8b5cf6',
|
||||||
|
backgroundColor: 'rgba(139, 92, 246, 0.08)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiAppBar: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
background: 'rgba(10, 10, 10, 0.8)',
|
||||||
|
backdropFilter: 'blur(12px)',
|
||||||
|
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiDrawer: {
|
||||||
|
styleOverrides: {
|
||||||
|
paper: {
|
||||||
|
backgroundColor: '#0a0a0a',
|
||||||
|
borderRight: '1px solid rgba(255, 255, 255, 0.08)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiCircularProgress: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
color: '#3b82f6',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiIconButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiLink: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
color: '#60a5fa',
|
||||||
|
textDecoration: 'none',
|
||||||
|
transition: 'color 0.2s ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
color: '#3b82f6',
|
||||||
|
textDecoration: 'underline',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiMenu: {
|
||||||
|
styleOverrides: {
|
||||||
|
paper: {
|
||||||
|
background: '#171717',
|
||||||
|
backdropFilter: 'blur(12px)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiMenuItem: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
},
|
||||||
|
'&.Mui-selected': {
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.15)',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.2)',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export const thumbnailLoader = ({ src, width, quality }: { src: string, width: number, quality?: number }) => {
|
|
||||||
return `${src}?w=${width}&q=${quality || 75}`;
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Webpage from "@/app/_components/webpage";
|
import Webpage from "@/app/_components/webpage";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import {useState} from "react";
|
import {useState, useEffect} from "react";
|
||||||
import Link from "next/link";
|
import { Link } from "react-router-dom";
|
||||||
|
import { useAssetThumbnail } from "@/app/hooks/useThumbnails";
|
||||||
|
|
||||||
// MUI Components
|
// MUI Components
|
||||||
import {
|
import {
|
||||||
@@ -36,7 +35,7 @@ interface SnackbarState {
|
|||||||
|
|
||||||
export default function MapfixDetailsPage() {
|
export default function MapfixDetailsPage() {
|
||||||
const { mapfixId } = useParams<{ mapfixId: string }>();
|
const { mapfixId } = useParams<{ mapfixId: string }>();
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const [newComment, setNewComment] = useState("");
|
const [newComment, setNewComment] = useState("");
|
||||||
const [showBeforeImage, setShowBeforeImage] = useState(false);
|
const [showBeforeImage, setShowBeforeImage] = useState(false);
|
||||||
const [snackbar, setSnackbar] = useState<SnackbarState>({
|
const [snackbar, setSnackbar] = useState<SnackbarState>({
|
||||||
@@ -70,16 +69,26 @@ export default function MapfixDetailsPage() {
|
|||||||
refreshData
|
refreshData
|
||||||
} = useReviewData({
|
} = useReviewData({
|
||||||
itemType: 'mapfixes',
|
itemType: 'mapfixes',
|
||||||
itemId: mapfixId
|
itemId: mapfixId!
|
||||||
});
|
});
|
||||||
const mapfix = mapfixData as MapfixInfo;
|
const mapfix = mapfixData as MapfixInfo;
|
||||||
|
|
||||||
useTitle(mapfix ? `${mapfix.DisplayName} Mapfix` : 'Loading Mapfix...');
|
useTitle(mapfix ? `${mapfix.DisplayName} Mapfix` : 'Loading Mapfix...');
|
||||||
|
|
||||||
|
// Use thumbnail hooks for before/after images
|
||||||
|
const { thumbnailUrl: beforeThumbnail, isLoading: beforeLoading } = useAssetThumbnail(
|
||||||
|
mapfix?.TargetAssetID,
|
||||||
|
'420x420'
|
||||||
|
);
|
||||||
|
const { thumbnailUrl: afterThumbnail, isLoading: afterLoading } = useAssetThumbnail(
|
||||||
|
mapfix?.AssetID,
|
||||||
|
'420x420'
|
||||||
|
);
|
||||||
|
|
||||||
// Handle review button actions
|
// Handle review button actions
|
||||||
async function handleReviewAction(action: string, mapfixId: number) {
|
async function handleReviewAction(action: string, mapfixId: number) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/mapfixes/${mapfixId}/status/${action}`, {
|
const response = await fetch(`/v1/mapfixes/${mapfixId}/status/${action}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
@@ -112,13 +121,22 @@ export default function MapfixDetailsPage() {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// cycle before and after images every 2 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setShowBeforeImage((prev) => !prev);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCommentSubmit = async () => {
|
const handleCommentSubmit = async () => {
|
||||||
if (!newComment.trim()) {
|
if (!newComment.trim()) {
|
||||||
return; // Don't submit empty comments
|
return; // Don't submit empty comments
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/mapfixes/${mapfixId}/comment`, {
|
const response = await fetch(`/v1/mapfixes/${mapfixId}/comment`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/plain',
|
'Content-Type': 'text/plain',
|
||||||
@@ -177,7 +195,7 @@ export default function MapfixDetailsPage() {
|
|||||||
title="Error Loading Mapfix"
|
title="Error Loading Mapfix"
|
||||||
message={error || "Mapfix not found"}
|
message={error || "Mapfix not found"}
|
||||||
buttonText="Return to Mapfixes"
|
buttonText="Return to Mapfixes"
|
||||||
onButtonClick={() => router.push('/mapfixes')}
|
onButtonClick={() => navigate('/mapfixes')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -191,10 +209,10 @@ export default function MapfixDetailsPage() {
|
|||||||
aria-label="breadcrumb"
|
aria-label="breadcrumb"
|
||||||
sx={{ mb: 3 }}
|
sx={{ mb: 3 }}
|
||||||
>
|
>
|
||||||
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
<Typography color="text.primary">Home</Typography>
|
<Typography color="text.primary">Home</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/mapfixes" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
<Link to="/mapfixes" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
<Typography color="text.primary">Mapfixes</Typography>
|
<Typography color="text.primary">Mapfixes</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
<Typography color="text.secondary">{mapfix.DisplayName}</Typography>
|
<Typography color="text.secondary">{mapfix.DisplayName}</Typography>
|
||||||
@@ -207,6 +225,22 @@ export default function MapfixDetailsPage() {
|
|||||||
<Box sx={{ position: 'relative', width: '100%', aspectRatio: '1/1' }}>
|
<Box sx={{ position: 'relative', width: '100%', aspectRatio: '1/1' }}>
|
||||||
{/* Before/After Images Container */}
|
{/* Before/After Images Container */}
|
||||||
<Box sx={{ position: 'relative', width: '100%', height: '100%' }}>
|
<Box sx={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||||
|
{/* Loading Skeleton for Before Image */}
|
||||||
|
<Skeleton
|
||||||
|
variant="rectangular"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
zIndex: 1,
|
||||||
|
opacity: beforeLoading && showBeforeImage ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
animation="wave"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Before Image */}
|
{/* Before Image */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -216,18 +250,34 @@ export default function MapfixDetailsPage() {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
opacity: showBeforeImage ? 1 : 0,
|
opacity: showBeforeImage ? (beforeLoading ? 0 : 1) : 0,
|
||||||
transition: 'opacity 0.5s ease-in-out'
|
transition: 'opacity 0.5s ease-in-out'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardMedia
|
<CardMedia
|
||||||
component="img"
|
component="img"
|
||||||
image={`/thumbnails/asset/${mapfix.TargetAssetID}`}
|
image={beforeThumbnail || '/placeholder-map.png'}
|
||||||
alt="Before Map Thumbnail"
|
alt="Before Map Thumbnail"
|
||||||
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Loading Skeleton for After Image */}
|
||||||
|
<Skeleton
|
||||||
|
variant="rectangular"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
zIndex: 0,
|
||||||
|
opacity: afterLoading && !showBeforeImage ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
animation="wave"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* After Image */}
|
{/* After Image */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -237,13 +287,13 @@ export default function MapfixDetailsPage() {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
opacity: showBeforeImage ? 0 : 1,
|
opacity: showBeforeImage ? 0 : (afterLoading ? 0 : 1),
|
||||||
transition: 'opacity 0.5s ease-in-out'
|
transition: 'opacity 0.5s ease-in-out'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardMedia
|
<CardMedia
|
||||||
component="img"
|
component="img"
|
||||||
image={`/thumbnails/asset/${mapfix.AssetID}`}
|
image={afterThumbnail || '/placeholder-map.png'}
|
||||||
alt="After Map Thumbnail"
|
alt="After Map Thumbnail"
|
||||||
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
/>
|
/>
|
||||||
@@ -282,33 +332,6 @@ export default function MapfixDetailsPage() {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 16,
|
|
||||||
left: '50%',
|
|
||||||
transform: 'translateX(-50%)',
|
|
||||||
zIndex: 2,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{
|
|
||||||
color: 'white',
|
|
||||||
bgcolor: 'rgba(0,0,0,0.4)',
|
|
||||||
padding: '2px 8px',
|
|
||||||
borderRadius: 1,
|
|
||||||
backdropFilter: 'blur(2px)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Click to compare
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -322,7 +345,6 @@ export default function MapfixDetailsPage() {
|
|||||||
background: 'linear-gradient(rgba(0,0,0,0.02), rgba(0,0,0,0.05))',
|
background: 'linear-gradient(rgba(0,0,0,0.02), rgba(0,0,0,0.05))',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
onClick={() => setShowBeforeImage(!showBeforeImage)}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -343,6 +365,10 @@ export default function MapfixDetailsPage() {
|
|||||||
<ReviewItem
|
<ReviewItem
|
||||||
item={mapfix}
|
item={mapfix}
|
||||||
handleCopyValue={handleCopyId}
|
handleCopyValue={handleCopyId}
|
||||||
|
currentUserId={user ?? undefined}
|
||||||
|
userId={user}
|
||||||
|
onDescriptionUpdate={() => refreshData(true)}
|
||||||
|
showSnackbar={showSnackbar}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Comments Section */}
|
{/* Comments Section */}
|
||||||
@@ -353,6 +379,7 @@ export default function MapfixDetailsPage() {
|
|||||||
handleCommentSubmit={handleCommentSubmit}
|
handleCommentSubmit={handleCommentSubmit}
|
||||||
validatorUser={validatorUser}
|
validatorUser={validatorUser}
|
||||||
userId={user}
|
userId={user}
|
||||||
|
currentStatus={mapfix.StatusID}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { MapfixList } from "../ts/Mapfix";
|
import { MapfixList } from "../ts/Mapfix";
|
||||||
import { MapCard } from "../_components/mapCard";
|
import { MapCard } from "../_components/mapCard";
|
||||||
@@ -8,12 +6,14 @@ import { ListSortConstants } from "../ts/Sort";
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
CircularProgress,
|
Card,
|
||||||
|
CardContent,
|
||||||
Container,
|
Container,
|
||||||
Pagination,
|
Pagination,
|
||||||
|
Skeleton,
|
||||||
Typography
|
Typography
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import Link from "next/link";
|
import { Link } from "react-router-dom";
|
||||||
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
||||||
import {useTitle} from "@/app/hooks/useTitle";
|
import {useTitle} from "@/app/hooks/useTitle";
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ export default function MapfixInfoPage() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/api/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`,
|
`/v1/mapfixes?Page=${currentPage}&Limit=${cardsPerPage}&Sort=${ListSortConstants.ListSortDateDescending}`,
|
||||||
{ signal: controller.signal }
|
{ signal: controller.signal }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -55,33 +55,38 @@ export default function MapfixInfoPage() {
|
|||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [currentPage]);
|
}, [currentPage]);
|
||||||
|
|
||||||
if (isLoading || !mapfixes) {
|
const skeletonCards = Array.from({ length: cardsPerPage }, (_, i) => i);
|
||||||
|
const totalPages = mapfixes ? Math.ceil(mapfixes.Total / cardsPerPage) : 0;
|
||||||
|
|
||||||
|
if (mapfixes && mapfixes.Total === 0) {
|
||||||
return (
|
return (
|
||||||
<Webpage>
|
<Webpage>
|
||||||
<Container sx={{ height: '100vh', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
<Container sx={{ py: 6 }}>
|
||||||
<Box display="flex" flexDirection="column" alignItems="center">
|
<Typography variant="body1">
|
||||||
<CircularProgress />
|
Mapfixes list is empty.
|
||||||
<Typography variant="body1" sx={{ mt: 2 }}>
|
</Typography>
|
||||||
Loading mapfixes...
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Webpage>
|
</Webpage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalPages = Math.ceil(mapfixes.Total / cardsPerPage);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Webpage>
|
<Webpage>
|
||||||
<Container maxWidth="lg" sx={{ py: 6 }}>
|
<Box sx={{
|
||||||
<Box component="main" sx={{ width: '100%', px: 2 }}>
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
py: 6,
|
||||||
|
px: 2,
|
||||||
|
boxSizing: 'border-box'
|
||||||
|
}}>
|
||||||
|
<Box sx={{ width: '100%', maxWidth: '1200px', minWidth: 0 }}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
separator={<NavigateNextIcon fontSize="small" />}
|
separator={<NavigateNextIcon fontSize="small" />}
|
||||||
aria-label="breadcrumb"
|
aria-label="breadcrumb"
|
||||||
sx={{ mb: 3 }}
|
sx={{ mb: 3 }}
|
||||||
>
|
>
|
||||||
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
<Typography color="text.primary">Home</Typography>
|
<Typography color="text.primary">Home</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
<Typography color="text.secondary">Mapfixes</Typography>
|
<Typography color="text.secondary">Mapfixes</Typography>
|
||||||
@@ -99,26 +104,52 @@ export default function MapfixInfoPage() {
|
|||||||
className="grid"
|
className="grid"
|
||||||
sx={{
|
sx={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
|
gridTemplateColumns: {
|
||||||
|
xs: 'repeat(1, 1fr)',
|
||||||
|
sm: 'repeat(2, 1fr)',
|
||||||
|
md: 'repeat(3, 1fr)',
|
||||||
|
lg: 'repeat(4, 1fr)',
|
||||||
|
},
|
||||||
gap: 3,
|
gap: 3,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
minWidth: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{mapfixes.Mapfixes.map((mapfix) => (
|
{!mapfixes || isLoading ? (
|
||||||
<MapCard
|
skeletonCards.map((i) => (
|
||||||
key={mapfix.ID}
|
<Card key={i} sx={{ height: '100%' }}>
|
||||||
id={mapfix.ID}
|
<Skeleton variant="rectangular" height={180} />
|
||||||
assetId={mapfix.AssetID}
|
<CardContent>
|
||||||
displayName={mapfix.DisplayName}
|
<Skeleton variant="text" width="80%" height={32} sx={{ mb: 1.5 }} />
|
||||||
author={mapfix.Creator}
|
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
|
||||||
authorId={mapfix.Submitter}
|
<Skeleton variant="text" width={80} />
|
||||||
rating={mapfix.StatusID}
|
<Skeleton variant="text" width={100} />
|
||||||
statusID={mapfix.StatusID}
|
</Box>
|
||||||
gameID={mapfix.GameID}
|
<Skeleton variant="text" width="60%" />
|
||||||
created={mapfix.CreatedAt}
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 2 }}>
|
||||||
type="mapfix"
|
<Skeleton variant="circular" width={28} height={28} />
|
||||||
/>
|
<Skeleton variant="text" width={100} />
|
||||||
))}
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
mapfixes.Mapfixes.map((mapfix) => (
|
||||||
|
<MapCard
|
||||||
|
key={mapfix.ID}
|
||||||
|
id={mapfix.ID}
|
||||||
|
assetId={mapfix.AssetID}
|
||||||
|
displayName={mapfix.DisplayName}
|
||||||
|
author={mapfix.Creator}
|
||||||
|
authorId={mapfix.Submitter}
|
||||||
|
rating={mapfix.StatusID}
|
||||||
|
statusID={mapfix.StatusID}
|
||||||
|
gameID={mapfix.GameID}
|
||||||
|
created={mapfix.CreatedAt}
|
||||||
|
type="mapfix"
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
@@ -133,7 +164,7 @@ export default function MapfixInfoPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Box>
|
||||||
</Webpage>
|
</Webpage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -16,10 +14,10 @@ import {
|
|||||||
import SendIcon from '@mui/icons-material/Send';
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
||||||
import Webpage from "@/app/_components/webpage";
|
import Webpage from "@/app/_components/webpage";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||||
import Link from "next/link";
|
|
||||||
import {MapInfo} from "@/app/ts/Map";
|
import {MapInfo} from "@/app/ts/Map";
|
||||||
import {useTitle} from "@/app/hooks/useTitle";
|
import {useTitle} from "@/app/hooks/useTitle";
|
||||||
|
import { getGameName } from "@/app/utils/games";
|
||||||
|
|
||||||
interface MapfixPayload {
|
interface MapfixPayload {
|
||||||
AssetID: number;
|
AssetID: number;
|
||||||
@@ -27,15 +25,9 @@ interface MapfixPayload {
|
|||||||
Description: string;
|
Description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Game ID mapping
|
|
||||||
const gameTypes: Record<number, string> = {
|
|
||||||
1: "Bhop",
|
|
||||||
2: "Surf",
|
|
||||||
5: "Flytrials"
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function MapfixInfoPage() {
|
export default function MapfixInfoPage() {
|
||||||
const { mapId } = useParams<{ mapId: string }>();
|
const { mapId } = useParams<{ mapId: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [mapDetails, setMapDetails] = useState<MapInfo | null>(null);
|
const [mapDetails, setMapDetails] = useState<MapInfo | null>(null);
|
||||||
@@ -48,7 +40,7 @@ export default function MapfixInfoPage() {
|
|||||||
const fetchMapDetails = async () => {
|
const fetchMapDetails = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const response = await fetch(`/api/maps/${mapId}`);
|
const response = await fetch(`/v1/maps/${mapId}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch map details: ${response.statusText}`);
|
throw new Error(`Failed to fetch map details: ${response.statusText}`);
|
||||||
@@ -69,12 +61,6 @@ export default function MapfixInfoPage() {
|
|||||||
}
|
}
|
||||||
}, [mapId]);
|
}, [mapId]);
|
||||||
|
|
||||||
// Get game type from game ID
|
|
||||||
const getGameType = (gameId: number | undefined): string => {
|
|
||||||
if (!gameId) return "Unknown";
|
|
||||||
return gameTypes[gameId] || "Unknown";
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
@@ -106,7 +92,7 @@ export default function MapfixInfoPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/mapfixes", {
|
const response = await fetch("/v1/mapfixes", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
@@ -118,7 +104,7 @@ export default function MapfixInfoPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { OperationID } = await response.json();
|
const { OperationID } = await response.json();
|
||||||
window.location.assign(`/operations/${OperationID}`);
|
navigate(`/operations/${OperationID}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error submitting data:", error);
|
console.error("Error submitting data:", error);
|
||||||
setError(error instanceof Error ? error.message : "An unknown error occurred");
|
setError(error instanceof Error ? error.message : "An unknown error occurred");
|
||||||
@@ -134,14 +120,14 @@ export default function MapfixInfoPage() {
|
|||||||
aria-label="breadcrumb"
|
aria-label="breadcrumb"
|
||||||
sx={{ mb: 3 }}
|
sx={{ mb: 3 }}
|
||||||
>
|
>
|
||||||
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
<Typography color="text.primary">Home</Typography>
|
<Typography color="text.primary">Home</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/maps" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
<Link to="/maps" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
<Typography color="text.primary">Maps</Typography>
|
<Typography color="text.primary">Maps</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
{mapDetails && (
|
{mapDetails && (
|
||||||
<Link href={`/maps/${mapId}`} passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
<Link to={`/maps/${mapId}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
<Typography color="text.primary">{mapDetails.DisplayName}</Typography>
|
<Typography color="text.primary">{mapDetails.DisplayName}</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
@@ -201,7 +187,7 @@ export default function MapfixInfoPage() {
|
|||||||
label="Game Type"
|
label="Game Type"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={getGameType(mapDetails?.GameID)}
|
value={mapDetails?.GameID ? getGameName(mapDetails.GameID) : "Unknown"}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { MapInfo } from "@/app/ts/Map";
|
import { MapInfo } from "@/app/ts/Map";
|
||||||
import Webpage from "@/app/_components/webpage";
|
import Webpage from "@/app/_components/webpage";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import React, { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Link from "next/link";
|
import { Link } from "react-router-dom";
|
||||||
import { Snackbar, Alert } from "@mui/material";
|
import { Snackbar, Alert } from "@mui/material";
|
||||||
import { MapfixStatus, type MapfixInfo } from "@/app/ts/Mapfix";
|
import { MapfixStatus, type MapfixInfo, getMapfixStatusInfo } from "@/app/ts/Mapfix";
|
||||||
import LaunchIcon from '@mui/icons-material/Launch';
|
import LaunchIcon from '@mui/icons-material/Launch';
|
||||||
|
import { useAssetThumbnail } from "@/app/hooks/useThumbnails";
|
||||||
|
import { getGameInfo } from "@/app/utils/games";
|
||||||
|
|
||||||
// MUI Components
|
// MUI Components
|
||||||
import {
|
import {
|
||||||
@@ -24,7 +24,11 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
CardMedia,
|
CardMedia,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
IconButton
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
Pagination
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
||||||
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
|
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
|
||||||
@@ -34,27 +38,39 @@ import BugReportIcon from "@mui/icons-material/BugReport";
|
|||||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||||
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import BuildIcon from '@mui/icons-material/Build';
|
||||||
|
import PendingIcon from '@mui/icons-material/Pending';
|
||||||
import {hasRole, RolesConstants} from "@/app/ts/Roles";
|
import {hasRole, RolesConstants} from "@/app/ts/Roles";
|
||||||
import {useTitle} from "@/app/hooks/useTitle";
|
import {useTitle} from "@/app/hooks/useTitle";
|
||||||
|
|
||||||
export default function MapDetails() {
|
export default function MapDetails() {
|
||||||
const { mapId } = useParams();
|
const { mapId } = useParams();
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const [map, setMap] = useState<MapInfo | null>(null);
|
const [map, setMap] = useState<MapInfo | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [copySuccess, setCopySuccess] = useState(false);
|
const [copySuccess, setCopySuccess] = useState(false);
|
||||||
const [roles, setRoles] = useState(RolesConstants.Empty);
|
const [roles, setRoles] = useState(RolesConstants.Empty);
|
||||||
const [mapfixes, setMapfixes] = useState<MapfixInfo[]>([]);
|
const [mapfixes, setMapfixes] = useState<MapfixInfo[]>([]);
|
||||||
|
const [fixesPage, setFixesPage] = useState(1);
|
||||||
|
|
||||||
useTitle(map ? `${map.DisplayName}` : 'Loading Map...');
|
useTitle(map ? `${map.DisplayName}` : 'Loading Map...');
|
||||||
|
|
||||||
|
// Use thumbnail hook for the map preview image
|
||||||
|
const { thumbnailUrl, isLoading: thumbnailLoading } = useAssetThumbnail(
|
||||||
|
map?.ID,
|
||||||
|
'768x432'
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getMap() {
|
async function getMap() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const res = await fetch(`/api/maps/${mapId}`);
|
const res = await fetch(`/v1/maps/${mapId}`);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`Failed to fetch map: ${res.status}`);
|
throw new Error(`Failed to fetch map: ${res.status}`);
|
||||||
}
|
}
|
||||||
@@ -73,7 +89,7 @@ export default function MapDetails() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getRoles() {
|
async function getRoles() {
|
||||||
try {
|
try {
|
||||||
const rolesResponse = await fetch("/api/session/roles");
|
const rolesResponse = await fetch("/v1/session/roles");
|
||||||
if (rolesResponse.ok) {
|
if (rolesResponse.ok) {
|
||||||
const rolesData = await rolesResponse.json();
|
const rolesData = await rolesResponse.json();
|
||||||
setRoles(rolesData.Roles);
|
setRoles(rolesData.Roles);
|
||||||
@@ -99,16 +115,15 @@ export default function MapDetails() {
|
|||||||
let allMapfixes: MapfixInfo[] = [];
|
let allMapfixes: MapfixInfo[] = [];
|
||||||
let total = 0;
|
let total = 0;
|
||||||
do {
|
do {
|
||||||
const res = await fetch(`/api/mapfixes?Page=${page}&Limit=${limit}&TargetAssetID=${targetAssetId}`);
|
const res = await fetch(`/v1/mapfixes?Page=${page}&Limit=${limit}&TargetAssetID=${targetAssetId}`);
|
||||||
if (!res.ok) break;
|
if (!res.ok) break;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (page === 1) total = data.Total;
|
if (page === 1) total = data.Total;
|
||||||
allMapfixes = allMapfixes.concat(data.Mapfixes);
|
allMapfixes = allMapfixes.concat(data.Mapfixes);
|
||||||
page++;
|
page++;
|
||||||
} while (allMapfixes.length < total);
|
} while (allMapfixes.length < total);
|
||||||
// Filter out rejected, uploading, uploaded (StatusID > 7)
|
// Store all mapfixes for history display
|
||||||
const active = allMapfixes.filter((fix: MapfixInfo) => fix.StatusID <= MapfixStatus.Validated);
|
setMapfixes(allMapfixes);
|
||||||
setMapfixes(active);
|
|
||||||
} catch {
|
} catch {
|
||||||
setMapfixes([]);
|
setMapfixes([]);
|
||||||
}
|
}
|
||||||
@@ -124,33 +139,18 @@ export default function MapDetails() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGameInfo = (gameId: number) => {
|
const getStatusIcon = (iconName: string) => {
|
||||||
switch (gameId) {
|
switch (iconName) {
|
||||||
case 1:
|
case "Build": return BuildIcon;
|
||||||
return {
|
case "Pending": return PendingIcon;
|
||||||
name: "Bhop",
|
case "CheckCircle": return CheckCircleIcon;
|
||||||
color: "#2196f3" // blue
|
case "Cancel": return CancelIcon;
|
||||||
};
|
default: return PendingIcon;
|
||||||
case 2:
|
|
||||||
return {
|
|
||||||
name: "Surf",
|
|
||||||
color: "#4caf50" // green
|
|
||||||
};
|
|
||||||
case 5:
|
|
||||||
return {
|
|
||||||
name: "Fly Trials",
|
|
||||||
color: "#ff9800" // orange
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
name: "Unknown",
|
|
||||||
color: "#9e9e9e" // gray
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitMapfix = () => {
|
const handleSubmitMapfix = () => {
|
||||||
router.push(`/maps/${mapId}/fix`);
|
navigate(`/maps/${mapId}/fix`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyId = (idToCopy: string) => {
|
const handleCopyId = (idToCopy: string) => {
|
||||||
@@ -180,7 +180,7 @@ export default function MapDetails() {
|
|||||||
<Typography variant="body1">{error}</Typography>
|
<Typography variant="body1">{error}</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => router.push('/maps')}
|
onClick={() => navigate('/maps')}
|
||||||
sx={{ mt: 3 }}
|
sx={{ mt: 3 }}
|
||||||
>
|
>
|
||||||
Return to Maps
|
Return to Maps
|
||||||
@@ -200,10 +200,10 @@ export default function MapDetails() {
|
|||||||
aria-label="breadcrumb"
|
aria-label="breadcrumb"
|
||||||
sx={{ mb: 3 }}
|
sx={{ mb: 3 }}
|
||||||
>
|
>
|
||||||
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
<Typography color="text.primary">Home</Typography>
|
<Typography color="text.primary">Home</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/maps" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
<Link to="/maps" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
<Typography color="text.primary">Maps</Typography>
|
<Typography color="text.primary">Maps</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
<Typography color="text.secondary">{loading ? "Loading..." : map?.DisplayName || "Map Details"}</Typography>
|
<Typography color="text.secondary">{loading ? "Loading..." : map?.DisplayName || "Map Details"}</Typography>
|
||||||
@@ -299,7 +299,7 @@ export default function MapDetails() {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
component="a"
|
component="a"
|
||||||
href={`/api/maps/${mapId}/download`}
|
href={`/v1/maps/${mapId}/download`}
|
||||||
download={`${map?.DisplayName}.rbxm`}
|
download={`${map?.DisplayName}.rbxm`}
|
||||||
sx={{ ml: 1 }}
|
sx={{ ml: 1 }}
|
||||||
>
|
>
|
||||||
@@ -319,19 +319,263 @@ export default function MapDetails() {
|
|||||||
sx={{
|
sx={{
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
position: 'relative'
|
position: 'relative',
|
||||||
|
mb: 3
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardMedia
|
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
|
||||||
component="img"
|
<Skeleton
|
||||||
image={`/thumbnails/asset/${map.ID}`}
|
variant="rectangular"
|
||||||
alt={`Preview of map: ${map.DisplayName}`}
|
height={400}
|
||||||
|
animation="wave"
|
||||||
sx={{
|
sx={{
|
||||||
height: 400,
|
position: 'absolute',
|
||||||
objectFit: 'cover',
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
opacity: thumbnailLoading ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
image={thumbnailUrl || '/placeholder-map.png'}
|
||||||
|
alt={`Preview of map: ${map.DisplayName}`}
|
||||||
|
sx={{
|
||||||
|
height: 400,
|
||||||
|
objectFit: 'cover',
|
||||||
|
opacity: thumbnailLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
{/* Mapfix Section - Active + History */}
|
||||||
|
{mapfixes.length > 0 && (() => {
|
||||||
|
const activeFix = mapfixes.find(fix => fix.StatusID !== MapfixStatus.Rejected && fix.StatusID !== MapfixStatus.Released);
|
||||||
|
const releasedFixes = mapfixes.filter(fix => fix.StatusID === MapfixStatus.Released);
|
||||||
|
const hasContent = activeFix || releasedFixes.length > 0;
|
||||||
|
|
||||||
|
if (!hasContent) return null;
|
||||||
|
|
||||||
|
// Pagination for released fixes
|
||||||
|
const fixesPerPage = 5;
|
||||||
|
const totalPages = Math.ceil(releasedFixes.length / fixesPerPage);
|
||||||
|
const startIndex = (fixesPage - 1) * fixesPerPage;
|
||||||
|
const endIndex = startIndex + fixesPerPage;
|
||||||
|
const paginatedFixes = releasedFixes
|
||||||
|
.sort((a, b) => b.CreatedAt - a.CreatedAt)
|
||||||
|
.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper elevation={3} sx={{ p: 3, borderRadius: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
|
<HistoryIcon sx={{ mr: 1.5, color: 'primary.main', fontSize: 24 }} />
|
||||||
|
<Typography variant="h6" component="h2" sx={{ fontWeight: 'bold' }}>
|
||||||
|
Mapfixes
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
|
||||||
|
<List sx={{ width: '100%' }}>
|
||||||
|
{/* Active Mapfix - shown first with special styling */}
|
||||||
|
{activeFix && (
|
||||||
|
<Box key={activeFix.ID}>
|
||||||
|
<ListItem
|
||||||
|
component={Link}
|
||||||
|
to={`/mapfixes/${activeFix.ID}`}
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
px: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
backgroundColor: 'rgba(25, 118, 210, 0.08)',
|
||||||
|
borderLeft: '4px solid',
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
mb: releasedFixes.length > 0 ? 2 : 0,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(25, 118, 210, 0.12)',
|
||||||
|
transform: 'translateX(4px)'
|
||||||
|
},
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
display: 'block'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||||
|
<ListItemIcon sx={{ minWidth: 36, mt: 0.5 }}>
|
||||||
|
{(() => {
|
||||||
|
const statusInfo = getMapfixStatusInfo(activeFix.StatusID);
|
||||||
|
const StatusIcon = getStatusIcon(statusInfo.iconName);
|
||||||
|
return (
|
||||||
|
<StatusIcon
|
||||||
|
sx={{
|
||||||
|
fontSize: 24,
|
||||||
|
color: statusInfo.color === 'default' ? 'text.secondary' :
|
||||||
|
statusInfo.color === 'error' ? 'error.main' :
|
||||||
|
statusInfo.color === 'warning' ? 'warning.main' :
|
||||||
|
statusInfo.color === 'success' ? 'success.main' :
|
||||||
|
statusInfo.color === 'primary' ? 'primary.main' : 'info.main'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</ListItemIcon>
|
||||||
|
|
||||||
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
component="div"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 'bold',
|
||||||
|
mb: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeFix.Description}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, mb: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
<Chip
|
||||||
|
label="Active"
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
sx={{ fontWeight: 'bold' }}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label={getMapfixStatusInfo(activeFix.StatusID).label}
|
||||||
|
size="small"
|
||||||
|
color={getMapfixStatusInfo(activeFix.StatusID).color as any}
|
||||||
|
sx={{ fontWeight: 'medium' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', color: 'text.secondary' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
|
<PersonIcon sx={{ fontSize: 16 }} />
|
||||||
|
<Typography variant="caption">
|
||||||
|
{activeFix.Creator}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
|
<CalendarTodayIcon sx={{ fontSize: 16 }} />
|
||||||
|
<Typography variant="caption">
|
||||||
|
{formatDate(activeFix.CreatedAt)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<LaunchIcon sx={{ color: 'primary.main', fontSize: 18, mt: 0.5, flexShrink: 0 }} />
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Released Fixes History */}
|
||||||
|
{releasedFixes.length > 0 && (
|
||||||
|
<>
|
||||||
|
{activeFix && (
|
||||||
|
<Box sx={{ mb: 2, mt: 2 }}>
|
||||||
|
<Divider>
|
||||||
|
<Chip label={`${releasedFixes.length} Previous Fix${releasedFixes.length !== 1 ? 'es' : ''}`} size="small" />
|
||||||
|
</Divider>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{paginatedFixes.map((fix, index) => {
|
||||||
|
const statusInfo = getMapfixStatusInfo(fix.StatusID);
|
||||||
|
const StatusIcon = getStatusIcon(statusInfo.iconName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={fix.ID}>
|
||||||
|
<ListItem
|
||||||
|
component={Link}
|
||||||
|
to={`/mapfixes/${fix.ID}`}
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
px: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'action.hover',
|
||||||
|
transform: 'translateX(4px)'
|
||||||
|
},
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
display: 'block'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||||
|
<ListItemIcon sx={{ minWidth: 36, mt: 0.5 }}>
|
||||||
|
<StatusIcon
|
||||||
|
sx={{
|
||||||
|
fontSize: 24,
|
||||||
|
color: 'success.main'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemIcon>
|
||||||
|
|
||||||
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
component="div"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 'bold',
|
||||||
|
mb: 0.5,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fix.Description}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', color: 'text.secondary' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
|
<PersonIcon sx={{ fontSize: 16 }} />
|
||||||
|
<Typography variant="caption">
|
||||||
|
{fix.Creator}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
|
<CalendarTodayIcon sx={{ fontSize: 16 }} />
|
||||||
|
<Typography variant="caption">
|
||||||
|
{formatDate(fix.CreatedAt)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<LaunchIcon sx={{ color: 'primary.main', fontSize: 18, mt: 0.5, flexShrink: 0 }} />
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
{index < paginatedFixes.length - 1 && <Divider sx={{ my: 1 }} />}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
|
||||||
|
<Pagination
|
||||||
|
count={totalPages}
|
||||||
|
page={fixesPage}
|
||||||
|
onChange={(_, page) => setFixesPage(page)}
|
||||||
|
color="primary"
|
||||||
|
size="medium"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Map Details Section */}
|
{/* Map Details Section */}
|
||||||
@@ -376,39 +620,6 @@ export default function MapDetails() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Active Mapfix in Map Details */}
|
|
||||||
{mapfixes.length > 0 && (() => {
|
|
||||||
const active = mapfixes.find(fix => fix.StatusID <= MapfixStatus.Validated);
|
|
||||||
const latest = mapfixes.reduce((a, b) => (a.CreatedAt > b.CreatedAt ? a : b));
|
|
||||||
const showFix = active || latest;
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="subtitle2" color="text.secondary">
|
|
||||||
Active Mapfix
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
component={Link}
|
|
||||||
href={`/mapfixes/${showFix.ID}`}
|
|
||||||
sx={{
|
|
||||||
textDecoration: 'underline',
|
|
||||||
cursor: 'pointer',
|
|
||||||
color: 'primary.main',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 0.5,
|
|
||||||
mt: 0.5
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{showFix.Description}
|
|
||||||
<LaunchIcon sx={{ fontSize: '1rem', ml: 0.5 }} />
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {useState, useEffect} from "react";
|
import {useState, useEffect} from "react";
|
||||||
import Image from "next/image";
|
|
||||||
import {useRouter} from "next/navigation";
|
|
||||||
import Webpage from "@/app/_components/webpage";
|
import Webpage from "@/app/_components/webpage";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -16,18 +12,20 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
Pagination,
|
Pagination,
|
||||||
CircularProgress,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
SelectChangeEvent, Breadcrumbs
|
SelectChangeEvent,
|
||||||
|
Breadcrumbs,
|
||||||
|
Skeleton
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {Search as SearchIcon} from "@mui/icons-material";
|
import {Search as SearchIcon} from "@mui/icons-material";
|
||||||
import Link from "next/link";
|
import { Link } from "react-router-dom";
|
||||||
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
||||||
import {useTitle} from "@/app/hooks/useTitle";
|
import {useTitle} from "@/app/hooks/useTitle";
|
||||||
import {thumbnailLoader} from '@/app/lib/thumbnailLoader';
|
import {usePrefetchThumbnails, useAssetThumbnail} from "@/app/hooks/useThumbnails";
|
||||||
|
import { getGameName, getGameLabelStyles } from "@/app/utils/games";
|
||||||
|
|
||||||
interface Map {
|
interface Map {
|
||||||
ID: number;
|
ID: number;
|
||||||
@@ -37,10 +35,92 @@ interface Map {
|
|||||||
Date: number;
|
Date: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MapCardProps {
|
||||||
|
map: Map;
|
||||||
|
formatDate: (timestamp: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MapCard({ map, formatDate }: MapCardProps) {
|
||||||
|
const { thumbnailUrl, isLoading } = useAssetThumbnail(map.ID, '420x420');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
elevation={1}
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
transition: 'transform 0.2s, box-shadow 0.2s',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'translateY(-4px)',
|
||||||
|
boxShadow: 4,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardActionArea component={Link} to={`/maps/${map.ID}`}>
|
||||||
|
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
|
||||||
|
<Skeleton
|
||||||
|
variant="rectangular"
|
||||||
|
height={180}
|
||||||
|
animation="wave"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
opacity: isLoading ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
image={thumbnailUrl || '/placeholder-map.png'}
|
||||||
|
alt={map.DisplayName}
|
||||||
|
sx={{
|
||||||
|
height: 180,
|
||||||
|
objectFit: 'cover',
|
||||||
|
opacity: isLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={10}
|
||||||
|
right={10}
|
||||||
|
px={1}
|
||||||
|
py={0.5}
|
||||||
|
borderRadius={1}
|
||||||
|
fontSize="0.75rem"
|
||||||
|
fontWeight="bold"
|
||||||
|
sx={{
|
||||||
|
...getGameLabelStyles(map.GameID),
|
||||||
|
opacity: isLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.3s ease-in-out 0.1s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getGameName(map.GameID)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" component="h2" noWrap>
|
||||||
|
{map.DisplayName}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||||
|
By {map.Creator}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Added {formatDate(map.Date)}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function MapsPage() {
|
export default function MapsPage() {
|
||||||
useTitle("Map Collection");
|
useTitle("Map Collection");
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const [maps, setMaps] = useState<Map[]>([]);
|
const [maps, setMaps] = useState<Map[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
@@ -59,7 +139,7 @@ export default function MapsPage() {
|
|||||||
let hasMore = true;
|
let hasMore = true;
|
||||||
|
|
||||||
while (hasMore) {
|
while (hasMore) {
|
||||||
const res = await fetch(`/api/maps?Page=${page}&Limit=${requestPageSize}`);
|
const res = await fetch(`/v1/maps?Page=${page}&Limit=${requestPageSize}`);
|
||||||
const data: Map[] = await res.json();
|
const data: Map[] = await res.json();
|
||||||
allMaps = [...allMaps, ...data];
|
allMaps = [...allMaps, ...data];
|
||||||
hasMore = data.length === requestPageSize;
|
hasMore = data.length === requestPageSize;
|
||||||
@@ -102,15 +182,17 @@ export default function MapsPage() {
|
|||||||
currentPage * mapsPerPage
|
currentPage * mapsPerPage
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Prefetch thumbnails for current page maps to batch them together
|
||||||
|
usePrefetchThumbnails(
|
||||||
|
currentMaps.map(map => ({ assetId: map.ID })),
|
||||||
|
'420x420'
|
||||||
|
);
|
||||||
|
|
||||||
const handlePageChange = (_event: React.ChangeEvent<unknown>, page: number) => {
|
const handlePageChange = (_event: React.ChangeEvent<unknown>, page: number) => {
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
window.scrollTo({top: 0, behavior: 'smooth'});
|
window.scrollTo({top: 0, behavior: 'smooth'});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMapClick = (mapId: number) => {
|
|
||||||
router.push(`/maps/${mapId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (timestamp: number) => {
|
const formatDate = (timestamp: number) => {
|
||||||
return new Date(timestamp * 1000).toLocaleDateString('en-US', {
|
return new Date(timestamp * 1000).toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -119,44 +201,6 @@ export default function MapsPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGameName = (gameId: number) => {
|
|
||||||
switch (gameId) {
|
|
||||||
case 1:
|
|
||||||
return "Bhop";
|
|
||||||
case 2:
|
|
||||||
return "Surf";
|
|
||||||
case 5:
|
|
||||||
return "Fly Trials";
|
|
||||||
default:
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGameLabelStyles = (gameId: number) => {
|
|
||||||
switch (gameId) {
|
|
||||||
case 1: // Bhop
|
|
||||||
return {
|
|
||||||
bgcolor: "info.main",
|
|
||||||
color: "white",
|
|
||||||
};
|
|
||||||
case 2: // Surf
|
|
||||||
return {
|
|
||||||
bgcolor: "success.main",
|
|
||||||
color: "white",
|
|
||||||
};
|
|
||||||
case 5: // Fly Trials
|
|
||||||
return {
|
|
||||||
bgcolor: "warning.main",
|
|
||||||
color: "white",
|
|
||||||
};
|
|
||||||
default: // Unknown
|
|
||||||
return {
|
|
||||||
bgcolor: "grey.500",
|
|
||||||
color: "white",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Webpage>
|
<Webpage>
|
||||||
<Container maxWidth="lg" sx={{py: 6}}>
|
<Container maxWidth="lg" sx={{py: 6}}>
|
||||||
@@ -166,7 +210,7 @@ export default function MapsPage() {
|
|||||||
aria-label="breadcrumb"
|
aria-label="breadcrumb"
|
||||||
sx={{ mb: 3 }}
|
sx={{ mb: 3 }}
|
||||||
>
|
>
|
||||||
<Link href="/" passHref style={{ textDecoration: 'none', color: 'inherit' }}>
|
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
<Typography color="text.primary">Home</Typography>
|
<Typography color="text.primary">Home</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
<Typography color="text.secondary">Maps</Typography>
|
<Typography color="text.secondary">Maps</Typography>
|
||||||
@@ -197,9 +241,27 @@ export default function MapsPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Box display="flex" justifyContent="center" my={8}>
|
<Grid container spacing={3}>
|
||||||
<CircularProgress/>
|
{Array.from({ length: mapsPerPage }).map((_, index) => (
|
||||||
</Box>
|
<Grid size={{ xs: 12, sm: 6, md: 4}} key={index}>
|
||||||
|
<Card
|
||||||
|
elevation={1}
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Skeleton variant="rectangular" height={180} animation="wave" />
|
||||||
|
<CardContent>
|
||||||
|
<Skeleton variant="text" width="80%" height={32} sx={{ mb: 1 }} />
|
||||||
|
<Skeleton variant="text" width="60%" height={20} sx={{ mb: 1 }} />
|
||||||
|
<Skeleton variant="text" width="40%" height={16} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||||
@@ -227,62 +289,10 @@ export default function MapsPage() {
|
|||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{currentMaps.map((map) => (
|
{currentMaps.map((map) => (
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 4}} key={map.ID}>
|
<Grid size={{ xs: 12, sm: 6, md: 4}} key={map.ID}>
|
||||||
<Card
|
<MapCard
|
||||||
elevation={1}
|
map={map}
|
||||||
sx={{
|
formatDate={formatDate}
|
||||||
height: '100%',
|
/>
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
transition: 'transform 0.2s, box-shadow 0.2s',
|
|
||||||
'&:hover': {
|
|
||||||
transform: 'translateY(-4px)',
|
|
||||||
boxShadow: 4,
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CardActionArea onClick={() => handleMapClick(map.ID)}>
|
|
||||||
<CardMedia
|
|
||||||
component="div"
|
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
height: 180,
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
top={10}
|
|
||||||
right={10}
|
|
||||||
px={1}
|
|
||||||
py={0.5}
|
|
||||||
borderRadius={1}
|
|
||||||
fontSize="0.75rem"
|
|
||||||
fontWeight="bold"
|
|
||||||
{...getGameLabelStyles(map.GameID)}
|
|
||||||
>
|
|
||||||
{getGameName(map.GameID)}
|
|
||||||
</Box>
|
|
||||||
<Image
|
|
||||||
loader={thumbnailLoader}
|
|
||||||
src={`/thumbnails/asset/${map.ID}`}
|
|
||||||
alt={map.DisplayName}
|
|
||||||
fill
|
|
||||||
style={{objectFit: 'cover'}}
|
|
||||||
/>
|
|
||||||
</CardMedia>
|
|
||||||
<CardContent>
|
|
||||||
<Typography variant="h6" component="h2" noWrap>
|
|
||||||
{map.DisplayName}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
||||||
By {map.Creator}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Added {formatDate(map.Date)}
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</CardActionArea>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
191
web/src/app/not-found/page.tsx
Normal file
191
web/src/app/not-found/page.tsx
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { Box, Container, Typography, Button } from "@mui/material";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import Webpage from "@/app/_components/webpage";
|
||||||
|
import { useTitle } from "@/app/hooks/useTitle";
|
||||||
|
import HomeIcon from "@mui/icons-material/Home";
|
||||||
|
import MapIcon from "@mui/icons-material/Map";
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
useTitle("404 - Page Not Found");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Webpage>
|
||||||
|
<Box sx={{ width: '100%', bgcolor: 'background.default' }}>
|
||||||
|
{/* 404 Hero Section */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: 'linear-gradient(to bottom, #0a0a0a 0%, #0f0f0f 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Subtle Gradient Background */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '20%',
|
||||||
|
right: '30%',
|
||||||
|
width: '500px',
|
||||||
|
height: '500px',
|
||||||
|
background: 'radial-gradient(circle, rgba(239, 68, 68, 0.1) 0%, transparent 70%)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
filter: 'blur(80px)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '20%',
|
||||||
|
left: '25%',
|
||||||
|
width: '450px',
|
||||||
|
height: '450px',
|
||||||
|
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.08) 0%, transparent 70%)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
filter: 'blur(80px)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Container maxWidth="md" sx={{ position: 'relative', zIndex: 1, py: 8 }}>
|
||||||
|
<Box textAlign="center">
|
||||||
|
{/* 404 Number */}
|
||||||
|
<Typography
|
||||||
|
variant="h1"
|
||||||
|
sx={{
|
||||||
|
fontSize: { xs: '6rem', sm: '8rem', md: '10rem' },
|
||||||
|
fontWeight: 800,
|
||||||
|
lineHeight: 1,
|
||||||
|
mb: 2,
|
||||||
|
letterSpacing: '-0.04em',
|
||||||
|
background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
backgroundClip: 'text',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
404
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Main Message */}
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 700,
|
||||||
|
mb: 2,
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
fontSize: { xs: '2rem', sm: '2.5rem', md: '3rem' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Page Not Found
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Subtext */}
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
color: 'text.secondary',
|
||||||
|
mb: 6,
|
||||||
|
maxWidth: '600px',
|
||||||
|
mx: 'auto',
|
||||||
|
lineHeight: 1.7,
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: { xs: '1rem', md: '1.125rem' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Looks like this page doesn't exist. The page you're looking for might have been removed, renamed, or never existed in the first place.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
gap={2.5}
|
||||||
|
justifyContent="center"
|
||||||
|
flexWrap="wrap"
|
||||||
|
mb={8}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to="/"
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
startIcon={<HomeIcon />}
|
||||||
|
sx={{
|
||||||
|
fontSize: '1rem',
|
||||||
|
px: 4,
|
||||||
|
py: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back to Home
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to="/maps"
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
startIcon={<MapIcon />}
|
||||||
|
sx={{
|
||||||
|
fontSize: '1rem',
|
||||||
|
px: 4,
|
||||||
|
py: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Browse Maps
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Quick Links */}
|
||||||
|
<Box sx={{ mt: 8 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: 'text.secondary',
|
||||||
|
mb: 3,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Quick Links
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 3,
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
{ label: 'Submissions', path: '/submissions' },
|
||||||
|
{ label: 'Map Fixes', path: '/mapfixes' },
|
||||||
|
{ label: 'Submit Map', path: '/submit' },
|
||||||
|
].map((link) => (
|
||||||
|
<Button
|
||||||
|
key={link.path}
|
||||||
|
component={Link}
|
||||||
|
to={link.path}
|
||||||
|
sx={{
|
||||||
|
color: 'text.secondary',
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '1rem',
|
||||||
|
'&:hover': {
|
||||||
|
color: 'primary.main',
|
||||||
|
background: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Webpage>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {useEffect, useState, useRef, ReactElement} from "react";
|
import {useEffect, useState, useRef, ReactElement} from "react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -13,7 +11,11 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Alert,
|
Alert,
|
||||||
Collapse,
|
Collapse,
|
||||||
IconButton
|
IconButton,
|
||||||
|
Fade,
|
||||||
|
Grow,
|
||||||
|
Slide,
|
||||||
|
keyframes
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||||
@@ -23,6 +25,67 @@ import PendingIcon from '@mui/icons-material/Pending';
|
|||||||
import Webpage from "@/app/_components/webpage";
|
import Webpage from "@/app/_components/webpage";
|
||||||
import {useTitle} from "@/app/hooks/useTitle";
|
import {useTitle} from "@/app/hooks/useTitle";
|
||||||
|
|
||||||
|
const pulse = keyframes`
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const spin = keyframes`
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const slideInUp = keyframes`
|
||||||
|
from {
|
||||||
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const successPop = keyframes`
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const errorShake = keyframes`
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
10%, 30%, 50%, 70%, 90% {
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
20%, 40%, 60%, 80% {
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
interface Operation {
|
interface Operation {
|
||||||
OperationID: number;
|
OperationID: number;
|
||||||
Status: number;
|
Status: number;
|
||||||
@@ -33,7 +96,7 @@ interface Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function OperationStatusPage() {
|
export default function OperationStatusPage() {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const { operationId } = useParams();
|
const { operationId } = useParams();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -49,7 +112,7 @@ export default function OperationStatusPage() {
|
|||||||
|
|
||||||
const fetchOperation = async () => {
|
const fetchOperation = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/operations/${operationId}`);
|
const response = await fetch(`/v1/operations/${operationId}`);
|
||||||
|
|
||||||
if (!response.ok) throw new Error("Failed to fetch operation");
|
if (!response.ok) throw new Error("Failed to fetch operation");
|
||||||
|
|
||||||
@@ -72,13 +135,12 @@ export default function OperationStatusPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchOperation();
|
fetchOperation();
|
||||||
if (!intervalRef.current) {
|
intervalRef.current = setInterval(fetchOperation, 1000);
|
||||||
intervalRef.current = setInterval(fetchOperation, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [operationId]);
|
}, [operationId]);
|
||||||
@@ -134,12 +196,27 @@ export default function OperationStatusPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStatusAnimation = (status: number) => {
|
||||||
|
switch (status) {
|
||||||
|
case 0:
|
||||||
|
return pulse;
|
||||||
|
case 1:
|
||||||
|
return successPop;
|
||||||
|
case 2:
|
||||||
|
return errorShake;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Webpage>
|
<Webpage>
|
||||||
<Container maxWidth="md" sx={{ py: 6 }}>
|
<Container maxWidth="md" sx={{ py: 6 }}>
|
||||||
<Typography variant="h4" component="h1" fontWeight="bold" mb={4}>
|
<Fade in timeout={500}>
|
||||||
Operation Status
|
<Typography variant="h4" component="h1" fontWeight="bold" mb={4}>
|
||||||
</Typography>
|
Operation Status
|
||||||
|
</Typography>
|
||||||
|
</Fade>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Box display="flex" flexDirection="column" alignItems="center" my={8}>
|
<Box display="flex" flexDirection="column" alignItems="center" my={8}>
|
||||||
@@ -149,33 +226,47 @@ export default function OperationStatusPage() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<Alert severity="error" sx={{ my: 2 }}>
|
<Slide direction="up" in mountOnEnter unmountOnExit>
|
||||||
<Typography variant="body1">{error}</Typography>
|
<Alert severity="error" sx={{ my: 2 }}>
|
||||||
</Alert>
|
<Typography variant="body1">{error}</Typography>
|
||||||
|
</Alert>
|
||||||
|
</Slide>
|
||||||
) : operation ? (
|
) : operation ? (
|
||||||
<Paper
|
<Grow in timeout={600}>
|
||||||
elevation={3}
|
<Box>
|
||||||
sx={{
|
<Paper
|
||||||
p: 3,
|
elevation={3}
|
||||||
borderRadius: 2,
|
sx={{
|
||||||
border: 1,
|
p: 3,
|
||||||
borderColor: 'divider'
|
borderRadius: 2,
|
||||||
}}
|
border: 1,
|
||||||
>
|
borderColor: 'divider',
|
||||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
animation: `${slideInUp} 0.5s ease-out`
|
||||||
<Typography variant="h5">
|
}}
|
||||||
Operation #{operation.OperationID}
|
>
|
||||||
</Typography>
|
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||||
<Chip
|
<Typography variant="h5">
|
||||||
icon={getStatusIcon(operation.Status)}
|
Operation #{operation.OperationID}
|
||||||
label={getStatusText(operation.Status)}
|
</Typography>
|
||||||
color={getStatusColor(operation.Status) as "success" | "warning" | "error" | "default"}
|
<Chip
|
||||||
variant="filled"
|
icon={getStatusIcon(operation.Status)}
|
||||||
sx={{ fontWeight: 'bold', px: 1 }}
|
label={getStatusText(operation.Status)}
|
||||||
/>
|
color={getStatusColor(operation.Status) as "success" | "warning" | "error" | "default"}
|
||||||
</Box>
|
variant="filled"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 'bold',
|
||||||
|
px: 1,
|
||||||
|
animation: operation.Status === 0
|
||||||
|
? `${pulse} 2s ease-in-out infinite`
|
||||||
|
: `${getStatusAnimation(operation.Status)} 0.5s ease-out`,
|
||||||
|
'& .MuiChip-icon': {
|
||||||
|
animation: operation.Status === 0 ? `${spin} 2s linear infinite` : 'none'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Divider sx={{ my: 2 }} />
|
<Divider sx={{ my: 2 }} />
|
||||||
|
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
<Typography variant="body1" color="text.secondary" gutterBottom>
|
<Typography variant="body1" color="text.secondary" gutterBottom>
|
||||||
@@ -233,24 +324,35 @@ export default function OperationStatusPage() {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{operation.Status === 1 && (
|
{operation.Status === 1 && (
|
||||||
<Box sx={{ mt: 4, textAlign: 'center' }}>
|
<Box sx={{ mt: 4, textAlign: 'center' }}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => router.push(operation.Path)}
|
onClick={() => navigate(operation.Path)}
|
||||||
startIcon={<CheckCircleIcon />}
|
startIcon={<CheckCircleIcon />}
|
||||||
>
|
sx={{
|
||||||
Next Step
|
animation: `${successPop} 0.6s ease-out`,
|
||||||
</Button>
|
transition: 'transform 0.2s',
|
||||||
</Box>
|
'&:hover': {
|
||||||
)}
|
transform: 'scale(1.05)'
|
||||||
</Paper>
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Next Step
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Grow>
|
||||||
) : (
|
) : (
|
||||||
<Alert severity="info" sx={{ my: 2 }}>
|
<Fade in>
|
||||||
<Typography variant="body1">No operation found with ID: {operationId}</Typography>
|
<Alert severity="info" sx={{ my: 2 }}>
|
||||||
</Alert>
|
<Typography variant="body1">No operation found with ID: {operationId}</Typography>
|
||||||
|
</Alert>
|
||||||
|
</Fade>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</Webpage>
|
</Webpage>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user