Skip to content

Updating a group with no group selected sends PATCH /v1/groups/undefined #1642

Open

Description

@joshunrau

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

  1. Sign in as an admin with no current group selected.
  2. Go to /group/manage directly, 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

  1. added
    BugType: existing behavior is wrong
    Priority: LowNice to have, cosmetic or speculative
    Difficulty: LowIsolated change, about 2 hours or less
    Area: Webapps/web (clinician-facing React SPA)
    Good First IssueWell-scoped, low-risk and self-contained; a good entry point for a new contributor
    on Oct 5, 2026
  2. joshunrau commented on Oct 26, 2026

    @joshunrau
    CollaboratorAuthor

    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, data is null (apps/web/src/routes/_app/group/manage.tsx:939-948, since 87ce6b5 on 2026-07-11), so WithFallback shows LoadingFallback forever and the form never renders. manage.test.tsx:198 pins that spinner. GroupEmailTemplates returns its empty state before it can save (GroupEmailTemplates.tsx:52-64). Nothing in the app sends PATCH /v1/groups/undefined today.

    Typing the URL does not get there in production either. A full page load drops the in-memory accessToken (it is not in PERSISTED_KEYS, apps/web/src/store/index.ts:19-24), so _app/route.tsx:20-23 redirects 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 survives page.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

    1. useUpdateGroupMutation.ts: stop reading currentGroup and take the target as a variable, mutateAsync({ data, groupId }). useDeleteGroupMutation and the repo mutation at admin/groups/index.tsx:33-41 already use that shape. Passing currentGroup?.id then fails type-checking. A runtime throw would be a guard neither caller can reach.
    2. manage.tsx: every hook in RouteComponent runs before its JSX, so no split is needed. Render a "Select a group" empty state in place of WithFallback when currentGroup is null, with inline t() in en, es and fr and a data-testid. Move EmptyState from GroupEmailTemplates.tsx:27 to src/components/EmptyState/ rather than copying it. ManageGroupForm already has data.groupId (:555, set at :942), so change the onSubmit prop (:104, called at :822) to take { data, groupId }.
    3. GroupEmailTemplates.tsx:123: pass groupId.

    remote-assignments.tsx:74-76 renders nothing with no group, header included (pinned by remote-assignments.test.tsx:115). Give it the same empty state in a follow-up.

    Tests

    • useUpdateGroupMutation.test.ts: delete the it.fails at :92-99 rather than dropping .fails, since the no-group case no longer exists. Drop currentGroup from the fake store (:12, :48). The mutateAsync calls in :51-90 pass { data, groupId: 'group-1' }.
    • manage.test.tsx: invert :198 to it('should ask the user to select a group when none is selected, rather than loading forever'). Move the mock type at :22, savedIds at :146 and the toHaveBeenCalledWith calls at :633 and :650 to the { data, groupId } shape, and assert groupId: 'group-1' in one of them.
    • GroupEmailTemplates.test.tsx:147-155: also assert the patch URL, /v1/groups/group-1.
    • e2e, testing/src/specs/group-manage.spec.ts: create a group and an admin with groupIds: [], as group-switcher.spec.ts:34 does. Sign in with authenticateAs and open new GroupManagePage(page).goto('/group/manage'). Assert that the empty state shows (add its locator to GroupManagePage) and submitButton does not. Then switchGroup into the group and assert that the form appears. The existing saves in group-manage.spec.ts:98 and group-email-templates.spec.ts cover the new hook signature.

    Land this in one PR with #1691 and #1719, one commit per issue, as planned there. All three change useUpdateGroupMutation and onSubmit at manage.tsx:975-978.

    Triage note written with Claude Code. Code references point at main @ d86740e.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: Webapps/web (clinician-facing React SPA)BugType: existing behavior is wrongDifficulty: LowIsolated change, about 2 hours or lessGood First IssueWell-scoped, low-risk and self-contained; a good entry point for a new contributorPriority: LowNice to have, cosmetic or speculative

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions