@@ -27,6 +27,11 @@ import { UserMedia } from "@thunderstore/ts-uploader/src/client/types";
27
27
import { DapperTs , PackageSubmissionResponse } from "@thunderstore/dapper-ts" ;
28
28
import { MetaFunction } from "@remix-run/node" ;
29
29
import { useLoaderData } from "@remix-run/react" ;
30
+ import {
31
+ packageSubmissionErrorSchema ,
32
+ packageSubmissionStatusSchema ,
33
+ } from "@thunderstore/dapper-ts/src/methods/package" ;
34
+ import { PackageSubmissionStatus } from "@thunderstore/dapper/types" ;
30
35
31
36
interface CommunityOption {
32
37
value : string ;
@@ -70,6 +75,13 @@ export default function Upload() {
70
75
const [ categoryOptions , setCategoryOptions ] = useState <
71
76
{ communityId : string ; categories : CategoryOption [ ] } [ ]
72
77
> ( [ ] ) ;
78
+ const [ availableTeams , setAvailableTeams ] = useState <
79
+ {
80
+ name : string ;
81
+ role : string ;
82
+ member_count : number ;
83
+ } [ ]
84
+ > ( [ ] ) ;
73
85
74
86
for ( const community of uploadData . results ) {
75
87
communityOptions . push ( {
@@ -81,30 +93,33 @@ export default function Upload() {
81
93
// const outletContext = useOutletContext() as OutletContextShape;
82
94
const session = useSession ( ) ;
83
95
96
+ // Teams do not have a separate identifier, the team name is the identifier
97
+ useEffect ( ( ) => {
98
+ setAvailableTeams ( session . getSessionCurrentUser ( ) ?. teams ?? [ ] ) ;
99
+ } , [ session ] ) ;
100
+
84
101
const [ NSFW , setNSFW ] = useState < boolean > ( false ) ;
85
102
const [ team , setTeam ] = useState < string > ( ) ;
86
103
const [ selectedCommunities , setSelectedCommunities ] = useState <
87
104
NewSelectOption [ ]
88
105
> ( [ ] ) ;
89
- const [ selectedCategories , setSelectedCategories ] = useState <
90
- { communityId : string ; categoryId : string } [ ]
91
- > ( [ ] ) ;
106
+ const [ selectedCategories , setSelectedCategories ] = useState < {
107
+ [ key : string ] : string [ ] ;
108
+ } > ( { } ) ;
109
+ const [ submissionStatus , setSubmissionStatus ] =
110
+ useState < PackageSubmissionStatus > ( ) ;
111
+ const [ pollingError , setPollingError ] = useState < boolean > ( false ) ;
92
112
93
113
const handleCategoryChange = useCallback (
94
- (
95
- val : NewSelectOption [ ] | undefined ,
96
- categories : CategoryOption [ ] ,
97
- communityId : string
98
- ) => {
114
+ ( val : NewSelectOption [ ] | undefined , communityId : string ) => {
99
115
setSelectedCategories ( ( prev ) => {
100
- const filtered = prev . filter ( ( cat ) => cat . communityId !== communityId ) ;
116
+ const newCategories = { ... prev } ;
101
117
if ( val ) {
102
- return [
103
- ...filtered ,
104
- ...val . map ( ( v ) => ( { communityId, categoryId : v . value } ) ) ,
105
- ] ;
118
+ newCategories [ communityId ] = val . map ( ( v ) => v . value ) ;
119
+ } else {
120
+ delete newCategories [ communityId ] ;
106
121
}
107
- return filtered ;
122
+ return newCategories ;
108
123
} ) ;
109
124
} ,
110
125
[ ]
@@ -124,6 +139,7 @@ export default function Upload() {
124
139
useState < PackageSubmissionResponse > ( { } ) ;
125
140
126
141
const startUpload = useCallback ( async ( ) => {
142
+ // console.log("Starting upload");
127
143
if ( ! file ) return ;
128
144
129
145
try {
@@ -157,61 +173,50 @@ export default function Upload() {
157
173
} , [ file , session ] ) ;
158
174
159
175
const submit = useCallback ( async ( ) => {
176
+ // console.log("Submitting package");
160
177
if ( ! usermedia ?. uuid ) {
161
178
setSubmissionError ( {
162
179
__all__ : [ "Upload not completed" ] ,
163
180
} ) ;
164
181
return ;
165
182
}
166
183
167
- try {
168
- setSubmissionError ( { } ) ;
169
- const config = session . getConfig ( ) ;
170
- if ( ! config . apiHost ) {
171
- throw new Error ( "API host is not configured" ) ;
172
- }
173
- const dapper = new DapperTs ( ( ) => config ) ;
174
- const result = await dapper . postPackageSubmissionMetadata (
175
- team ?? "" ,
176
- selectedCommunities . map (
177
- ( community ) => ( community as NewSelectOption ) . value
178
- ) ,
179
- NSFW ,
180
- usermedia . uuid ,
181
- selectedCategories . map ( ( cat ) => cat . categoryId ) ,
182
- { }
183
- ) ;
184
-
185
- // Check if the result is a SubmissionError
186
- if (
187
- "__all__" in result ||
188
- "author_name" in result ||
189
- "communities" in result
190
- ) {
184
+ setSubmissionError ( { } ) ;
185
+ const config = session . getConfig ( ) ;
186
+ const dapper = new DapperTs ( ( ) => config ) ;
187
+ const result = await dapper . postPackageSubmissionMetadata (
188
+ team ?? "" ,
189
+ selectedCommunities . map (
190
+ ( community ) => ( community as NewSelectOption ) . value
191
+ ) ,
192
+ NSFW ,
193
+ usermedia . uuid ,
194
+ [ ] ,
195
+ selectedCategories
196
+ ) ;
197
+
198
+ const sub = packageSubmissionStatusSchema . safeParse ( result ) ;
199
+ // console.log("Submission statuaaas:", sub);
200
+ if ( sub . success ) {
201
+ setSubmissionStatus ( sub . data ) ;
202
+ // Start polling immediately when we get a submission status
203
+ pollSubmission ( sub . data . id ) ;
204
+ // console.log("Submission status:", sub.data);
205
+ } else {
206
+ // Check if the submission request had an error
207
+ // console.log("Submission error:", result);
208
+ if ( packageSubmissionErrorSchema . safeParse ( result ) . success ) {
191
209
setSubmissionError ( result ) ;
192
210
return ;
193
211
}
212
+ }
194
213
195
- // Handle successful submission
196
- if ( "task_error" in result && result . task_error ) {
197
- setSubmissionError ( {
198
- __all__ : [ `Submission failed: ${ result . result } ` ] ,
199
- } ) ;
200
- return ;
201
- }
202
-
203
- alert ( "Package submitted successfully!" ) ;
204
- } catch ( error ) {
205
- console . error ( "Submission failed:" , error ) ;
206
- if ( error instanceof Error ) {
207
- setSubmissionError ( {
208
- __all__ : [ error . message ] ,
209
- } ) ;
210
- } else {
211
- setSubmissionError ( {
212
- __all__ : [ "An unexpected error occurred during submission" ] ,
213
- } ) ;
214
- }
214
+ // Handle successful submission
215
+ if ( "task_error" in result && result . task_error ) {
216
+ setSubmissionError ( {
217
+ __all__ : [ `Submission failed: ${ result . result } ` ] ,
218
+ } ) ;
219
+ return ;
215
220
}
216
221
} , [ usermedia , NSFW , team , selectedCommunities , selectedCategories , session ] ) ;
217
222
@@ -240,6 +245,64 @@ export default function Upload() {
240
245
}
241
246
} , [ selectedCommunities ] ) ;
242
247
248
+ const pollSubmission = async ( submissionId : string ) => {
249
+ // console.log("Polling submission status for:", submissionId);
250
+ const result = await window . Dapper . getPackageSubmissionStatus ( submissionId ) ;
251
+ // console.log("Result:", result);
252
+ const parsedResult = packageSubmissionStatusSchema . safeParse ( result ) ;
253
+ // console.log("Parsed result:", parsedResult);
254
+ if ( parsedResult . success ) {
255
+ setSubmissionStatus ( parsedResult . data ) ;
256
+ } else {
257
+ setSubmissionError ( result ) ;
258
+ return ;
259
+ }
260
+ } ;
261
+
262
+ useEffect ( ( ) => {
263
+ let retriesLeft = 3 ;
264
+ let isPolling = true ;
265
+ const pollSubmissionStatus = async ( ) => {
266
+ while ( isPolling && submissionStatus ?. status === "PENDING" ) {
267
+ // Wait 5 seconds before polling again
268
+ await new Promise ( ( resolve ) => setTimeout ( resolve , 5000 ) ) ;
269
+ try {
270
+ await pollSubmission ( submissionStatus . id ) ;
271
+ retriesLeft = 3 ;
272
+ setPollingError ( false ) ;
273
+ // Stop polling if status is no longer PENDING
274
+ if ( submissionStatus ?. status !== "PENDING" ) {
275
+ isPolling = false ;
276
+ break ;
277
+ }
278
+ } catch {
279
+ retriesLeft -= 1 ;
280
+ if ( retriesLeft < 0 ) {
281
+ setPollingError ( true ) ;
282
+ isPolling = false ;
283
+ break ;
284
+ }
285
+ }
286
+ }
287
+ } ;
288
+
289
+ if ( submissionStatus ?. status === "PENDING" ) {
290
+ pollSubmissionStatus ( ) ;
291
+ }
292
+
293
+ // Cleanup function to stop polling when component unmounts or status changes
294
+ return ( ) => {
295
+ isPolling = false ;
296
+ } ;
297
+ } , [ submissionStatus ?. status ] ) ;
298
+
299
+ const retryPolling = ( ) => {
300
+ if ( submissionStatus ?. id ) {
301
+ setPollingError ( false ) ;
302
+ pollSubmission ( submissionStatus . id ) ;
303
+ }
304
+ } ;
305
+
243
306
// Helper function to format field names for display
244
307
const formatFieldName = ( field : string ) => {
245
308
return field
@@ -326,19 +389,10 @@ export default function Upload() {
326
389
</ div >
327
390
< div className = "upload__content" >
328
391
< NewSelectSearch
329
- options = { [
330
- { value : "Test_Team_0" , label : "Test_Team_0" } ,
331
- { value : "Test_Team_1" , label : "Test_Team_1" } ,
332
- { value : "Test_Team_2" , label : "Test_Team_2" } ,
333
- { value : "Test_Team_3" , label : "Test_Team_3" } ,
334
- { value : "Test_Team_4" , label : "Test_Team_4" } ,
335
- { value : "Test_Team_5" , label : "Test_Team_5" } ,
336
- { value : "Test_Team_6" , label : "Test_Team_6" } ,
337
- { value : "Test_Team_7" , label : "Test_Team_7" } ,
338
- { value : "Test_Team_8" , label : "Test_Team_8" } ,
339
- { value : "Test_Team_9" , label : "Test_Team_9" } ,
340
- { value : "Test_Team_10" , label : "Test_Team_10" } ,
341
- ] }
392
+ options = { availableTeams ?. map ( ( team ) => ( {
393
+ value : team . name ,
394
+ label : team . name ,
395
+ } ) ) }
342
396
onChange = { ( val ) => setTeam ( val ?. value ) }
343
397
value = { team ? { value : team , label : team } : undefined }
344
398
/>
@@ -363,13 +417,15 @@ export default function Upload() {
363
417
setSelectedCommunities ( newCommunities ) ;
364
418
// Remove categories for communities that are no longer selected
365
419
setSelectedCategories ( ( prev ) =>
366
- prev . filter ( ( cat ) =>
367
- newCommunities . some ( ( c ) => c . value === cat . communityId )
420
+ Object . fromEntries (
421
+ Object . entries ( prev ) . filter ( ( [ communityId ] ) =>
422
+ newCommunities . some ( ( c ) => c . value === communityId )
423
+ )
368
424
)
369
425
) ;
370
426
} else {
371
427
setSelectedCommunities ( [ ] ) ;
372
- setSelectedCategories ( [ ] ) ;
428
+ setSelectedCategories ( { } ) ;
373
429
}
374
430
} }
375
431
value = { selectedCommunities }
@@ -405,18 +461,17 @@ export default function Upload() {
405
461
onChange = { ( val ) => {
406
462
handleCategoryChange (
407
463
val ? ( Array . isArray ( val ) ? val : [ val ] ) : undefined ,
408
- categories ,
409
464
community . value
410
465
) ;
411
466
} }
412
- value = { selectedCategories
413
- . filter ( ( cat ) => cat . communityId === community . value )
414
- . map ( ( cat ) => ( {
415
- value : cat . categoryId ,
467
+ value = { selectedCategories [ community . value ] ?. map (
468
+ ( categoryId ) => ( {
469
+ value : categoryId ,
416
470
label :
417
- categories . find ( ( c ) => c . value === cat . categoryId )
471
+ categories . find ( ( c ) => c . value === categoryId )
418
472
?. label || "" ,
419
- } ) ) }
473
+ } )
474
+ ) }
420
475
/>
421
476
</ div >
422
477
) ;
@@ -464,9 +519,10 @@ export default function Upload() {
464
519
setUsermedia ( undefined ) ;
465
520
setIsDone ( false ) ;
466
521
setSelectedCommunities ( [ ] ) ;
467
- setSelectedCategories ( [ ] ) ;
522
+ setSelectedCategories ( { } ) ;
468
523
setTeam ( undefined ) ;
469
524
setNSFW ( false ) ;
525
+ setSubmissionStatus ( undefined ) ;
470
526
} }
471
527
csVariant = "secondary"
472
528
csSize = "big"
@@ -496,6 +552,44 @@ export default function Upload() {
496
552
) }
497
553
</ div >
498
554
< UploadProgressDisplay handle = { handle } />
555
+ { submissionStatus && (
556
+ < div className = "upload__status" >
557
+ < p > Submission Status: { submissionStatus . status } </ p >
558
+ { pollingError && (
559
+ < div className = "upload__status-error" >
560
+ < p > Unable to check submission status</ p >
561
+ < NewButton onClick = { retryPolling } >
562
+ Retry Status Check
563
+ </ NewButton >
564
+ </ div >
565
+ ) }
566
+ { submissionStatus . form_errors &&
567
+ Object . keys ( submissionStatus . form_errors ) . length > 0 && (
568
+ < div className = "upload__error" >
569
+ < p > Form Errors:</ p >
570
+ < ul >
571
+ { Object . entries ( submissionStatus . form_errors ) . map (
572
+ ( [ field , error ] ) => (
573
+ < li key = { field } >
574
+ { field !== "__all__" && (
575
+ < strong > { formatFieldName ( field ) } : </ strong >
576
+ ) }
577
+ { Array . isArray ( error )
578
+ ? error . join ( ", " )
579
+ : String ( error ) }
580
+ </ li >
581
+ )
582
+ ) }
583
+ </ ul >
584
+ </ div >
585
+ ) }
586
+ { submissionStatus . task_error && (
587
+ < div className = "upload__error" >
588
+ < p > Task Error: { submissionStatus . result } </ p >
589
+ </ div >
590
+ ) }
591
+ </ div >
592
+ ) }
499
593
{ error && (
500
594
< div className = "upload__error" >
501
595
< p > { error . message } </ p >
@@ -551,7 +645,12 @@ export default function Upload() {
551
645
</ span >
552
646
< span >
553
647
selectedCategories:{ " " }
554
- { selectedCategories . map ( ( c ) => `${ c . communityId } -${ c . categoryId } ` ) }
648
+ { Object . entries ( selectedCategories )
649
+ . map (
650
+ ( [ communityId , categoryIds ] ) =>
651
+ `${ communityId } : ${ categoryIds . join ( ", " ) } `
652
+ )
653
+ . join ( " | " ) }
555
654
</ span >
556
655
</ div >
557
656
) ;
0 commit comments