From c528cc533326d8f657e1a2c73c5b22e746417abc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:09:53 +1100 Subject: [PATCH] fix(frontend): refresh migrated-files list after upload (#64) The home route loader used ensureQueryData, which returns cached data, so router.invalidate() after an upload never refetched the list. The loader now passes revalidateIfStale so the list refetches on every loader run. Adds a regression test that fails without the fix. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019YPoXzBSj89ZHgw7nfTmbB --- .../src/__tests__/files-refresh.test.ts | 34 +++++++++++++++++++ csv-table-hub-main/src/lib/csv.functions.ts | 8 +++++ .../src/routes/_authenticated/index.tsx | 8 ++--- 3 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 csv-table-hub-main/src/__tests__/files-refresh.test.ts diff --git a/csv-table-hub-main/src/__tests__/files-refresh.test.ts b/csv-table-hub-main/src/__tests__/files-refresh.test.ts new file mode 100644 index 0000000..2d68f92 --- /dev/null +++ b/csv-table-hub-main/src/__tests__/files-refresh.test.ts @@ -0,0 +1,34 @@ +// src/__tests__/files-refresh.test.ts +// Regression test for #64: after an upload, router.invalidate() re-runs the +// home route loader. The loader must refetch the files list rather than hand +// back the cached one, or the new file only appears after navigating away. + +import { describe, it, expect, vi, afterEach } from "vitest"; +import { QueryClient } from "@tanstack/react-query"; +import { filesQuery, loadFiles } from "@/lib/csv.functions"; + +const file = (name: string) => ({ + id: name, + file_name: name, + table_name: `csv_${name}`, + row_count: 1, + column_names: ["a"], + created_at: "2026-10-26T00:00:00Z", +}); + +afterEach(() => vi.unstubAllGlobals()); + +describe("loadFiles", () => { + it("refetches the files list when the loader re-runs", async () => { + const responses = [[file("a")], [file("a"), file("b")]]; + const fetchMock = vi.fn(async () => new Response(JSON.stringify(responses.shift()))); + vi.stubGlobal("fetch", fetchMock); + const qc = new QueryClient(); + + await loadFiles(qc); // first page load + await loadFiles(qc); // router.invalidate() after an upload + + await vi.waitFor(() => expect(qc.getQueryData(filesQuery.queryKey)).toHaveLength(2)); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/csv-table-hub-main/src/lib/csv.functions.ts b/csv-table-hub-main/src/lib/csv.functions.ts index a638181..f44d79c 100644 --- a/csv-table-hub-main/src/lib/csv.functions.ts +++ b/csv-table-hub-main/src/lib/csv.functions.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import type { QueryClient } from "@tanstack/react-query"; const columnTypeSchema = z.enum(["int8", "numeric", "date", "timestamptz", "boolean", "text"]); export type ColumnType = z.infer; @@ -163,6 +164,13 @@ export async function listCsvFiles(): Promise { return apiFetch("/api/csv/files", csvFileSummaryListSchema); } +export const filesQuery = { queryKey: ["csv-files"], queryFn: listCsvFiles }; + +// Route loader for the files list. revalidateIfStale makes router.invalidate() +// (called after each upload) refetch instead of returning the cached list (#64). +export const loadFiles = (queryClient: QueryClient) => + queryClient.ensureQueryData({ ...filesQuery, revalidateIfStale: true }); + const csvPreviewResultSchema = z.discriminatedUnion("status", [ z.object({ status: z.literal("ok"), diff --git a/csv-table-hub-main/src/routes/_authenticated/index.tsx b/csv-table-hub-main/src/routes/_authenticated/index.tsx index 98727d1..5eb31c3 100644 --- a/csv-table-hub-main/src/routes/_authenticated/index.tsx +++ b/csv-table-hub-main/src/routes/_authenticated/index.tsx @@ -23,7 +23,7 @@ import { createFileRoute, Link, useRouter } from "@tanstack/react-router"; import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { - listCsvFiles, previewCsvTable, previewCsv, listTeTables, uploadCsv, + filesQuery, loadFiles, previewCsvTable, previewCsv, listTeTables, uploadCsv, type CsvFileSummary, type ColumnType, type TeTableInfo, } from "@/lib/csv.functions"; import { parseCsvPreview, COLUMN_TYPE_LABEL, sanitizeColumns, type CsvPreview } from "@/lib/csv-preview"; @@ -59,13 +59,9 @@ import { import { Progress } from "@/components/ui/progress"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -// ─── query ─────────────────────────────────────────────────────────────────── - -const filesQuery = { queryKey: ["csv-files"], queryFn: () => listCsvFiles() }; - export const Route = createFileRoute("/_authenticated/")({ head: () => ({ meta: [{ title: "CSV Migrator — Upload CSV files to your database" }, { name: "description", content: "Upload any number of CSV files. Each becomes its own database table, with duplicates automatically skipped." }, { property: "og:title", content: "CSV Migrator" }] }), - loader: ({ context }) => context.queryClient.ensureQueryData(filesQuery), + loader: ({ context }) => loadFiles(context.queryClient), component: Home, });