Skip to content

Commit 15bef47

Browse files
committed
Enhance Upload component with package submission status tracking
- Added functionality to track and display the status of package submissions. - Implemented polling mechanism to check submission status and handle errors. - Updated state management for selected categories and teams. - Refactored submission logic to improve error handling and user feedback. - Integrated new API methods for fetching package submission status.
1 parent b9b7cea commit 15bef47

File tree

8 files changed

+278
-100
lines changed

8 files changed

+278
-100
lines changed

Diff for: apps/cyberstorm-remix/app/upload/upload.tsx

+182-83
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ import { UserMedia } from "@thunderstore/ts-uploader/src/client/types";
2727
import { DapperTs, PackageSubmissionResponse } from "@thunderstore/dapper-ts";
2828
import { MetaFunction } from "@remix-run/node";
2929
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";
3035

3136
interface CommunityOption {
3237
value: string;
@@ -70,6 +75,13 @@ export default function Upload() {
7075
const [categoryOptions, setCategoryOptions] = useState<
7176
{ communityId: string; categories: CategoryOption[] }[]
7277
>([]);
78+
const [availableTeams, setAvailableTeams] = useState<
79+
{
80+
name: string;
81+
role: string;
82+
member_count: number;
83+
}[]
84+
>([]);
7385

7486
for (const community of uploadData.results) {
7587
communityOptions.push({
@@ -81,30 +93,33 @@ export default function Upload() {
8193
// const outletContext = useOutletContext() as OutletContextShape;
8294
const session = useSession();
8395

96+
// Teams do not have a separate identifier, the team name is the identifier
97+
useEffect(() => {
98+
setAvailableTeams(session.getSessionCurrentUser()?.teams ?? []);
99+
}, [session]);
100+
84101
const [NSFW, setNSFW] = useState<boolean>(false);
85102
const [team, setTeam] = useState<string>();
86103
const [selectedCommunities, setSelectedCommunities] = useState<
87104
NewSelectOption[]
88105
>([]);
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);
92112

93113
const handleCategoryChange = useCallback(
94-
(
95-
val: NewSelectOption[] | undefined,
96-
categories: CategoryOption[],
97-
communityId: string
98-
) => {
114+
(val: NewSelectOption[] | undefined, communityId: string) => {
99115
setSelectedCategories((prev) => {
100-
const filtered = prev.filter((cat) => cat.communityId !== communityId);
116+
const newCategories = { ...prev };
101117
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];
106121
}
107-
return filtered;
122+
return newCategories;
108123
});
109124
},
110125
[]
@@ -124,6 +139,7 @@ export default function Upload() {
124139
useState<PackageSubmissionResponse>({});
125140

126141
const startUpload = useCallback(async () => {
142+
// console.log("Starting upload");
127143
if (!file) return;
128144

129145
try {
@@ -157,61 +173,50 @@ export default function Upload() {
157173
}, [file, session]);
158174

159175
const submit = useCallback(async () => {
176+
// console.log("Submitting package");
160177
if (!usermedia?.uuid) {
161178
setSubmissionError({
162179
__all__: ["Upload not completed"],
163180
});
164181
return;
165182
}
166183

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) {
191209
setSubmissionError(result);
192210
return;
193211
}
212+
}
194213

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;
215220
}
216221
}, [usermedia, NSFW, team, selectedCommunities, selectedCategories, session]);
217222

@@ -240,6 +245,64 @@ export default function Upload() {
240245
}
241246
}, [selectedCommunities]);
242247

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+
243306
// Helper function to format field names for display
244307
const formatFieldName = (field: string) => {
245308
return field
@@ -326,19 +389,10 @@ export default function Upload() {
326389
</div>
327390
<div className="upload__content">
328391
<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+
}))}
342396
onChange={(val) => setTeam(val?.value)}
343397
value={team ? { value: team, label: team } : undefined}
344398
/>
@@ -363,13 +417,15 @@ export default function Upload() {
363417
setSelectedCommunities(newCommunities);
364418
// Remove categories for communities that are no longer selected
365419
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+
)
368424
)
369425
);
370426
} else {
371427
setSelectedCommunities([]);
372-
setSelectedCategories([]);
428+
setSelectedCategories({});
373429
}
374430
}}
375431
value={selectedCommunities}
@@ -405,18 +461,17 @@ export default function Upload() {
405461
onChange={(val) => {
406462
handleCategoryChange(
407463
val ? (Array.isArray(val) ? val : [val]) : undefined,
408-
categories,
409464
community.value
410465
);
411466
}}
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,
416470
label:
417-
categories.find((c) => c.value === cat.categoryId)
471+
categories.find((c) => c.value === categoryId)
418472
?.label || "",
419-
}))}
473+
})
474+
)}
420475
/>
421476
</div>
422477
);
@@ -464,9 +519,10 @@ export default function Upload() {
464519
setUsermedia(undefined);
465520
setIsDone(false);
466521
setSelectedCommunities([]);
467-
setSelectedCategories([]);
522+
setSelectedCategories({});
468523
setTeam(undefined);
469524
setNSFW(false);
525+
setSubmissionStatus(undefined);
470526
}}
471527
csVariant="secondary"
472528
csSize="big"
@@ -496,6 +552,44 @@ export default function Upload() {
496552
)}
497553
</div>
498554
<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+
)}
499593
{error && (
500594
<div className="upload__error">
501595
<p>{error.message}</p>
@@ -551,7 +645,12 @@ export default function Upload() {
551645
</span>
552646
<span>
553647
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(" | ")}
555654
</span>
556655
</div>
557656
);

0 commit comments

Comments
 (0)