From 91bfe0d043f876ac69f4a3b870924d75aedddd71 Mon Sep 17 00:00:00 2001 From: Louis King Date: Wed, 22 Jul 2026 23:40:56 +0100 Subject: [PATCH] fix: align Channels queryFn return shape with Dashboard Both Dashboard.tsx and Channels.tsx used the same React Query key qk.channels.list({}) but returned different data shapes: Dashboard returned the raw {items, total} object while Channels returned just the items array. When Dashboard loaded first, React Query cached the raw object, and Channels' for...of on the cached object threw 'g is not iterable'. Fix: Channels queryFn now returns the raw API response (matching Dashboard), and channels extraction uses data?.items ?? []. Added regression test that pre-seeds the cache with the Dashboard shape. --- .../web/static/js/spa-react/pages/Channels.test.tsx | 13 ++++++++++++- .../web/static/js/spa-react/pages/Channels.tsx | 12 +++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/meshcore_hub/web/static/js/spa-react/pages/Channels.test.tsx b/src/meshcore_hub/web/static/js/spa-react/pages/Channels.test.tsx index 12b6857..d392756 100644 --- a/src/meshcore_hub/web/static/js/spa-react/pages/Channels.test.tsx +++ b/src/meshcore_hub/web/static/js/spa-react/pages/Channels.test.tsx @@ -2,7 +2,7 @@ import { screen, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { Channels } from "@/pages/Channels"; -import { renderWithProviders } from "@/test/renderWithProviders"; +import { renderWithProviders, createTestQueryClient } from "@/test/renderWithProviders"; import { makeConfig } from "@/test/makeConfig"; import * as api from "@/utils/api"; @@ -69,4 +69,15 @@ describe("Channels", () => { expect(screen.queryByText("Public")).not.toBeInTheDocument(); }); }); + + it("renders correctly when cache was pre-populated by Dashboard (raw object shape)", async () => { + mockChannelsApi(); + const client = createTestQueryClient(); + client.setQueryData(["channels", "list", {}], CHANNELS); + renderWithProviders(, { client }); + await waitFor(() => { + expect(screen.getByText("Public")).toBeInTheDocument(); + expect(screen.getByText("Ops")).toBeInTheDocument(); + }); + }); }); diff --git a/src/meshcore_hub/web/static/js/spa-react/pages/Channels.tsx b/src/meshcore_hub/web/static/js/spa-react/pages/Channels.tsx index 18dff39..679e901 100644 --- a/src/meshcore_hub/web/static/js/spa-react/pages/Channels.tsx +++ b/src/meshcore_hub/web/static/js/spa-react/pages/Channels.tsx @@ -293,16 +293,10 @@ export function Channels() { error: queryError, } = useQuery({ queryKey: qk.channels.list({}), - queryFn: async ({ signal }) => { - const resp = await apiGet( - "/api/v1/channels", - {}, - { signal }, - ); - return resp.items || []; - }, + queryFn: ({ signal }) => + apiGet("/api/v1/channels", {}, { signal }), }); - const channels = data ?? []; + const channels = data?.items ?? []; const error = queryError ? queryError.message : null; const [modal, setModal] = useState(null);