From edc36fb429685fb4912a8fdf090182662b61e1cc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 12:10:53 +1100 Subject: [PATCH] feat(frontend): show stored failure reasons in the Migrated files Preview (M3c, #107) When a migrated file has failed rows, its Preview now loads the reasons from GET /api/csv/uploads/{id} and lists them, with an error report download. They come from the server, so any user in the tenant sees them, not only the browser that uploaded the file. vitest 143 passed (2 new); tsc 0; production build OK. End to end in Chromium with a fresh browser: the stored reason is shown, 0 page errors. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019YPoXzBSj89ZHgw7nfTmbB --- .../src/__tests__/stored-row-errors.test.tsx | 46 +++++++++++++++++++ csv-table-hub-main/src/lib/csv.functions.ts | 18 ++++++++ .../src/routes/_authenticated/index.tsx | 24 +++++++++- 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 csv-table-hub-main/src/__tests__/stored-row-errors.test.tsx diff --git a/csv-table-hub-main/src/__tests__/stored-row-errors.test.tsx b/csv-table-hub-main/src/__tests__/stored-row-errors.test.tsx new file mode 100644 index 0000000..2b526d6 --- /dev/null +++ b/csv-table-hub-main/src/__tests__/stored-row-errors.test.tsx @@ -0,0 +1,46 @@ +// M3c (#107): the Migrated files Preview shows why rows failed, read from the server. + +import { afterEach, expect, it, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { StoredRowErrors } from "@/routes/_authenticated/index"; + +const detail = { + id: "7", + status: "partly_imported", + fileName: "people.csv", + uploader: "alice", + failedRows: 1, + rowErrors: [{ rowNumber: 2, column: "n", value: "x", reason: '"x" is not a whole number' }], + logs: [], +}; + +function show(body: unknown) { + const fetchMock = vi + .fn() + .mockResolvedValue( + new Response(JSON.stringify(body), { headers: { "content-type": "application/json" } }), + ); + vi.stubGlobal("fetch", fetchMock); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + render( + + + , + ); + return fetchMock; +} + +afterEach(() => vi.unstubAllGlobals()); + +it("lists the stored reasons from the upload's server record", async () => { + const fetchMock = show(detail); + expect(await screen.findByText('"x" is not a whole number')).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /error report/i })).toBeInTheDocument(); + expect(String(fetchMock.mock.calls[0][0])).toMatch(/\/api\/csv\/uploads\/7$/); +}); + +it("says so when no reasons were stored", async () => { + show({ ...detail, rowErrors: [] }); + expect(await screen.findByText(/no failure reasons were stored/i)).toBeInTheDocument(); +}); diff --git a/csv-table-hub-main/src/lib/csv.functions.ts b/csv-table-hub-main/src/lib/csv.functions.ts index d073be8..cd6c603 100644 --- a/csv-table-hub-main/src/lib/csv.functions.ts +++ b/csv-table-hub-main/src/lib/csv.functions.ts @@ -89,6 +89,8 @@ const csvFileSummarySchema = z.object({ row_count: z.number(), total_rows: z.number().nullable().optional(), failed_rows: z.number().nullable().optional(), + uploader: z.string().nullable().optional(), + upload_id: z.string().nullable().optional(), column_names: z.array(z.string()), created_at: z.string(), }); @@ -231,3 +233,19 @@ export async function previewCsvTable( previewRowsSchema, ); } + +// M3c (#107): an upload's failure reasons and step log, stored on the server. +const uploadDetailSchema = z.object({ + id: z.string(), + status: z.string(), + fileName: z.string(), + uploader: z.string().nullable(), + failedRows: z.number().nullable(), + rowErrors: z.array(rowErrorSchema).nullable(), + logs: z.array(processingLogSchema).nullable(), +}); +export type UploadDetail = z.infer; + +export async function getUploadDetail(uploadId: string): Promise { + return apiFetch(`/api/csv/uploads/${encodeURIComponent(uploadId)}`, uploadDetailSchema); +} diff --git a/csv-table-hub-main/src/routes/_authenticated/index.tsx b/csv-table-hub-main/src/routes/_authenticated/index.tsx index cfc995a..ae1a1af 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 { - filesQuery, loadFiles, previewCsvTable, previewCsv, listTeTables, uploadCsv, + filesQuery, loadFiles, previewCsvTable, getUploadDetail, 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"; @@ -743,6 +743,28 @@ function PreviewTable({file}:{file:CsvFileSummary}){

Showing up to 50 rows. Table has {file.row_count} row{file.row_count===1?"":"s"} total.{failedRowsNote(file)&& {failedRowsNote(file)} validation and weren't imported.}

+ {file.upload_id&&(file.failed_rows??0)>0&&} + + ); +} + +// M3c (#107): why rows failed, read from the server, so every user in the tenant sees the same reasons. +export function StoredRowErrors({uploadId,fileName}:{uploadId:string;fileName:string}){ + const {data,isLoading,error}=useQuery({queryKey:["upload-detail",uploadId],queryFn:()=>getUploadDetail(uploadId)}); + if(isLoading)return

Loading failure reasons…

; + if(error)return

{(error as Error).message}

; + const errors=data?.rowErrors??[]; + if(errors.length===0)return

No failure reasons were stored for this upload.

; + return ( +
+
+

Rows that failed{(data?.failedRows??0)>errors.length&&` (first ${errors.length} of ${data?.failedRows})`}

+ +
+ + RowColumnValueReason + {errors.slice(0,50).map((re,i)=>{re.rowNumber}{re.column??"—"}{re.value??"—"}{re.reason})} +
); }