Repository navigation
Updating a group with no group selected sends PATCH /v1/groups/undefined #1642
Description
useUpdateGroupMutation sends PATCH /v1/groups/undefined when no group is selected. The URL is built from currentGroup?.id, and nothing checks that a group exists first. The Manage Group page (/group/manage) can be reached by typing the URL with no group selected, because the nav only hides the link. It still renders an editable form, and saving it sends a request the API cannot satisfy. The user sees a generic server error instead of being told to pick a group. This breaks the repo's rule against failing silently on an undeclared state. GroupEmailTemplates already guards against this case and shows an empty state.
Where
apps/web/src/hooks/useUpdateGroupMutation.ts:24-29:
const currentGroup = useAppStore((store) => store.currentGroup);
// ...
const response = await axios.patch(`/v1/groups/${currentGroup?.id}`, data, {Caller with no guard: apps/web/src/routes/_app/group/manage.tsx:866 and :984-993.
Reproduce
- Sign in as an admin with no current group selected.
- Go to
/group/managedirectly, change a setting, and save.
Actual: the browser sends PATCH /v1/groups/undefined, and the API fails on the malformed ID.
Expected: the hook refuses the update before any request is sent, and the page asks the user to select a group.
Tests
apps/web/src/hooks/__tests__/useUpdateGroupMutation.test.ts: it.fails('should refuse to patch when no group is selected, so no request reaches /v1/groups/undefined'). Drop .fails once the hook rejects. If the fix also guards the Manage Group page, add a unit test for that page's empty state.
Suggested fix
In mutationFn, throw an explicit error when currentGroup is null before calling axios. Optionally give /group/manage the same "select a group" empty state that GroupEmailTemplates uses.
Found while raising unit test coverage in #1632.
Activity
joshunrau commentedon Oct 26, 2026
Still valid on
main(d86740e), but only inside the hook. The reproduction does not work, and did not when this was filed. With no group selected,dataisnull(apps/web/src/routes/_app/group/manage.tsx:939-948, since 87ce6b5 on 2026-07-11), soWithFallbackshowsLoadingFallbackforever and the form never renders.manage.test.tsx:198pins that spinner.GroupEmailTemplatesreturns its empty state before it can save (GroupEmailTemplates.tsx:52-64). Nothing in the app sendsPATCH /v1/groups/undefinedtoday.Typing the URL does not get there in production either. A full page load drops the in-memory
accessToken(it is not inPERSISTED_KEYS,apps/web/src/store/index.ts:19-24), so_app/route.tsx:20-23redirects to/auth/login, and login always lands on/dashboard(login.tsx:87). The state is reachable with the dev auth bypass for a user with no group, and in Playwright, whose injected token survivespage.goto. What a user can actually hit is a spinner that never stops, so the empty state is the fix, not an optional extra.Plan
useUpdateGroupMutation.ts: stop readingcurrentGroupand take the target as a variable,mutateAsync({ data, groupId }).useDeleteGroupMutationand the repo mutation atadmin/groups/index.tsx:33-41already use that shape. PassingcurrentGroup?.idthen fails type-checking. A runtime throw would be a guard neither caller can reach.manage.tsx: every hook inRouteComponentruns before its JSX, so no split is needed. Render a "Select a group" empty state in place ofWithFallbackwhencurrentGroupis null, with inlinet()inen,esandfrand adata-testid. MoveEmptyStatefromGroupEmailTemplates.tsx:27tosrc/components/EmptyState/rather than copying it.ManageGroupFormalready hasdata.groupId(:555, set at:942), so change theonSubmitprop (:104, called at:822) to take{ data, groupId }.GroupEmailTemplates.tsx:123: passgroupId.remote-assignments.tsx:74-76renders nothing with no group, header included (pinned byremote-assignments.test.tsx:115). Give it the same empty state in a follow-up.Tests
useUpdateGroupMutation.test.ts: delete theit.failsat:92-99rather than dropping.fails, since the no-group case no longer exists. DropcurrentGroupfrom the fake store (:12,:48). ThemutateAsynccalls in:51-90pass{ data, groupId: 'group-1' }.manage.test.tsx: invert:198toit('should ask the user to select a group when none is selected, rather than loading forever'). Move the mock type at:22,savedIdsat:146and thetoHaveBeenCalledWithcalls at:633and:650to the{ data, groupId }shape, and assertgroupId: 'group-1'in one of them.GroupEmailTemplates.test.tsx:147-155: also assert the patch URL,/v1/groups/group-1.testing/src/specs/group-manage.spec.ts: create a group and an admin withgroupIds: [], asgroup-switcher.spec.ts:34does. Sign in withauthenticateAsand opennew GroupManagePage(page).goto('/group/manage'). Assert that the empty state shows (add its locator toGroupManagePage) andsubmitButtondoes not. ThenswitchGroupinto the group and assert that the form appears. The existing saves ingroup-manage.spec.ts:98andgroup-email-templates.spec.tscover the new hook signature.Land this in one PR with #1691 and #1719, one commit per issue, as planned there. All three change
useUpdateGroupMutationandonSubmitatmanage.tsx:975-978.Triage note written with Claude Code. Code references point at
main@d86740e.