diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx index 847dbd2..d20153f 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx @@ -3,11 +3,25 @@ import { useRsi } from "../../hooks/useRsi" import type { Meta } from "./types" import { addErrorsAndRunHooks } from "./utils/dataMutations" import { generateColumns } from "./components/columns" -// @ts-ignore -import type { Column, RowsChangeData } from "react-data-grid" -// @ts-ignore -import DataGrid from "react-data-grid" import type { Data } from "../../types" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + useReactTable, + getCoreRowModel, + type ColumnDef, + flexRender, + type Row, + type RowSelectionState, +} from "@tanstack/react-table" +import { Checkbox } from "@/components/ui/checkbox" +import { Input } from "@/components/ui/input" import { AlertDialog, AlertDialogAction, @@ -30,16 +44,71 @@ type Props = { onBack?: () => void } +const EditableCell = ({ value, onChange, error }: { value: string, onChange: (value: string) => void, error?: { level: string } }) => { + const [isEditing, setIsEditing] = useState(false) + const [inputValue, setInputValue] = useState(value || "") + + // Always show input for error cells + if (error?.level === "error" || isEditing) { + return ( + setInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + onChange(inputValue) + if (!error?.level) { + setIsEditing(false) + } + } + }} + onBlur={() => { + onChange(inputValue) + if (!error?.level) { + setIsEditing(false) + } + }} + className={`w-full bg-transparent ${ + error?.level === "error" + ? "border-destructive text-destructive" + : "" + }`} + autoFocus={!error?.level} + /> + ) + } + + return ( +
{ + setIsEditing(true) + setInputValue(value || "") + }} + className="cursor-text py-2" + > + {value || ""} +
+ ) +} + export const ValidationStep = ({ initialData, file, onBack }: Props) => { const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi() const { toast } = useToast() const [data, setData] = useState<(Data & Meta)[]>(initialData) - const [selectedRows, setSelectedRows] = useState>(new Set()) + const [rowSelection, setRowSelection] = useState({}) const [filterByErrors, setFilterByErrors] = useState(false) const [showSubmitAlert, setShowSubmitAlert] = useState(false) const [isSubmitting, setSubmitting] = useState(false) + // Memoize filtered data to prevent recalculation on every render + const filteredData = useMemo(() => { + if (!filterByErrors) return data + return data.filter(row => + row.__errors && Object.values(row.__errors).some(err => err.level === "error") + ) + }, [data, filterByErrors]) + const updateData = useCallback( async (rows: typeof data, indexes?: number[]) => { // Check if hooks are async - if they are we want to apply changes optimistically for better UX @@ -51,44 +120,86 @@ export const ValidationStep = ({ initialData, file, onBack }: [rowHook, tableHook, fields], ) - const deleteSelectedRows = () => { - if (selectedRows.size) { - const newData = data.filter((value) => !selectedRows.has(value.__index)) - updateData(newData) - setSelectedRows(new Set()) - } - } - const updateRows = useCallback( - (rows: typeof data, changedData?: RowsChangeData<(typeof data)[number]>) => { - const changes = changedData?.indexes.reduce((acc: Record, index: number) => { - // when data is filtered val !== actual index in data - const realIndex = data.findIndex((value) => value.__index === rows[index].__index) - acc[realIndex] = rows[index] - return acc - }, {} as Record) - const realIndexes = changes && Object.keys(changes).map((index) => Number(index)) - const newData = Object.assign([], data, changes) - updateData(newData, realIndexes) + (rowIndex: number, columnId: string, value: string) => { + const newData = [...data] + // Get the actual row from the filtered or unfiltered data + const row = filteredData[rowIndex] + if (row) { + // Find the original index in the full dataset + const originalIndex = data.findIndex(r => r.__index === row.__index) + const updatedRow = { + ...row, + [columnId]: value, + } + newData[originalIndex] = updatedRow + updateData(newData, [originalIndex]) + } }, - [data, updateData], + [data, filteredData, updateData], ) - const columns = useMemo(() => generateColumns(fields), [fields]) + const columns = useMemo & Meta>[]>(() => { + const baseColumns: ColumnDef & Meta>[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + ...generateColumns(fields).map((col): ColumnDef & Meta> => ({ + accessorKey: col.key, + header: col.name, + cell: ({ row, column }) => { + const value = row.getValue(column.id) as string + const error = row.original.__errors?.[column.id] + const rowIndex = row.index + + return ( + updateRows(rowIndex, column.id, newValue)} + error={error} + /> + ) + }, + })), + ] + return baseColumns + }, [fields, updateRows]) - const tableData = useMemo(() => { - if (filterByErrors) { - return data.filter((value) => { - if (value?.__errors) { - return Object.values(value.__errors)?.filter((err) => err.level === "error").length - } - return false - }) + const table = useReactTable({ + data: filteredData, + columns, + state: { + rowSelection, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + }) + + const deleteSelectedRows = () => { + if (Object.keys(rowSelection).length) { + const selectedRows = Object.keys(rowSelection).map(Number) + const newData = data.filter((_, index) => !selectedRows.includes(index)) + updateData(newData) + setRowSelection({}) } - return data - }, [data, filterByErrors]) - - const rowKeyGetter = useCallback((row: Data & Meta) => row.__index, []) + } const submitData = async () => { const calculatedData = data.reduce( @@ -179,7 +290,11 @@ export const ValidationStep = ({ initialData, file, onBack }: {translations.validationStep.title}
-
@@ -194,28 +309,54 @@ export const ValidationStep = ({ initialData, file, onBack }:
-
- - {filterByErrors - ? translations.validationStep.noRowsMessageWhenFiltered - : translations.validationStep.noRowsMessage} -
- } - /> +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + {filterByErrors + ? translations.validationStep.noRowsMessageWhenFiltered + : translations.validationStep.noRowsMessage} + + + )} + +
-
-
+
+
{onBack && (