Skip to content

Conversation

@anttimaki
Copy link
Contributor

No description provided.

@codecov
Copy link

codecov bot commented Nov 7, 2025

Codecov Report

❌ Patch coverage is 0% with 491 lines in your changes missing coverage. Please review.
✅ Project coverage is 9.85%. Comparing base (06e8ed7) to head (32a09a2).

Files with missing lines Patch % Lines
.../settings/teams/team/tabs/Members/MembersTable.tsx 0.00% 163 Missing ⚠️
...settings/teams/team/tabs/Members/MemberAddForm.tsx 0.00% 115 Missing ⚠️
...team/tabs/ServiceAccounts/ServiceAccountsTable.tsx 0.00% 55 Missing ⚠️
...app/settings/teams/team/tabs/Settings/Settings.tsx 0.00% 52 Missing ⚠️
...x/app/settings/teams/team/tabs/Profile/Profile.tsx 0.00% 39 Missing ⚠️
...eams/team/tabs/ServiceAccounts/ServiceAccounts.tsx 0.00% 38 Missing ⚠️
...x/app/settings/teams/team/tabs/Members/Members.tsx 0.00% 29 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##           master   #1604      +/-   ##
=========================================
- Coverage    9.88%   9.85%   -0.03%     
=========================================
  Files         310     313       +3     
  Lines       22489   22548      +59     
  Branches      406     409       +3     
=========================================
  Hits         2223    2223              
- Misses      20266   20325      +59     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai
Copy link

coderabbitai bot commented Nov 7, 2025

Walkthrough

The PR refactors team settings UI components (Members, Profile, ServiceAccounts, Settings) by extracting inline form and table logic into dedicated subcomponents (MemberAddForm, MembersTable, ServiceAccountsTable, ProfileForm). It replaces the previous monolithic component structure with a Suspense/Await-based async data handling pattern. HydrateFallback components are removed throughout. The Settings loader data shape is updated to include teamName, and authentication checks are moved earlier in the render cycle. Form handling is consolidated into reusable components that manage their own state and submission flows.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive No description was provided by the author, making it impossible to assess whether it relates to the changeset. Add a pull request description explaining the motivation and impact of using Suspense in team settings tabs.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: refactoring team settings tabs to use React Suspense for async data handling.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch team-settings-pt4-suspense

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines 121 to 151
<NewTextInput
name={"username"}
placeholder={"Enter username..."}
onChange={(e) => {
updateFormFieldState({
field: "username",
value: e.target.value,
});
}}
rootClasses="add-member-form__username-input"
id="username"
/>
</div>
<div className="add-member-form__field">
<label className="add-member-form__label" htmlFor="role">
Role
</label>
<NewSelect
name={"role"}
options={roleOptions}
defaultValue="member"
placeholder="Select role..."
onChange={(value) => {
updateFormFieldState({ field: "role", value: value });
}}
id="role"
/>
</div>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the value prop be passed to the TextInput and NewSelect components here, so that they are controlled and properly synced with the form state?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes for both

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or well NewSelect will work without the value set, as it's using a Radix component, that will hold the state based on the defaultValue and onChange changes.

NewTextInput does need the value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added for both. It was in fact the NewSelect that required the value prop to work prop-erly. NewTextInput at least seemed seemed to work without it.

Comment on lines 74 to 123
const tableData = members.map((member, index) => {
return [
{
value: (
<NewLink
primitiveType="cyberstormLink"
linkId="User"
key={`user_${index}`}
user={member.username}
rootClasses="members__user"
>
<NewAvatar
src={member.avatar}
username={member.username}
csSize="small"
/>
<span>{member.username}</span>
</NewLink>
),
sortValue: member.username,
},
{
value: (
<div key={`role_${index}`}>
<NewSelect
csSize="xsmall"
options={roleOptions}
value={member.role}
onChange={(val: "owner" | "member") =>
changeMemberRole(member.username, val)
}
/>
</div>
),
sortValue: member.role,
},
{
value: (
<RemoveTeamMemberForm
indexKey={`action_${index}`}
teamName={teamName}
userName={member.username}
updateTrigger={updateTrigger}
config={config}
/>
),
sortValue: 0,
},
];
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the keys here be more stable instead of relying solely on key={<x>_${index}}? For example, you could use key={<x>_${member.username}_${index}} to make them both stable and unique.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed as suggested.

</p>
<LeaveTeamForm
userName={currentUser}
teamName={teamName}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use resolvedTeamName?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed this is three spots.

contact Mythic#0001 on the Thunderstore Discord.
</p>
<DisbandTeamForm
teamName={teamName}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use resolvedTeamName?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment on lines 34 to 37
export const clientLoader = makeTeamSettingsTabLoader(
// TODO: add end point for checking can leave/disband status.
async (dapper, teamName) => ({})
async (dapper, teamName) => ({ teamName })
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there a wrapper for this? Should the clientLoader return something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if by "wrapper" you're referring to:

  1. the parenthesis around { teamname }: that's just something the linter in the mod manager project requires and I've inadvertently carried the habit over
  2. the fact that we have an async function that takes teamName as argument only to return it: that's because that's what makeTeamSettingsTabLoader expects as an argument. This is just a placeholder for now, in a later PR this will actually call the backend via Dapper.
  3. makeTeamSettingsTabLoader itself: it's a helper function that returns a function that will actually make clientLoader to return a value. It was introduced in the preceding PR to reduce boilerplate, as all four tabs of the team settings view follow the same pattern (as shown in that PR)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did no code changes for this one.

Copy link
Contributor

@Roffenlund Roffenlund left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few comments, mainly minor/unclear things.

Base automatically changed from team-settings-pt3-dry-client-loader to master November 12, 2025 15:26
As part of the upcoming change, clientLoader will start returning a
Promise rather than the resolved value. For Suspense to work, the
body of the component function can't depend on the resolved values,
so move such parts into subcomponents.
As part of the upcoming change, clientLoader will start returning a
Promise rather than the resolved value. For Suspense to work, the
body of the component function can't depend on the resolved values,
so move such parts into subcomponents.
As part of the upcoming change, clientLoader will start returning a
Promise rather than the resolved value. For Suspense to work, the
body of the component function can't depend on the resolved values,
so move such parts into subcomponents.
Using await in clientLoader causes navigating between the tabs to do
nothing until the clientLoader promise resolves. Removing the await
forces the component to deal with the promises returned by the
clientLoader, for which Suspense+Await components is a good pattern.
Using it rendes HydarteFallback obsolete.

All tabs use all-or-nothing rendering policy. For profile tab, only a
form is rendered and it requires the resolved values. For Members and
Service Account tabs we could consider rendering the sidebar and empty
table initially, but I though it would be awkward for the add button
to pop on the sidebar once the download finished (or alternatively a
skeleton component to disappear). For Settings tab partial rendering
could be considered, but that tab doesn't have an API endpoint
available yet and shows only dummy data. On the other hand, for
consistency, all-or-nothing strategy could be used there as well.
@anttimaki anttimaki force-pushed the team-settings-pt4-suspense branch from 79cf5dd to 32a09a2 Compare November 12, 2025 15:54
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MembersTable.tsx (1)

74-123: Keys may be unnecessary on individual cell elements.

The key props on lines 81 and 97 are applied to elements that aren't direct children of an array iteration, so they may not be needed. Only the indexKey on line 113 is actually used by the RemoveTeamMemberForm component. Consider removing the keys on lines 81 and 97 unless NewTable specifically requires them.

🧹 Nitpick comments (2)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx (2)

31-31: Consider using a styled loading component.

The fallback uses a plain <div>Loading...</div> which lacks styling. Consider using a consistent loading component or spinner from your design system.


32-56: Consider adding error handling to the Await component.

The Await component supports an errorElement prop for handling promise rejections inline. While the loader's error handling may throw a Response that gets caught by a parent error boundary, adding explicit error handling here would provide better user experience and more granular control.

Example:

-      <Await resolve={members}>
+      <Await 
+        resolve={members}
+        errorElement={<div>Failed to load members. Please try again.</div>}
+      >
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06e8ed7 and 32a09a2.

📒 Files selected for processing (7)
  • apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MemberAddForm.tsx (1 hunks)
  • apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx (1 hunks)
  • apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MembersTable.tsx (1 hunks)
  • apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx (3 hunks)
  • apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx (3 hunks)
  • apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccountsTable.tsx (1 hunks)
  • apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx (10)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx (1)
  • clientLoader (15-19)
apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx (1)
  • clientLoader (30-34)
apps/cyberstorm-remix/app/p/packageListing.tsx (1)
  • clientLoader (121-150)
apps/cyberstorm-remix/app/root.tsx (2)
  • clientLoader (122-173)
  • OutletContextShape (84-89)
apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx (1)
  • clientLoader (44-76)
apps/cyberstorm-remix/app/p/tabs/Versions/Versions.tsx (1)
  • clientLoader (44-65)
apps/cyberstorm-remix/app/p/tabs/Readme/Readme.tsx (1)
  • clientLoader (32-50)
apps/cyberstorm-remix/app/p/tabs/Changelog/Changelog.tsx (1)
  • clientLoader (35-56)
apps/cyberstorm-remix/app/c/community.tsx (1)
  • clientLoader (50-65)
packages/thunderstore-api/src/schemas/objectSchemas.ts (1)
  • TeamDetails (109-109)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx (5)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx (1)
  • clientLoader (15-19)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx (1)
  • clientLoader (21-28)
apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx (1)
  • clientLoader (30-34)
apps/cyberstorm-remix/app/root.tsx (2)
  • clientLoader (122-173)
  • OutletContextShape (84-89)
packages/cyberstorm/src/newComponents/Toast/Provider.tsx (1)
  • useToast (79-87)
apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccountsTable.tsx (4)
packages/thunderstore-api/src/schemas/objectSchemas.ts (1)
  • TeamServiceAccount (378-378)
apps/cyberstorm-remix/app/root.tsx (1)
  • OutletContextShape (84-89)
apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccountRemoveModal.tsx (1)
  • ServiceAccountRemoveModal (18-104)
packages/cyberstorm/src/index.ts (1)
  • TableSort (103-103)
apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx (4)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx (1)
  • clientLoader (15-19)
apps/cyberstorm-remix/app/root.tsx (2)
  • clientLoader (122-173)
  • OutletContextShape (84-89)
apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts (1)
  • makeTeamSettingsTabLoader (8-29)
apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccountsTable.tsx (1)
  • ServiceAccountsTable (17-67)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MembersTable.tsx (3)
packages/thunderstore-api/src/index.ts (1)
  • RequestConfig (1-7)
packages/thunderstore-api/src/patch/teamEditMember.ts (1)
  • teamEditMember (19-43)
packages/thunderstore-api/src/delete/teamRemoveMember.ts (1)
  • teamRemoveMember (4-23)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MemberAddForm.tsx (4)
packages/thunderstore-api/src/index.ts (1)
  • RequestConfig (1-7)
packages/thunderstore-api/src/schemas/requestSchemas.ts (1)
  • TeamAddMemberRequestData (574-576)
packages/thunderstore-api/src/post/teamMember.ts (1)
  • teamAddMember (13-37)
apps/cyberstorm-remix/cyberstorm/utils/StrongForm/useStrongForm.ts (1)
  • useStrongForm (24-169)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx (4)
apps/cyberstorm-remix/app/root.tsx (2)
  • clientLoader (122-173)
  • OutletContextShape (84-89)
apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts (1)
  • makeTeamSettingsTabLoader (8-29)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MemberAddForm.tsx (1)
  • MemberAddForm (27-160)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MembersTable.tsx (1)
  • MembersTable (37-133)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build
  • GitHub Check: Generate visual diffs
  • GitHub Check: ESLint
🔇 Additional comments (6)
apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MembersTable.tsx (4)

1-35: LGTM!

Imports and constant definitions are clean and appropriate.


37-73: LGTM!

The ApiAction setup for role changes is well-structured with proper success/error handling and data refresh.


125-136: LGTM!

Table rendering is clean with sensible default sorting.


137-212: LGTM!

The RemoveTeamMemberForm correctly handles the removal flow with proper confirmation dialog, error handling, and modal state management. The modal only closes on successful removal, allowing users to retry on error.

apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx (2)

1-19: LGTM!

The loader correctly returns the members promise for deferred resolution via Await, enabling the Suspense pattern.


21-29: LGTM!

Component setup is clean and the revalidation function integrates well with the new pattern.

Comment on lines 75 to +79
function AddServiceAccountForm(props: {
teamName: string;
config: () => RequestConfig;
serviceAccountRevalidate?: () => void;
serviceAccountRevalidate: () => void;
}) {
const outletContext = useOutletContext() as OutletContextShape;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix prop type for serviceAccountRevalidate

Line 77 declares serviceAccountRevalidate as () => void, but the value passed in (serviceAccountRevalidate from the parent) is an async function returning Promise<void>. TypeScript will reject this assignment (Promise<void> is not assignable to void), so the build breaks. Update the prop type to accept the promised result (and keep the call site unchanged).

 function AddServiceAccountForm(props: {
   teamName: string;
-  serviceAccountRevalidate: () => void;
+  serviceAccountRevalidate: () => Promise<void>;
 }) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function AddServiceAccountForm(props: {
teamName: string;
config: () => RequestConfig;
serviceAccountRevalidate?: () => void;
serviceAccountRevalidate: () => void;
}) {
const outletContext = useOutletContext() as OutletContextShape;
function AddServiceAccountForm(props: {
teamName: string;
serviceAccountRevalidate: () => Promise<void>;
}) {
const outletContext = useOutletContext() as OutletContextShape;
🤖 Prompt for AI Agents
In
apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx
around lines 75 to 79, the AddServiceAccountForm prop serviceAccountRevalidate
is typed as () => void but the parent passes an async function returning
Promise<void>; update the prop type to () => Promise<void> so the types align
and the build succeeds, leaving the call site unchanged.

@anttimaki anttimaki requested a review from Roffenlund November 12, 2025 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants