From 260f5f0f7d33ae50e5c1d665c3ac38e5ad1e89f9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Antti=20M=C3=A4ki?=
Date: Fri, 7 Nov 2025 11:56:30 +0200
Subject: [PATCH 1/2] Avoid repetition in team setting tab client loaders
Team name is always returned from the clientLoader to avoid needing to
call useParams just to access it.
makeTeamSettingsTabLoader assumes params.namespaceId is always defined.
On manual testing I couldn't produce a case where this was not true, as
React Router returned 404 for any requests for URLs where it was empty
(except for profiles which act as index, as for that it's a different
route altogether).
---
.../teams/team/tabs/Members/Members.tsx | 50 +++++------------
.../teams/team/tabs/Profile/Profile.tsx | 47 ++++++----------
.../tabs/ServiceAccounts/ServiceAccounts.tsx | 54 ++++++-------------
.../teams/team/tabs/Settings/Settings.tsx | 38 ++++++-------
.../cyberstorm/utils/dapperClientLoaders.ts | 34 ++++++++++++
5 files changed, 97 insertions(+), 126 deletions(-)
create mode 100644 apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts
diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx
index 477c406eb..2b544061b 100644
--- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx
+++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx
@@ -1,6 +1,8 @@
-import "./Members.css";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus, faTrashCan } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useReducer, useState } from "react";
+import { useLoaderData, useOutletContext, useRevalidator } from "react-router";
+
import {
Modal,
NewAvatar,
@@ -13,50 +15,26 @@ import {
type SelectOption,
useToast,
} from "@thunderstore/cyberstorm";
-import { type LoaderFunctionArgs } from "react-router";
-import { useLoaderData, useOutletContext, useRevalidator } from "react-router";
+import { TableSort } from "@thunderstore/cyberstorm/src/newComponents/Table/Table";
import {
- ApiError,
type RequestConfig,
teamAddMember,
type TeamAddMemberRequestData,
teamEditMember,
teamRemoveMember,
} from "@thunderstore/thunderstore-api";
-import { type OutletContextShape } from "../../../../../root";
-import { TableSort } from "@thunderstore/cyberstorm/src/newComponents/Table/Table";
import { ApiAction } from "@thunderstore/ts-api-react-actions";
-import { DapperTs } from "@thunderstore/dapper-ts";
-import { getSessionTools } from "cyberstorm/security/publicEnvVariables";
+
+import { type OutletContextShape } from "app/root";
+import { makeTeamSettingsTabLoader } from "cyberstorm/utils/dapperClientLoaders";
import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm";
-import { useReducer, useState } from "react";
+import "./Members.css";
-// REMIX TODO: Add check for "user has permission to see this page"
-export async function clientLoader({ params }: LoaderFunctionArgs) {
- if (params.namespaceId) {
- try {
- const tools = getSessionTools();
- const config = tools?.getConfig();
- const dapper = new DapperTs(() => {
- return {
- apiHost: config.apiHost,
- sessionId: config.sessionId,
- };
- });
- return {
- teamName: params.namespaceId,
- members: await dapper.getTeamMembers(params.namespaceId),
- };
- } catch (error) {
- if (error instanceof ApiError) {
- throw new Response("Team members not found", { status: 404 });
- } else {
- throw error;
- }
- }
- }
- throw new Response("Team not found", { status: 404 });
-}
+export const clientLoader = makeTeamSettingsTabLoader(
+ async (dapper, teamName) => ({
+ members: await dapper.getTeamMembers(teamName),
+ })
+);
export function HydrateFallback() {
return Loading...
;
diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx
index 18252986b..ef26cd41e 100644
--- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx
+++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx
@@ -1,42 +1,25 @@
-import { type LoaderFunctionArgs } from "react-router";
+import { useReducer } from "react";
import { useLoaderData, useOutletContext, useRevalidator } from "react-router";
+
+import { NewButton, NewTextInput, useToast } from "@thunderstore/cyberstorm";
import {
- ApiError,
teamDetailsEdit,
type TeamDetailsEditRequestData,
} from "@thunderstore/thunderstore-api";
-import { type OutletContextShape } from "~/root";
-import "./Profile.css";
-import { DapperTs } from "@thunderstore/dapper-ts";
-import { getSessionTools } from "cyberstorm/security/publicEnvVariables";
+
+import { type OutletContextShape } from "app/root";
+import { makeTeamSettingsTabLoader } from "cyberstorm/utils/dapperClientLoaders";
import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm";
-import { useReducer } from "react";
-import { NewButton, NewTextInput, useToast } from "@thunderstore/cyberstorm";
+import "./Profile.css";
-export async function clientLoader({ params }: LoaderFunctionArgs) {
- if (params.namespaceId) {
- try {
- const tools = getSessionTools();
- const dapper = new DapperTs(() => {
- return {
- apiHost: tools?.getConfig().apiHost,
- sessionId: tools?.getConfig().sessionId,
- };
- });
- return {
- team: await dapper.getTeamDetails(params.namespaceId),
- };
- } catch (error) {
- if (error instanceof ApiError) {
- throw new Response("Team not found", { status: 404 });
- } else {
- // REMIX TODO: Add sentry
- throw error;
- }
- }
- }
- throw new Response("Team not found", { status: 404 });
-}
+export const clientLoader = makeTeamSettingsTabLoader(
+ async (dapper, teamName) => ({
+ // TODO: for hygienie we shouldn't use this public endpoint but
+ // have an endpoint that confirms user permissions and returns
+ // possibly sensitive information.
+ team: await dapper.getTeamDetails(teamName),
+ })
+);
export function HydrateFallback() {
return Loading...
;
diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx
index fa5467811..81c822ab4 100644
--- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx
+++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx
@@ -1,4 +1,8 @@
-import "./ServiceAccounts.css";
+import { faPlus } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useReducer, useState } from "react";
+import { useLoaderData, useOutletContext, useRevalidator } from "react-router";
+
import {
NewAlert,
NewButton,
@@ -9,52 +13,24 @@ import {
Heading,
CodeBox,
} from "@thunderstore/cyberstorm";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faPlus } from "@fortawesome/free-solid-svg-icons";
-import { type LoaderFunctionArgs } from "react-router";
-import { useLoaderData, useOutletContext, useRevalidator } from "react-router";
+import { TableSort } from "@thunderstore/cyberstorm/src/newComponents/Table/Table";
import {
- ApiError,
type RequestConfig,
teamAddServiceAccount,
type TeamServiceAccountAddRequestData,
} from "@thunderstore/thunderstore-api";
-import { TableSort } from "@thunderstore/cyberstorm/src/newComponents/Table/Table";
-import { type OutletContextShape } from "../../../../../root";
-import { useReducer, useState } from "react";
-import { DapperTs } from "@thunderstore/dapper-ts";
-import { getSessionTools } from "cyberstorm/security/publicEnvVariables";
+
+import { type OutletContextShape } from "app/root";
+import { makeTeamSettingsTabLoader } from "cyberstorm/utils/dapperClientLoaders";
import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm";
import { ServiceAccountRemoveModal } from "./ServiceAccountRemoveModal";
+import "./ServiceAccounts.css";
-// REMIX TODO: Add check for "user has permission to see this page"
-export async function clientLoader({ params }: LoaderFunctionArgs) {
- if (params.namespaceId) {
- try {
- const tools = getSessionTools();
- const config = tools?.getConfig();
- const dapper = new DapperTs(() => {
- return {
- apiHost: config?.apiHost,
- sessionId: config?.sessionId,
- };
- });
- return {
- teamName: params.namespaceId,
- serviceAccounts: await dapper.getTeamServiceAccounts(
- params.namespaceId
- ),
- };
- } catch (error) {
- if (error instanceof ApiError) {
- throw new Response("Team not found", { status: 404 });
- } else {
- throw error;
- }
- }
- }
- throw new Response("Team not found", { status: 404 });
-}
+export const clientLoader = makeTeamSettingsTabLoader(
+ async (dapper, teamName) => ({
+ serviceAccounts: await dapper.getTeamServiceAccounts(teamName),
+ })
+);
export function HydrateFallback() {
return Loading...
;
diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx
index bd1cb6879..c43c1ec15 100644
--- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx
+++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx
@@ -1,3 +1,8 @@
+import { faTrashCan } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useReducer, useState } from "react";
+import { useLoaderData, useNavigate, useOutletContext } from "react-router";
+
import "./Settings.css";
import {
NewAlert,
@@ -8,11 +13,6 @@ import {
NewTextInput,
useToast,
} from "@thunderstore/cyberstorm";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faTrashCan } from "@fortawesome/free-solid-svg-icons";
-import { useNavigate, useOutletContext, useParams } from "react-router";
-
-import { type OutletContextShape } from "~/root";
import {
type RequestConfig,
teamDisband,
@@ -20,24 +20,24 @@ import {
teamRemoveMember,
} from "@thunderstore/thunderstore-api";
import { ApiAction } from "@thunderstore/ts-api-react-actions";
-import { NotLoggedIn } from "~/commonComponents/NotLoggedIn/NotLoggedIn";
+
+import { NotLoggedIn } from "app/commonComponents/NotLoggedIn/NotLoggedIn";
+import { type OutletContextShape } from "app/root";
+import { makeTeamSettingsTabLoader } from "cyberstorm/utils/dapperClientLoaders";
import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm";
-import { useReducer, useState } from "react";
-// REMIX TODO: Make sure user is redirected of this page, if the user is not logged in
+export const clientLoader = makeTeamSettingsTabLoader(
+ // TODO: add end point for checking can leave/disband status.
+ async (dapper, teamName) => ({})
+);
+
export default function Settings() {
- const params = useParams();
+ const { teamName } = useLoaderData();
const outletContext = useOutletContext() as OutletContextShape;
- if (
- !outletContext.currentUser ||
- !outletContext.currentUser.username ||
- !params.namespaceId
- )
+ if (!outletContext.currentUser || !outletContext.currentUser.username)
return ;
- if (!params.namespaceId) return Team not found
;
-
const toast = useToast();
const navigate = useNavigate();
@@ -69,7 +69,7 @@ export default function Settings() {
You cannot currently disband this team as it has packages.
- You are about to disband the team {params.namespaceId}.
+ You are about to disband the team {teamName}.
Be aware you can currently only disband teams with no packages. If
you need to archive a team with existing pages, contact Mythic#0001
on the Thunderstore Discord.
(
+ dataFetcher: (dapper: DapperTs, teamName: string) => Promise
+) {
+ return async function clientLoader({ params }: LoaderFunctionArgs) {
+ const teamName = params.namespaceId!;
+
+ try {
+ const dapper = setupDapper();
+ const data = await dataFetcher(dapper, teamName);
+ return { teamName, ...data };
+ } catch (error) {
+ if (error instanceof ApiError) {
+ throw new Response(`Team "${teamName}" not found`, { status: 404 });
+ }
+ throw error;
+ }
+ };
+}
+
+const setupDapper = () => {
+ const tools = getSessionTools();
+ const config = tools?.getConfig();
+ return new DapperTs(() => ({
+ apiHost: config?.apiHost,
+ sessionId: config?.sessionId,
+ }));
+};
From 60d5c6eceea199982b3b471c44634b9fc1f733b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Antti=20M=C3=A4ki?=
Date: Fri, 7 Nov 2025 12:57:54 +0200
Subject: [PATCH 2/2] Add generic error handling for team settings tabs
This now handles at least 403, 404, and 500 errors by displaying the
status code and error message returned by the server. No more effort
was spent on this, as I believe this fulfills feature parity with the
legacy site. To fulfill design parity we would need customized error
pages for each error, which I consider better done separately.
It's worth to note that the Profile tab doesn't currently produce 403
errors as it reads the data from a public API. Settings tab doesn't
currently hit any APIs at all, although I plan to fix that shortly.
---
.../cyberstorm/utils/dapperClientLoaders.ts | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts b/apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts
index 5adcd5997..63fba0ab8 100644
--- a/apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts
+++ b/apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts
@@ -1,7 +1,7 @@
import { type LoaderFunctionArgs } from "react-router";
import { DapperTs } from "@thunderstore/dapper-ts";
-import { ApiError } from "@thunderstore/thunderstore-api";
+import { ApiError, type GenericApiError } from "@thunderstore/thunderstore-api";
import { getSessionTools } from "cyberstorm/security/publicEnvVariables";
@@ -17,7 +17,11 @@ export function makeTeamSettingsTabLoader(
return { teamName, ...data };
} catch (error) {
if (error instanceof ApiError) {
- throw new Response(`Team "${teamName}" not found`, { status: 404 });
+ const status = error.response.status;
+ const statusText =
+ (error.responseJson as GenericApiError)?.detail ??
+ error.response.statusText;
+ throw new Response(statusText, { status, statusText });
}
throw error;
}