Skip to content

feat(editor): move the admin directory onto TanStack Query - #7726

Merged
Frooodle merged 2 commits into
mainfrom
claude/react-query-admin-reads
Aug 29, 2026
Merged

feat(editor): move the admin directory onto TanStack Query#7726
Frooodle merged 2 commits into
mainfrom
claude/react-query-admin-reads

Conversation

@ConnorYoh

@ConnorYoh ConnorYoh commented Aug 28, 2026

Copy link
Copy Markdown
Member

Description of Changes

Step 5 of the TanStack Query rollout, covering the admin People, Teams and Team details screens. Follows #7264, #7283, #7285.

The problem

Two separate ones, in the same three files.

Reads. Each section fetched and held its own copy of the same resources: People read the roster and the team list, Teams read the team list plus the roster again when its add-member modal opened, Team details read all three. Cost scaled with how many screens you visited rather than with how much data exists.

Writes. Thirteen handlers each did the same five things by hand: set a processing flag, call the service, toast the outcome, dig a message out of an axios error, and reload their own slice. Refreshing was a convention, not a mechanism, and one handler had already forgotten it.

The fix

Three shared query keys (adminUsers, teams, teamDetails), and one useAdminMutation helper that every write is declared against:

const createTeam = useAdminMutation({
  write: (name: string) => teamService.createTeam(name),
  invalidates: ["teams"],
  success: t("workspace.teams.createTeam.success"),
  errorFallback: t("workspace.teams.createTeam.error"),
  onDone: () => { setNewTeamName(""); setCreateModalOpened(false); },
});

Each write names the slices it disturbs, which is the part that only works when reads and writes are designed together: createTeam invalidates the team list, while a membership move invalidates the list, both teams' detail rows and the roster, because it genuinely changes all three. Invalidation refetches only mounted queries, so this costs nothing extra.

The blanket "invalidate everything" helper survives in exactly one role: child components (invite, password change, seat update) that write through their own services, where the affected scopes are not visible from the call site.

Why it is better, measured

Request counts come from one harness driving teams -> team details -> back -> people, run against the branch point and against this branch. The assertion is committed, so it cannot silently regress.

Before After
Requests 7 3
getTeams 4 1
getUsers 2 1
getTeamDetails 1 1
Committed renders 17 15

Three is one per distinct resource, the floor for that sequence. The four getTeams were the Teams table, Team details fetching the same list for its "move to team" dropdown, the explicit refresh on the back button, and People.

Renders barely move, which is expected: this changes where data lives, not how often React draws. It is reported because a caching change can quietly cost renders, and this one does not.

On the code itself, across the three sections:

Net lines -216
useState/useEffect removed 11, none added
Duplicated isAxiosError blocks 13 to 1
setProcessing calls 19 to 0

isAxiosError is no longer imported by any of the three files.

Bug fixed

disableMfaByAdmin showed a success toast and never refreshed. The menu item renders only when user.mfaEnabled is true, so an admin disabled MFA, was told it worked, and watched the option stay on screen until a manual reload. It is covered by a test that fails if the invalidation is removed.

Behaviour worth checking in review

  • A write no longer blocks its handler before closing the modal. The dialog closes when the write succeeds and the table updates when the refetch lands, rather than the button spinning through both.
  • Modal submit buttons now track their own mutation rather than one shared flag. Team details still derives a single busy flag, now from its five mutations rather than a useState, so its row actions disable together as before.
  • The per-handler console.error is kept, once, in the shared error path.

Testing

Five tests, each verified by breaking the implementation and confirming that one test, and only that one, fails:

Mutation Caught by
Drop the shared stale window (staleTime: 0) request-count test
Make invalidation a no-op write-visibility test
Ignore the login-enabled gate login-disabled test
Stop invalidating after the MFA write MFA-refresh test
Fall back to the generic error message server-message test

The write tests drive the real flows through their modals and menus rather than calling hooks directly.

task frontend:check passes typecheck, lint and oxfmt, and 2383 of 2385 editor tests. The two failures, workbenchSession.test.ts and notificationActions.test.tsx, are untouched here and fail identically with this branch's changes reverted.

Scope

The three services keep their current shape; nothing outside these three sections and the new hook module changes. Child modals that write through their own services still refresh via the blanket helper, and converting those is separate work.

People, Teams and Team details each fetched and held their own copy of the
team list and the admin roster. One pass through those screens cost seven
requests where three distinct resources were being read.

They now read through shared query keys, and the write paths invalidate the
cache instead of calling their own reload function, so a change made in one
view is visible in the others.

Measured over teams -> team details -> back -> people: 7 requests to 3, and
17 committed renders to 15.
@ConnorYoh
ConnorYoh requested review from a team and balazs-szucs as code owners August 28, 2026 10:34
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines ignoring generated files. enhancement New feature or request labels Aug 28, 2026
@github-actions github-actions Bot added the Front End Issues or pull requests related to front-end development label Aug 28, 2026
The thirteen write handlers each did the same five things by hand: set a
processing flag, call the service, toast the outcome, extract a message from
an axios error, and reload their own slice of the data. They are now
declarations against one helper that does all five, and each says which
slices of the directory it invalidates.

Doing this alongside the reads changes one earlier decision: invalidation is
per operation rather than blanket, because a mutation is the natural place to
record which resources a write disturbs. The blanket helper survives only for
child components that write through their own services.

Fixes the missing refresh after disabling a user's MFA, which left the row
showing the old state and the menu item on screen.
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines ignoring generated files. and removed size:XL This PR changes 500-999 lines ignoring generated files. labels Aug 28, 2026
@ConnorYoh ConnorYoh changed the title feat(editor): share the admin directory reads through TanStack Query feat(editor): move the admin directory onto TanStack Query Aug 28, 2026
@Frooodle
Frooodle added this pull request to the merge queue Aug 29, 2026
Merged via the queue into main with commit c22d9ec Aug 29, 2026
47 checks passed
@Frooodle
Frooodle deleted the claude/react-query-admin-reads branch August 29, 2026 00:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request Front End Issues or pull requests related to front-end development size:XXL This PR changes 1000+ lines ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants