Team selector ux fix - #31
Conversation
Team visibility was driven entirely by the sidebar switcher, which starts on "All teams" every session. A caller with one team was told to go pick the only team they have, and the form flagged the field red the moment they chose "Team" — before they had done anything wrong. Resolve the team in the form instead: an explicit choice, else the sidebar's active team, else the caller's personal (or only) team. Callers in more than one team now pick inline via a new TeamSelect rather than being sent to the sidebar, and the requirement is raised on submit rather than on entering team visibility. The sidebar switcher stays authoritative for an open form (#5077) until the caller picks a team in the selector. Signed-off-by: Anna Effort <anna.effort@ibm.com>
d530df8 to
7d79a6b
Compare
gcgoncalves
left a comment
There was a problem hiding this comment.
Nice validation addition. 👏
marekdano
left a comment
There was a problem hiding this comment.
Findings
1. Editing a team-scoped MCP server silently reassigns its team (CRITICAL)
File: src/components/mcp-servers/AdvancedSettings.tsx:151
resolveTeamId(teams, selectedTeamId) is called without the record's current teamId as the explicit third argument, so opening the edit form for a team-scoped MCP server silently overwrites its team as soon as team data loads.
Failure scenario: A caller with a fresh session (selectedTeamId is null — AuthContext resets it on every login) opens the edit form for an MCP server scoped to a shared team that isn't their personal team. On mount, pickedInForm is false, so the effect runs resolveTeamId(teams, null), which falls back to the caller's personal team — differing from the server's actual teamId — and fires onTeamIdChange(personalTeamId) immediately, before the user touches anything. If they submit without noticing, the server is reassigned from the shared team to their personal team. This exact overwrite is even asserted as expected behavior by the existing test "propagates selectedTeamId change after teamId is already set (regression: was ignored by !teamId guard)" in AdvancedSettings.test.tsx.
2. Same defect in the Tools form (CRITICAL)
File: src/components/tools/ToolAdvancedSettings.tsx:93
Identical bug: resolveTeamId(teams, selectedTeamId) omits the tool's current teamId as the explicit override, so editing a team-scoped tool silently reassigns its team on mount.
Failure scenario: Same as above — opening the edit form for a tool scoped to a non-personal team, with the sidebar on its default "All teams," retargets the tool to the caller's personal team before they interact with the selector. Notably, usePromptForm.ts (same PR) avoids this exact bug by seeding chosenTeamId from initialValues.teamId and passing it as resolveTeamId's third argument — that protection was not applied to the Tools/Servers forms.
3. Team-resolution logic duplicated instead of centralized (altitude)
File: src/components/mcp-servers/AdvancedSettings.tsx:144
The pickedInForm state + useEffect calling resolveTeamId is duplicated near-verbatim in ToolAdvancedSettings.tsx, despite this PR's stated goal of centralizing the sidebar-mirroring logic that used to be copy-pasted per form.
Cost: Because this effect lives separately in both files rather than in a shared hook (e.g., alongside resolveTeamId in useTeams.ts), findings #1 and #2 had to be (and were) introduced independently twice, and a fix must likewise be applied twice.
4. Unused requiresSelection field (dead code)
File: src/hooks/useTeams.ts:13
useTeams()'s requiresSelection is computed and unit-tested but never consumed - AdvancedSettings, ToolAdvancedSettings, and usePromptForm all read only teams, and TeamSelect.tsx re-derives its own teams.length < 2 check independently.
Cost: Not a runtime bug, but a maintenance trap — a future call site could wire up requiresSelection while TeamSelect uses its own separate threshold, creating two sources of truth that can drift.
UX issue
If an user has only one team, they see an error message in a form they are filling out and are required to leave their workflow to find the team switcher and make a selection, even if they are only part of 1 team. e.g.
Changes
The sidebar switcher stays authoritative for an open form (#5077) until the caller picks a team in the selector.
Root cause
Team visibility was driven entirely by the sidebar switcher, and
selectedTeamIdstarts asnull("All teams") every session (AuthContext.tsx, also reset on login). The forms never looked at the actual team list, so they could not tell "no team chosen" apart from "only one team exists". A second, independent defect:usePromptFormre-validated in an effect whenever visibility wasteam, so the field turned red the moment the user picked "Team"... before they had done anything wrong.What was added
src/hooks/useTeams.ts:useTeams()plusresolveTeamId(teams, selectedTeamId, explicitTeamId). Resolution order: explicit choice → sidebar's active team → the caller's personal team → first team. The last two steps are what keep a single-team caller from ever being asked, and give multi-team callers a sane default instead of an empty required field behind a disabled submit button.src/components/common/TeamSelect.tsx: renders nothing below two teams; renders a labeled select above. It still renders an error with no selector, so a failed/teamsload explains an inert submit button rather than failing silently.Both are shared by all three forms, which previously each had their own copy of the sidebar-mirroring logic: prompts (
usePromptForm+PromptForm), tools (ToolAdvancedSettings), servers (mcp-servers/AdvancedSettings).Other changes
visibilitytoteamIdin the prompt schema, so the message lands on the selector rather than the visibility dropdown. APIteam_idfield errors now map toteamIdtoo (previouslyvisibility).prompts.add.visibility.team.selectedHintand...selectFromSidebarHint; addedcommon.team.label,common.team.placeholder,common.required. The hardcoded English equivalents in the tool and server forms are gone as well.-
teamErrorprop added toToolAdvancedSettingsandmcp-servers/AdvancedSettings—errors.teamIdexisted in both form hooks but was never rendered anywhere.-
usePromptFormnow owns team state (teamId+setTeamId) instead of deriving it read-only from the sidebar.Testing
npm testgreen: 161 files, 2857 passed / 1 skipped. Typecheck and lint clean.New:
useTeams.test.ts,TeamSelect.test.tsx. Updated the team-visibility cases inusePromptForm.test.ts,PromptForm.test.tsx,MCPServerForm.test.tsx,AdvancedSettings.test.tsx,ToolAdvancedSettings.test.tsxThe old ones asserted the sidebar hint text and the pre-submit error, both of which are the behavior being removed.
Notes
teamvisibility, the sidebar/default wins until the user touches the selector, so editing a team-scoped server can retarget it. This is pre-existing (the old code mirroredselectedTeamIdunconditionally) and not introduced here. We should consider changing this as a follow up.MCPServerForm.test.tsx > team visibility > shows the selector for several teamsasserts only that the selector renders, not that it defaults to the personal team. The state does propagate correctly (verified by instrumenting the effect); that MSW-driven test's DOM would not settle on it. The defaulting behavior is asserted inAdvancedSettings.test.tsxandPromptForm.test.tsx.ToolAuth.test.tsx > shows team scope hint.... The hint no longer exists, and that file has no/teamsmock, so any replacement would have passed for the wrong reason. Equivalent coverage lives inToolAdvancedSettings.test.tsx.