diff --git a/src/components/layout/use-nav-groups.ts b/src/components/layout/use-nav-groups.ts
index ca9e3661..9dd1e1b9 100644
--- a/src/components/layout/use-nav-groups.ts
+++ b/src/components/layout/use-nav-groups.ts
@@ -6,12 +6,15 @@ import { getNavGroups, type NavGroup } from "@/config/menu";
* when the backend's can-approve answer (UpliftService tier-role rule, the
* same authority behind uplift approve/reject) is true for either tier.
* Loading and error states stay closed so the entry never flashes for a
- * user the backend would refuse.
+ * user the backend would refuse, including when a refetch fails and the
+ * query still holds an earlier `true`.
*/
export function useNavGroups(): NavGroup[] {
const tier1 = useUpliftsCanApprove(1);
const tier2 = useUpliftsCanApprove(2);
- const canReviewUplifts = tier1.data === true || tier2.data === true;
+ const allows = (query: { data?: boolean; isError?: boolean }) =>
+ query.data === true && !query.isError;
+ const canReviewUplifts = allows(tier1) || allows(tier2);
return getNavGroups({ canReviewUplifts });
}
diff --git a/src/test/components/layout/app-sidebar-can-approve.test.tsx b/src/test/components/layout/app-sidebar-can-approve.test.tsx
new file mode 100644
index 00000000..9db93295
--- /dev/null
+++ b/src/test/components/layout/app-sidebar-can-approve.test.tsx
@@ -0,0 +1,58 @@
+import { screen, waitFor } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { AppSidebar } from "@/components/layout/app-sidebar";
+import { queryKeys } from "@/infra/query-key/query-key";
+import { createTestQueryClient, renderWithProviders } from "@/test/test-utils";
+
+const canApproveApiMock = vi.hoisted(() => vi.fn());
+
+vi.mock("@/domain/uplifts/api/uplifts-api", () => ({
+ upliftsApi: { canApprove: canApproveApiMock },
+}));
+
+describe("AppSidebar Uplift Approvals gating through the can-approve query", () => {
+ beforeEach(() => {
+ canApproveApiMock.mockReset();
+ });
+
+ it("shows the entry when the API allows tier 2 and fails for tier 1", async () => {
+ canApproveApiMock.mockImplementation(async (tier: number) => {
+ if (tier === 1) throw new Error("503");
+ return true;
+ });
+
+ renderWithProviders(, { route: "/", withAuth: false });
+
+ expect(await screen.findByRole("link", { name: "Uplift Approvals" })).toHaveAttribute(
+ "href",
+ "/uplifts",
+ );
+ expect(canApproveApiMock).toHaveBeenCalledWith(1);
+ expect(canApproveApiMock).toHaveBeenCalledWith(2);
+ });
+
+ it("keeps the entry hidden once the API has denied both tiers", async () => {
+ canApproveApiMock.mockResolvedValue(false);
+ const queryClient = createTestQueryClient();
+
+ renderWithProviders(, {
+ route: "/",
+ withAuth: false,
+ queryClient,
+ });
+
+ await waitFor(() => {
+ for (const tier of [1, 2]) {
+ expect(queryClient.getQueryState(queryKeys.uplifts.canApprove(tier))?.status).toBe(
+ "success",
+ );
+ }
+ });
+ expect(screen.queryByRole("link", { name: "Uplift Approvals" })).toBeNull();
+ expect(screen.getByRole("link", { name: "Services" })).toHaveAttribute(
+ "href",
+ "/settings/services",
+ );
+ });
+});
diff --git a/src/test/components/layout/app-sidebar-nav.test.tsx b/src/test/components/layout/app-sidebar-nav.test.tsx
index 6a1dc738..07e58155 100644
--- a/src/test/components/layout/app-sidebar-nav.test.tsx
+++ b/src/test/components/layout/app-sidebar-nav.test.tsx
@@ -76,6 +76,15 @@ describe("AppSidebar navigation entries", () => {
expect(links["Uplift Approvals"]).toBeUndefined();
});
+ it("hides Uplift Approvals when a refetch fails after an earlier approval", () => {
+ canApproveMock.mockReturnValue({ data: true, isError: true, error: new Error("403") });
+
+ renderSidebar("/");
+ const links = sidebarLinks();
+
+ expect(links["Uplift Approvals"]).toBeUndefined();
+ });
+
it("offers Services to every user", () => {
canApproveMock.mockReturnValue({ data: false });