Skip to content

refactor(cms-180): migrate studio admin data fetching to TanStack Query#113

Merged
iipanda merged 5 commits into
mainfrom
feat/cms-180-tanstack-query-migration
Apr 20, 2026
Merged

refactor(cms-180): migrate studio admin data fetching to TanStack Query#113
iipanda merged 5 commits into
mainfrom
feat/cms-180-tanstack-query-migration

Conversation

@iipanda
Copy link
Copy Markdown
Collaborator

@iipanda iipanda commented Apr 19, 2026

Summary

Finishes the TanStack Query migration in the Studio runtime that CMS-132 started. The last three raw useEffect + useState data-fetching sites in packages/studio are replaced with useQuery-based hooks:

  • Dashboard page (admin/page.tsx) fetches loadDashboardData through a new useDashboardData hook. The 200 ms skeleton-delay behavior is preserved with a single local effect so fast responses still avoid a loading flash.
  • Admin layout (admin/layout.tsx) is split into an outer QueryClientProvider wrapper and an inner AdminLayoutInner. Inside the provider, capabilities / session / environments are driven by useQuery. The token-mode capability-failure promotion to session.status = "token-error" is preserved through a derived memo.
  • Content overview page (pages/content-page.tsx) consumes loadStudioContentOverviewState through a new useStudioContentOverview hook.

Pure orchestration functions (loadDashboardData, loadStudioContentOverviewState) are untouched — the refactor is confined to the React data-fetching boundary. No external contracts change.

Jira: https://blazity.atlassian.net/browse/CMS-180

Test plan

  • bun run format:check clean
  • bun run check (Nx build + typecheck for all 6 projects) green
  • Targeted unit tests pass: layout.test.tsx, content-page.test.tsx, content-overview-state.test.ts, dashboard-data.test.ts (33/33)
  • Full studio suite: 518/519 pass (the 1 failure is an unrelated pre-existing, untracked button.test.tsx on the working tree, not introduced by this PR)
  • Manual smoke: reviewer exercises admin dashboard, content overview, and environment switching against a running Studio to confirm no visible regression in loading / token-error / unauthenticated flows

Summary by CodeRabbit

  • Refactor

    • Centralized admin layout provider and split inner layout to consolidate query client and simplify data loading across admin, dashboard, and content pages while preserving prior results during refetches.
  • New Features

    • Declarative React Query hooks for dashboard and content overview with stable query keys, gating, and placeholder data to improve loading behavior.
  • Bug Fixes

    • More deterministic session/auth/error handling, token-mode vs cookie-mode distinctions, and detection of bearer token rotation to trigger refreshed studio data.

Replaces the last three raw useEffect+useState data-fetching sites in the
Studio admin runtime with TanStack Query hooks, matching the pattern
established in CMS-132:

- admin/page.tsx (dashboard) now fetches loadDashboardData through a new
  useDashboardData hook; the 200ms skeleton-delay is preserved via a
  single local effect.
- admin/layout.tsx is split into an outer QueryClientProvider wrapper
  and an inner component whose capabilities, session, and environments
  are driven by useQuery. The token-mode capability failure promotion
  to session.status=token-error is preserved through a derived memo.
- pages/content-page.tsx now consumes loadStudioContentOverviewState
  through a new useStudioContentOverview hook.

No external contracts or behaviors change; all existing layout,
content-page, dashboard-data, and content-overview-state tests pass.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors admin UI to use TanStack React Query for capabilities, session, and environments; splits AdminLayout into an outer QueryClientProvider and AdminLayoutInner; adds extractStatusCode; and introduces query-based hooks for dashboard and content overview flows.

Changes

Cohort / File(s) Summary
Admin layout & session/capabilities
packages/studio/src/lib/runtime-ui/app/admin/layout.tsx
Split AdminLayout into outer QueryClientProvider + AdminLayoutInner. Replaced imperative effects with useQuery for capabilities, session, and environments. Added extractStatusCode(error) and deterministic token-mode error mapping. Adjusted auth redirect gating and added token-rotation invalidation logic.
Dashboard page → hook
packages/studio/src/lib/runtime-ui/app/admin/page.tsx
Replaced imperative dashboard data loading with useDashboardData hook + React Query; preserved 200ms skeleton timing and converted error handling to derive state from query.error / query.data.
New hooks: dashboard & content overview
packages/studio/src/lib/runtime-ui/hooks/use-dashboard-data.ts, packages/studio/src/lib/runtime-ui/hooks/use-content-overview.ts
Added query-key factories and React Query hooks (useDashboardData, useStudioContentOverview) that build stable query keys, memoize inputs/APIs, and call existing loader functions with enabled gating.
Content page integration
packages/studio/src/lib/runtime-ui/pages/content-page.tsx
Replaced local state/effect and 200ms timer with useStudioContentOverview; derive page state from query data/error/idle with fallback to missing-route state.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AdminLayout
  participant QueryClient
  participant CapabilitiesAPI
  participant SessionAPI
  participant EnvironmentsAPI

  Client->>AdminLayout: render children
  AdminLayout->>QueryClient: provide React Query context
  AdminLayout->>CapabilitiesAPI: useQuery(capabilities) [keyed on project/env/server/auth]
  AdminLayout->>SessionAPI: useQuery(session) [cookie-only or token-derived state]
  AdminLayout->>EnvironmentsAPI: useQuery(environments) [enabled when inputs present]
  CapabilitiesAPI-->>AdminLayout: capabilities / error
  SessionAPI-->>AdminLayout: session / error
  EnvironmentsAPI-->>AdminLayout: environments / error
  AdminLayout->>AdminLayout: derive permissions & sessionState (uses extractStatusCode)
  AdminLayout->>QueryClient: invalidateQueries(["studio"]) on token rotation
  AdminLayout->>Client: redirect if unauthenticated and not token mode
  Client->>AdminLayout: render inner UI with fetched data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐇
I hopped through effects to plant a queue,
Queries now bloom where side-effects grew,
Tokens checked, sessions tidy and spun,
Layout split, hooks hum — new flows begun,
A carrot-chip cheer for code well done!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: migrating studio admin data fetching from useEffect/useState to TanStack Query across multiple components (layout, page, content-page).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cms-180-tanstack-query-migration

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

Copy link
Copy Markdown

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx`:
- Around line 272-283: The query key for capabilitiesQuery currently embeds the
raw bearer token via capabilitiesLoadInput.auth.token—remove any token or
sensitive auth fields from the queryKey (keep project, environment, serverUrl,
auth.mode only) and instead store the token in memory/context and inject it
inside the queryFn using a custom fetch wrapper that reads the token from that
secure store; update any other queries that used the token in keys (same pattern
at the other occurrences) and implement token refresh handling by wiring
QueryCache.onError to detect 401 responses, refresh the token, and call
queryClient.invalidateQueries() so queries re-run with the new token.

In `@packages/studio/src/lib/runtime-ui/hooks/use-content-overview.ts`:
- Around line 13-18: The query key returned by getContentOverviewQueryKey is
missing discriminators (apiBaseUrl, supportedLocales, auth) that affect
loadInput and thus the query result; update getContentOverviewQueryKey to
include the current apiBaseUrl, supportedLocales, and auth (or a stable auth
identifier) alongside project and environment so TanStack Query v5 correctly
caches/invalidation, and ensure any other usage of this key (the hook that calls
loadInput and related functions) uses the expanded key so changes to apiBaseUrl,
supportedLocales or auth will refetch rather than reuse stale data.

In `@packages/studio/src/lib/runtime-ui/hooks/use-dashboard-data.ts`:
- Around line 15-20: getDashboardDataQueryKey currently only includes project
and environment so React Query can return stale dashboard data when the API host
changes; update the getDashboardDataQueryKey function to accept and include
apiBaseUrl (and optionally a non-sensitive auth discriminator) in the returned
tuple (i.e., ["studio","dashboard", project, environment, apiBaseUrl]) so
different API targets produce distinct keys; additionally, ensure callers that
build the apis memo or change auth either pass a lightweight, non-sensitive auth
discriminator into this key or call
queryClient.invalidateQueries(getDashboardDataQueryKey(...)) when credentials
rotate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 822c0535-c998-4a48-909b-ee5ef013eeee

📥 Commits

Reviewing files that changed from the base of the PR and between 4aa3401 and ce74964.

📒 Files selected for processing (5)
  • packages/studio/src/lib/runtime-ui/app/admin/layout.tsx
  • packages/studio/src/lib/runtime-ui/app/admin/page.tsx
  • packages/studio/src/lib/runtime-ui/hooks/use-content-overview.ts
  • packages/studio/src/lib/runtime-ui/hooks/use-dashboard-data.ts
  • packages/studio/src/lib/runtime-ui/pages/content-page.tsx

Comment thread packages/studio/src/lib/runtime-ui/app/admin/layout.tsx
Comment thread packages/studio/src/lib/runtime-ui/hooks/use-content-overview.ts
Comment thread packages/studio/src/lib/runtime-ui/hooks/use-dashboard-data.ts
- Drop raw auth.token from the three admin-layout query keys (capabilities,
  session, environments). Keep auth.mode as the auth discriminator, matching
  the pattern already used by use-user-list.ts, use-content-type-list.ts, and
  use-create-document.ts. Tokens still flow to requests via the API factory
  options, not via the query cache.
- Expand getDashboardDataQueryKey to include apiBaseUrl and auth.mode so a
  host-URL or auth-mode change produces a distinct cache entry.
- Expand getContentOverviewQueryKey to include apiBaseUrl, auth.mode, and
  supportedLocales; the last one flows into entries[i].locales inside
  loadStudioContentOverviewState, so it must be part of the cache key.
Copy link
Copy Markdown

@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)
packages/studio/src/lib/runtime-ui/app/admin/layout.tsx (1)

272-289: ⚠️ Potential issue | 🟠 Major

Invalidate Studio queries when the bearer token changes.

Now that the key only discriminates on auth.mode, a host-side token refresh or user switch can leave this query, environmentsQuery, and child studio/* queries pinned to the previous cache/error state until they go stale. In the bad case, an old 401/403 here keeps sessionState stuck in "token-error" even after the host supplies a fresh token. Add an auth-change invalidation/removal path at the layout level so the shared query client refetches with the new credentials. Based on learnings: Studio must be embedded in a host React application and rendered in a catch-all route.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx` around lines 272 -
289, The capabilities query only keys on auth.mode so token changes aren't
triggering refetch; add logic at the layout level to invalidate or remove all
"studio" queries when the auth credentials change (watch
capabilitiesLoadInput!.auth, e.g. the bearer token) so capabilitiesQuery,
environmentsQuery and child "studio/*" queries refetch with new credentials; use
the shared react-query client to call invalidateQueries or removeQueries for the
"studio" namespace (or specific keys used) when the auth token (not just
auth.mode) is updated, ensuring createStudioCurrentPrincipalCapabilitiesApi and
dependent queries refetch and sessionState can recover from prior 401/403.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx`:
- Around line 392-399: The current code treats missing capabilitiesQuery.data as
"no permissions" by defaulting each boolean to false; instead, stop collapsing
absent data into false and either (A) gate UI on capabilitiesQuery.isPending /
capabilitiesQuery.isError so the app shows loading/error state rather than
hiding features, or (B) preserve the last successful capabilities set and use
that until a new successful fetch arrives. Concretely, stop using expressions
like capabilities?.schema.read ?? false; read nested fields safely (e.g.
capabilities?.schema?.read) and if undefined use a preservedLastCapabilities
state/ref (updated only on successful fetch) or short-circuit rendering when
capabilitiesQuery.isPending/error; update uses of canReadSchema,
canCreateContent, canPublishContent, canUnpublishContent, canDeleteContent,
canManageUsers, and canManageSettings to rely on these non-defaulted values so
network/500 errors are not mistaken for an authorization denial.

---

Duplicate comments:
In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx`:
- Around line 272-289: The capabilities query only keys on auth.mode so token
changes aren't triggering refetch; add logic at the layout level to invalidate
or remove all "studio" queries when the auth credentials change (watch
capabilitiesLoadInput!.auth, e.g. the bearer token) so capabilitiesQuery,
environmentsQuery and child "studio/*" queries refetch with new credentials; use
the shared react-query client to call invalidateQueries or removeQueries for the
"studio" namespace (or specific keys used) when the auth token (not just
auth.mode) is updated, ensuring createStudioCurrentPrincipalCapabilitiesApi and
dependent queries refetch and sessionState can recover from prior 401/403.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 347e0eb7-7b13-4cea-89d1-5a733cedbb13

📥 Commits

Reviewing files that changed from the base of the PR and between ce74964 and dfe5310.

📒 Files selected for processing (3)
  • packages/studio/src/lib/runtime-ui/app/admin/layout.tsx
  • packages/studio/src/lib/runtime-ui/hooks/use-content-overview.ts
  • packages/studio/src/lib/runtime-ui/hooks/use-dashboard-data.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/studio/src/lib/runtime-ui/hooks/use-content-overview.ts

Comment thread packages/studio/src/lib/runtime-ui/app/admin/layout.tsx
…switch

TanStack Query v5 resets query.data to undefined when the queryKey changes
and no cache entry exists for the new key. That made the capability flags
and the environments list briefly collapse to default values during an
environment switch — the sidebar would flash "no permissions" until the
new fetch resolved.

The pre-refactor useEffect only called the setters inside .then/.catch, so
old flags persisted until the new response arrived. Restore that
continuity by adding placeholderData: keepPreviousData to both queries.

The sessionQuery does not need this — its key is keyed on serverUrl and
auth.mode, neither of which changes mid-session.
Copy link
Copy Markdown

@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.

♻️ Duplicate comments (2)
packages/studio/src/lib/runtime-ui/app/admin/layout.tsx (2)

276-298: ⚠️ Potential issue | 🟠 Major

Invalidate the auth-scoped queries when the token changes.

Lines 276-298 and 378-402 remove the bearer token from the key, which is correct, but there is no replacement invalidation path when context.auth.token changes. With the same queryKey, TanStack Query will keep the existing cache/error state, so a refreshed or corrected token can leave capabilities/environments stuck until a reload or unrelated refetch.

Possible fix
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 import {
   QueryClientProvider,
   keepPreviousData,
   useQuery,
+  useQueryClient,
 } from "@tanstack/react-query";
@@
 function AdminLayoutInner({
@@
 }) {
+  const queryClient = useQueryClient();
   const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+  const previousTokenRef = useRef(context.auth.token);
@@
+  useEffect(() => {
+    if (context.auth.mode !== "token") {
+      previousTokenRef.current = context.auth.token;
+      return;
+    }
+
+    if (previousTokenRef.current === context.auth.token) {
+      return;
+    }
+
+    previousTokenRef.current = context.auth.token;
+
+    void queryClient.invalidateQueries({ queryKey: ["studio", "capabilities"] });
+    void queryClient.invalidateQueries({ queryKey: ["studio", "environments"] });
+  }, [queryClient, context.auth.mode, context.auth.token]);

Also applies to: 378-402

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx` around lines 276 -
298, The query key was intentionally made token-agnostic, but you must
invalidate the auth-scoped queries when the bearer token changes so stale cached
results don't persist; add a useEffect that watches the auth token (e.g.
capabilitiesLoadInput?.auth.token or context.auth.token) and calls
queryClient.invalidateQueries with the same key shape used by capabilitiesQuery
(["studio","capabilities", capabilitiesLoadInput?.config.project,
capabilitiesLoadInput?.config.environment,
capabilitiesLoadInput?.config.serverUrl, capabilitiesLoadInput?.auth.mode]) to
force refetch on token change; apply the same pattern for the analogous
environments query (the other block around lines 378-402) using its query key.

404-413: ⚠️ Potential issue | 🟠 Major

Don’t treat missing capability data as “no permissions.”

Lines 404-413 still collapse undefined capability data into false for every permission, so first-load and non-auth failures silently hide features instead of surfacing the outage. Also, reads like capabilities?.schema.read are still unsafe if a nested section is omitted; that needs capabilities?.schema?.read.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx` around lines 404 -
413, The current code collapses missing capabilities into false and uses unsafe
nested property access; update the permissions logic so you do not coerce
undefined capability data into false and use safe optional chaining for nested
fields: read capabilities from capabilitiesQuery.data (keep it undefined/null
when missing) and set canReadSchema = capabilities?.schema?.read,
canCreateContent = capabilities?.content?.write, canPublishContent =
capabilities?.content?.publish, canUnpublishContent =
capabilities?.content?.unpublish, canDeleteContent =
capabilities?.content?.delete, canManageUsers = capabilities?.users?.manage, and
canManageSettings = capabilities?.settings?.manage (no default to false), then
handle undefined permissions elsewhere (e.g., show loading/error state) so
outages aren’t silently treated as “no permissions.”
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx`:
- Around line 276-298: The query key was intentionally made token-agnostic, but
you must invalidate the auth-scoped queries when the bearer token changes so
stale cached results don't persist; add a useEffect that watches the auth token
(e.g. capabilitiesLoadInput?.auth.token or context.auth.token) and calls
queryClient.invalidateQueries with the same key shape used by capabilitiesQuery
(["studio","capabilities", capabilitiesLoadInput?.config.project,
capabilitiesLoadInput?.config.environment,
capabilitiesLoadInput?.config.serverUrl, capabilitiesLoadInput?.auth.mode]) to
force refetch on token change; apply the same pattern for the analogous
environments query (the other block around lines 378-402) using its query key.
- Around line 404-413: The current code collapses missing capabilities into
false and uses unsafe nested property access; update the permissions logic so
you do not coerce undefined capability data into false and use safe optional
chaining for nested fields: read capabilities from capabilitiesQuery.data (keep
it undefined/null when missing) and set canReadSchema =
capabilities?.schema?.read, canCreateContent = capabilities?.content?.write,
canPublishContent = capabilities?.content?.publish, canUnpublishContent =
capabilities?.content?.unpublish, canDeleteContent =
capabilities?.content?.delete, canManageUsers = capabilities?.users?.manage, and
canManageSettings = capabilities?.settings?.manage (no default to false), then
handle undefined permissions elsewhere (e.g., show loading/error state) so
outages aren’t silently treated as “no permissions.”

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 71806741-648d-41ab-81ab-fd29add4ccc3

📥 Commits

Reviewing files that changed from the base of the PR and between dfe5310 and 5c53ace.

📒 Files selected for processing (1)
  • packages/studio/src/lib/runtime-ui/app/admin/layout.tsx

The previous round dropped the bearer token from query keys to keep secrets
out of the TanStack Query cache. Pre-refactor, the capabilities/session/
environments useEffects tracked context.auth.token as a dependency and
re-fetched when the host rotated the token. After removing the token from
keys, that reactivity was lost — a new token would reuse the previous
token's cached response until the cache expired.

Add a single useEffect inside AdminLayoutInner that invalidates every
query under the "studio" prefix whenever context.auth.token changes. That
covers capabilities, session, environments, dashboard, and content
overview in one place. The token still never enters a query key.
Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
packages/studio/src/lib/runtime-ui/app/admin/layout.tsx (1)

416-423: ⚠️ Potential issue | 🟠 Major

Capabilities still collapse fetch absence/error into “no permissions.”

Defaulting every flag to false here still makes pending/error states look like authorization denial. That can silently hide features during outages. Handle pending/error explicitly (or gate rendering), then map permissions from successful data only. Also prefer nested optional access (?.schema?.read, etc.) for resilience to partial payloads.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx` around lines 416 -
423, The code is collapsing fetch pending/error into “no permissions” by
defaulting flags to false; update the mapping so you only derive
canReadSchema/canCreateContent/canPublishContent/canUnpublishContent/canDeleteContent/canManageUsers/canManageSettings
from capabilitiesQuery.data when the query is successful (e.g., check
capabilitiesQuery.isSuccess or Boolean(capabilitiesQuery.data)) and otherwise
leave them undefined or handle loading/error states upstream to gate rendering;
also use nested optional chaining when reading properties
(capabilities?.schema?.read, capabilities?.content?.write, etc.) to avoid
crashes on partial payloads and ensure UI shows loading/error rather than
silently hiding features.
🧹 Nitpick comments (1)
packages/studio/src/lib/runtime-ui/app/admin/layout.tsx (1)

261-263: Avoid invalidating all studio queries on first mount.

This effect runs once on mount (not only on rotation), so it can trigger unnecessary refetch churn. Invalidate only when the token actually changes from a previous value.

♻️ Proposed adjustment
+import { useRef } from "react";
+
   const queryClient = useQueryClient();
+  const previousTokenRef = useRef<string | undefined>(context.auth.token);
+
   useEffect(() => {
-    void queryClient.invalidateQueries({ queryKey: ["studio"] });
+    const previousToken = previousTokenRef.current;
+    const nextToken = context.auth.token;
+    if (previousToken !== nextToken) {
+      void queryClient.invalidateQueries({ queryKey: ["studio"] });
+      previousTokenRef.current = nextToken;
+    }
   }, [queryClient, context.auth.token]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx` around lines 261 -
263, The effect currently invalidates all "studio" queries on first mount
because it runs whenever the component mounts; update the useEffect that calls
queryClient.invalidateQueries({ queryKey: ["studio"] }) so it only invalidates
when context.auth.token actually changes from a previous value: track the
previous token (e.g., with a ref) inside the effect and only call
queryClient.invalidateQueries when prevToken.current exists and
prevToken.current !== context.auth.token, then set prevToken.current =
context.auth.token; keep the effect dependencies as [queryClient,
context.auth.token].
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx`:
- Around line 416-423: The code is collapsing fetch pending/error into “no
permissions” by defaulting flags to false; update the mapping so you only derive
canReadSchema/canCreateContent/canPublishContent/canUnpublishContent/canDeleteContent/canManageUsers/canManageSettings
from capabilitiesQuery.data when the query is successful (e.g., check
capabilitiesQuery.isSuccess or Boolean(capabilitiesQuery.data)) and otherwise
leave them undefined or handle loading/error states upstream to gate rendering;
also use nested optional chaining when reading properties
(capabilities?.schema?.read, capabilities?.content?.write, etc.) to avoid
crashes on partial payloads and ensure UI shows loading/error rather than
silently hiding features.

---

Nitpick comments:
In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx`:
- Around line 261-263: The effect currently invalidates all "studio" queries on
first mount because it runs whenever the component mounts; update the useEffect
that calls queryClient.invalidateQueries({ queryKey: ["studio"] }) so it only
invalidates when context.auth.token actually changes from a previous value:
track the previous token (e.g., with a ref) inside the effect and only call
queryClient.invalidateQueries when prevToken.current exists and
prevToken.current !== context.auth.token, then set prevToken.current =
context.auth.token; keep the effect dependencies as [queryClient,
context.auth.token].

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 14f43652-4711-48ee-83bd-c1643bb5f563

📥 Commits

Reviewing files that changed from the base of the PR and between 5c53ace and 045d679.

📒 Files selected for processing (1)
  • packages/studio/src/lib/runtime-ui/app/admin/layout.tsx

Guard the bearer-token invalidation effect with a useRef so it only fires
when the token actually changes from a previous value. On first mount the
queries are in the middle of registering their initial fetch; invalidating
them would mark them stale and trigger a redundant refetch once the first
fetch settles.
Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
packages/studio/src/lib/runtime-ui/app/admin/layout.tsx (1)

419-426: ⚠️ Potential issue | 🟠 Major

Don’t map missing/failed capabilities to hard “no permissions,” and fix unsafe nested access.

Line 420–426 still collapses unavailable capability data into false, so first-load/network/server failures are indistinguishable from real authorization denial. Also, expressions like capabilities?.schema.read are not fully safe if schema is absent.

Suggested fix
-  const capabilities = capabilitiesQuery.data?.capabilities;
-  const canReadSchema = capabilities?.schema.read ?? false;
-  const canCreateContent = capabilities?.content.write ?? false;
-  const canPublishContent = capabilities?.content.publish ?? false;
-  const canUnpublishContent = capabilities?.content.unpublish ?? false;
-  const canDeleteContent = capabilities?.content.delete ?? false;
-  const canManageUsers = capabilities?.users.manage ?? false;
-  const canManageSettings = capabilities?.settings.manage ?? false;
+  const capabilities = capabilitiesQuery.data?.capabilities;
+
+  if (!capabilities && (capabilitiesQuery.isPending || capabilitiesQuery.isError)) {
+    return (
+      <div className="flex min-h-screen items-center justify-center bg-background">
+        <div className="text-foreground-muted text-sm">
+          {capabilitiesQuery.isError
+            ? "Capabilities could not be loaded."
+            : "Loading capabilities..."}
+        </div>
+      </div>
+    );
+  }
+
+  const canReadSchema = capabilities?.schema?.read ?? false;
+  const canCreateContent = capabilities?.content?.write ?? false;
+  const canPublishContent = capabilities?.content?.publish ?? false;
+  const canUnpublishContent = capabilities?.content?.unpublish ?? false;
+  const canDeleteContent = capabilities?.content?.delete ?? false;
+  const canManageUsers = capabilities?.users?.manage ?? false;
+  const canManageSettings = capabilities?.settings?.manage ?? false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx` around lines 419 -
426, The code currently coerces missing/failed capability data into false and
uses unsafe nested access; change each capability binding (canReadSchema,
canCreateContent, canPublishContent, canUnpublishContent, canDeleteContent,
canManageUsers, canManageSettings) to preserve undefined when data is absent and
use safe optional chaining into nested objects (e.g., capabilities?.schema?.read
instead of capabilities?.schema.read ?? false), removing the "?? false" so
callers can distinguish unknown vs explicit false and handle capabilitiesQuery
errors/loading separately.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/studio/src/lib/runtime-ui/app/admin/layout.tsx`:
- Around line 419-426: The code currently coerces missing/failed capability data
into false and uses unsafe nested access; change each capability binding
(canReadSchema, canCreateContent, canPublishContent, canUnpublishContent,
canDeleteContent, canManageUsers, canManageSettings) to preserve undefined when
data is absent and use safe optional chaining into nested objects (e.g.,
capabilities?.schema?.read instead of capabilities?.schema.read ?? false),
removing the "?? false" so callers can distinguish unknown vs explicit false and
handle capabilitiesQuery errors/loading separately.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d8d54742-c559-403f-b861-0ddd7a15ca7b

📥 Commits

Reviewing files that changed from the base of the PR and between 045d679 and 1fef55b.

📒 Files selected for processing (1)
  • packages/studio/src/lib/runtime-ui/app/admin/layout.tsx

@iipanda iipanda merged commit c8f173c into main Apr 20, 2026
5 checks passed
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.

1 participant