import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { addErrorsToData } from "@/lib/map-utils"; import { cn } from "@/lib/utils"; import { ColumnDef, ColumnFiltersState, flexRender, getCoreRowModel, getSortedRowModel, Row, RowData, Table, useReactTable, } from "@tanstack/react-table"; import { useVirtualizer, VirtualItem, Virtualizer, } from "@tanstack/react-virtual"; import { Trash } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { DataTableColumnHeader } from "./data-table-header"; import { FieldConfig, FieldMappingItem, FieldStatus, Meta } from "./types"; function EditableCell({ initialValue, updateData, row, columnName, }: { initialValue: unknown; updateData?: (rowIndex: string, columnName: string, value: unknown) => void; row: Row & Meta>; columnName: string; }) { const [value, setValue] = useState(initialValue); // When the input is blurred, update the table data. const onBlur = () => { if (value !== initialValue) { updateData?.(row.original.__index, columnName, value); } }; // Sync state when initialValue changes. useEffect(() => { setValue(initialValue); }, [initialValue]); return (
setValue(e.target.value)} onBlur={onBlur} />
); } declare module "@tanstack/react-table" { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface TableMeta { updateData: (rowIndex: string, columnName: string, value: unknown) => void; } } interface ReviewStepProps { fields: FieldConfig[]; data: (Record & Meta)[]; fieldMappings: FieldMappingItem[]; } export function ReviewStep(props: ReviewStepProps) { const { data, fieldMappings, fields } = props; const [rowData, setRowData] = useState<(Record & Meta)[]>(data); const [filterErrors, setFilterErrors] = useState(false); const [rowSelection, setRowSelection] = useState>({}); const [columnFilters, setColumnFilters] = useState([]); const tableContainerRef = useRef(null); const columns: ColumnDef & Meta>[] = useMemo< ColumnDef & Meta>[] >(() => { const mappingColumns = fieldMappings.reduce< ColumnDef & Meta>[] >((acc, mapping) => { if ( mapping.status === FieldStatus.Mapped || mapping.status === FieldStatus.Custom ) { acc.push({ id: mapping.id, header: ({ column }) => (
), enableSorting: true, accessorKey: mapping.mappedValue, size: 180, minSize: 100, cell: ({ getValue, row, table }) => ( ), }); } return acc; }, []); return [ { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value) } aria-label="Select all" /> ), cell: ({ row }) => (
row.toggleSelected(!!value)} aria-label="Select row" />
), size: 48, minSize: 48, enableResizing: false, }, ...mappingColumns, ]; }, [fieldMappings]); const filteredData = useMemo(() => { if (!filterErrors) return rowData; return rowData.filter( (row) => row.__errors && Object.values(row.__errors).some( (err: { level: string }) => err.level === "error" ) ); }, [rowData, filterErrors]); const table = useReactTable({ data: filteredData, columns, getRowId: (row) => row.__index, defaultColumn: { minSize: 50, maxSize: 600, }, columnResizeMode: "onChange", state: { rowSelection, columnFilters, }, onColumnFiltersChange: setColumnFilters, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), meta: { updateData: async (rowIndex, columnName, value) => { const newRowData = rowData.map((row) => { if (row.__index === rowIndex) { return { ...row, [columnName]: value, }; } return row; }); console.log("New Row Data with new errors", newRowData); const newDataWithErrors = await addErrorsToData( newRowData, fields, fieldMappings ); setRowData(newDataWithErrors); }, }, }); const selectedRowIds = useMemo( () => Object.keys(rowSelection).filter((id) => rowSelection[id]), [rowSelection] ); const deleteSelectedRows = async () => { const newData = rowData.filter( (row) => !selectedRowIds.includes(row.__index) ); const newDataWithErrors = await addErrorsToData( newData, fields, fieldMappings ); setRowData(newDataWithErrors); setRowSelection({}); }; const columnSizeVars = useMemo(() => { const headers = table.getFlatHeaders(); const colSizes: { [key: string]: number } = {}; for (let i = 0; i < headers.length; i++) { const header = headers[i]!; colSizes[`--header-${header.id}-size`] = header.getSize(); colSizes[`--col-${header.column.id}-size`] = header.column.getSize(); } return colSizes; // eslint-disable-next-line react-hooks/exhaustive-deps }, [table.getState().columnSizingInfo, table.getState().columnSizing]); return (
{/* Top controls: Filter and Delete */}

Review Your Mapped Data

Please verify the data below. You can update any field, select rows to discard, or filter to display only rows with errors before finalizing the import.

{/* Table container */}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))}
{header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )}
header.column.resetSize(), onMouseDown: header.getResizeHandler(), onTouchStart: header.getResizeHandler(), }} />
{/* Continue button */}
Total Rows: {rowData.length} {selectedRowIds.length > 0 && ( Selected Rows: {selectedRowIds.length} )}
); } interface TableBodyProps { table: Table & Meta>; tableContainerRef: React.RefObject; columnsLength: number; } function TableBody({ table, tableContainerRef, columnsLength, }: TableBodyProps) { const { rows } = table.getRowModel(); const rowVirtualizer = useVirtualizer({ count: rows.length, estimateSize: () => 34, getScrollElement: () => tableContainerRef.current, //measure dynamic row height, except in firefox because it measures table border height incorrectly measureElement: typeof window !== "undefined" && navigator.userAgent.indexOf("Firefox") === -1 ? (element) => element?.getBoundingClientRect().height : undefined, }); const virtualizedRowItems = rowVirtualizer.getVirtualItems(); return ( {virtualizedRowItems.length ? ( rowVirtualizer.getVirtualItems().map((virtualRow) => { const row = rows[virtualRow.index]; return ( ); }) ) : ( No results. )} ); } interface TableBodyRowProps { row: Row & Meta>; virtualRow: VirtualItem; rowVirtualizer: Virtualizer; } function TableBodyRow({ row, virtualRow, rowVirtualizer }: TableBodyRowProps) { return ( rowVirtualizer.measureElement(node)} key={row.id} className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted flex absolute w-full" data-state={row.getIsSelected() && "selected"} style={{ // display: "flex", // position: "absolute", transform: `translateY(${virtualRow.start}px)`, //this should always be a `style` as it changes on scroll // width: "100%", }} > {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ); }