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 type { Meta } from "./types"
|
||||||
import { addErrorsAndRunHooks } from "./utils/dataMutations"
|
import { addErrorsAndRunHooks } from "./utils/dataMutations"
|
||||||
import { generateColumns } from "./components/columns"
|
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 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 {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -30,16 +44,71 @@ type Props<T extends string> = {
|
|||||||
onBack?: () => void
|
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>) => {
|
export const ValidationStep = <T extends string>({ initialData, file, onBack }: Props<T>) => {
|
||||||
const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>()
|
const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
||||||
const [data, setData] = useState<(Data<T> & Meta)[]>(initialData)
|
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 [filterByErrors, setFilterByErrors] = useState(false)
|
||||||
const [showSubmitAlert, setShowSubmitAlert] = useState(false)
|
const [showSubmitAlert, setShowSubmitAlert] = useState(false)
|
||||||
const [isSubmitting, setSubmitting] = 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(
|
const updateData = useCallback(
|
||||||
async (rows: typeof data, indexes?: number[]) => {
|
async (rows: typeof data, indexes?: number[]) => {
|
||||||
// Check if hooks are async - if they are we want to apply changes optimistically for better UX
|
// 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],
|
[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(
|
const updateRows = useCallback(
|
||||||
(rows: typeof data, changedData?: RowsChangeData<(typeof data)[number]>) => {
|
(rowIndex: number, columnId: string, value: string) => {
|
||||||
const changes = changedData?.indexes.reduce((acc: Record<number, (typeof data)[number]>, index: number) => {
|
const newData = [...data]
|
||||||
// when data is filtered val !== actual index in data
|
// Get the actual row from the filtered or unfiltered data
|
||||||
const realIndex = data.findIndex((value) => value.__index === rows[index].__index)
|
const row = filteredData[rowIndex]
|
||||||
acc[realIndex] = rows[index]
|
if (row) {
|
||||||
return acc
|
// Find the original index in the full dataset
|
||||||
}, {} as Record<number, (typeof data)[number]>)
|
const originalIndex = data.findIndex(r => r.__index === row.__index)
|
||||||
const realIndexes = changes && Object.keys(changes).map((index) => Number(index))
|
const updatedRow = {
|
||||||
const newData = Object.assign([], data, changes)
|
...row,
|
||||||
updateData(newData, realIndexes)
|
[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(() => {
|
return (
|
||||||
if (filterByErrors) {
|
<EditableCell
|
||||||
return data.filter((value) => {
|
value={value}
|
||||||
if (value?.__errors) {
|
onChange={(newValue) => updateRows(rowIndex, column.id, newValue)}
|
||||||
return Object.values(value.__errors)?.filter((err) => err.level === "error").length
|
error={error}
|
||||||
}
|
/>
|
||||||
return false
|
)
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
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 submitData = async () => {
|
||||||
const calculatedData = data.reduce(
|
const calculatedData = data.reduce(
|
||||||
@@ -179,7 +290,11 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
|||||||
{translations.validationStep.title}
|
{translations.validationStep.title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
<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}
|
{translations.validationStep.discardButtonTitle}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -194,28 +309,54 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[calc(100vh-20rem)] w-full">
|
<div className="rounded-md border">
|
||||||
<DataGrid
|
<Table>
|
||||||
rowKeyGetter={rowKeyGetter}
|
<TableHeader>
|
||||||
rows={tableData}
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
onRowsChange={updateRows}
|
<TableRow key={headerGroup.id}>
|
||||||
columns={columns}
|
{headerGroup.headers.map((header) => (
|
||||||
selectedRows={selectedRows}
|
<TableHead key={header.id}>
|
||||||
onSelectedRowsChange={setSelectedRows}
|
{header.isPlaceholder
|
||||||
className="h-full w-full"
|
? null
|
||||||
noRowsFallback={
|
: flexRender(
|
||||||
<div className="col-span-full mt-8 flex justify-center">
|
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
|
{filterByErrors
|
||||||
? translations.validationStep.noRowsMessageWhenFiltered
|
? translations.validationStep.noRowsMessageWhenFiltered
|
||||||
: translations.validationStep.noRowsMessage}
|
: translations.validationStep.noRowsMessage}
|
||||||
</div>
|
</TableCell>
|
||||||
}
|
</TableRow>
|
||||||
/>
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-8 py-8 border-t bg-muted -mt-4">
|
<div className="mt-auto border-t bg-muted px-8 py-4">
|
||||||
<div className="flex items-center justify-between -mt-4">
|
<div className="flex items-center justify-between">
|
||||||
{onBack && (
|
{onBack && (
|
||||||
<Button variant="outline" onClick={onBack}>
|
<Button variant="outline" onClick={onBack}>
|
||||||
{translations.validationStep.backButtonTitle}
|
{translations.validationStep.backButtonTitle}
|
||||||
|
|||||||
Reference in New Issue
Block a user