Shadcn conversion, more validate page

This commit is contained in:
2025-02-18 16:02:30 -05:00
parent f823841b15
commit 08be0658cb

View File

@@ -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<T extends string> = {
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 (
<Input
value={inputValue}
onChange={(e) => 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 (
<div
onClick={() => {
setIsEditing(true)
setInputValue(value || "")
}}
className="cursor-text py-2"
>
{value || ""}
</div>
)
}
export const ValidationStep = <T extends string>({ initialData, file, onBack }: Props<T>) => {
const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>()
const { toast } = useToast()
const [data, setData] = useState<(Data<T> & Meta)[]>(initialData)
const [selectedRows, setSelectedRows] = useState<ReadonlySet<number | string>>(new Set())
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
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 = <T extends string>({ 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<number, (typeof data)[number]>, 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<number, (typeof data)[number]>)
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<ColumnDef<Data<T> & Meta>[]>(() => {
const baseColumns: ColumnDef<Data<T> & Meta>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
...generateColumns(fields).map((col): ColumnDef<Data<T> & 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
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
return (
<EditableCell
value={value}
onChange={(newValue) => updateRows(rowIndex, column.id, newValue)}
error={error}
/>
)
},
})),
]
return baseColumns
}, [fields, updateRows])
const table = useReactTable({
data: filteredData,
columns,
state: {
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
})
}
return data
}, [data, filterByErrors])
const rowKeyGetter = useCallback((row: Data<T> & Meta) => row.__index, [])
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({})
}
}
const submitData = async () => {
const calculatedData = data.reduce(
@@ -179,7 +290,11 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
{translations.validationStep.title}
</h2>
<div className="flex flex-wrap items-center gap-4">
<Button variant="outline" size="sm" onClick={deleteSelectedRows}>
<Button
variant="outline"
size="sm"
onClick={deleteSelectedRows}
>
{translations.validationStep.discardButtonTitle}
</Button>
<div className="flex items-center gap-2">
@@ -194,28 +309,54 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
</div>
</div>
</div>
<div className="h-[calc(100vh-20rem)] w-full">
<DataGrid
rowKeyGetter={rowKeyGetter}
rows={tableData}
onRowsChange={updateRows}
columns={columns}
selectedRows={selectedRows}
onSelectedRowsChange={setSelectedRows}
className="h-full w-full"
noRowsFallback={
<div className="col-span-full mt-8 flex justify-center">
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="p-2">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
{filterByErrors
? translations.validationStep.noRowsMessageWhenFiltered
: translations.validationStep.noRowsMessage}
</div>
}
/>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>
<div className="px-8 py-8 border-t bg-muted -mt-4">
<div className="flex items-center justify-between -mt-4">
<div className="mt-auto border-t bg-muted px-8 py-4">
<div className="flex items-center justify-between">
{onBack && (
<Button variant="outline" onClick={onBack}>
{translations.validationStep.backButtonTitle}