Shadcn conversion, more validate page
This commit is contained in:
@@ -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(),
|
||||
})
|
||||
|
||||
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<T> & Meta) => row.__index, [])
|
||||
}
|
||||
|
||||
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">
|
||||
{filterByErrors
|
||||
? translations.validationStep.noRowsMessageWhenFiltered
|
||||
: translations.validationStep.noRowsMessage}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<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}
|
||||
</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}
|
||||
|
||||
Reference in New Issue
Block a user