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})} +
); }