mirror of
https://github.com/wassname/Open-Assistant.git
synced 2026-07-01 16:50:12 +08:00
b7d0c4331c
part of #879
206 lines
5.6 KiB
TypeScript
206 lines
5.6 KiB
TypeScript
import {
|
|
Box,
|
|
Button,
|
|
Flex,
|
|
FormControl,
|
|
FormLabel,
|
|
Input,
|
|
Popover,
|
|
PopoverArrow,
|
|
PopoverBody,
|
|
PopoverCloseButton,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
Spacer,
|
|
Table,
|
|
TableCaption,
|
|
TableContainer,
|
|
TableRowProps,
|
|
Tbody,
|
|
Td,
|
|
Th,
|
|
Thead,
|
|
Tr,
|
|
useDisclosure,
|
|
} from "@chakra-ui/react";
|
|
import { Cell, ColumnDef, flexRender, getCoreRowModel, Row, useReactTable } from "@tanstack/react-table";
|
|
import { Filter } from "lucide-react";
|
|
import { useTranslation } from "next-i18next";
|
|
import { ChangeEvent, ReactNode } from "react";
|
|
import { useDebouncedCallback } from "use-debounce";
|
|
|
|
export type DataTableColumnDef<T> = ColumnDef<T> & {
|
|
filterable?: boolean;
|
|
span?: number | ((cell: Cell<T, unknown>) => number | undefined);
|
|
};
|
|
|
|
// TODO: stricter type
|
|
export type FilterItem = {
|
|
id: string;
|
|
value: string;
|
|
};
|
|
|
|
export type DataTableRowPropsCallback<T> = (row: Row<T>) => TableRowProps;
|
|
|
|
export type DataTableProps<T> = {
|
|
data: T[];
|
|
columns: DataTableColumnDef<T>[];
|
|
caption?: string;
|
|
filterValues?: FilterItem[];
|
|
onNextClick?: () => void;
|
|
onPreviousClick?: () => void;
|
|
onFilterChange?: (items: FilterItem[]) => void;
|
|
disableNext?: boolean;
|
|
disablePrevious?: boolean;
|
|
disablePagination?: boolean;
|
|
rowProps?: TableRowProps | DataTableRowPropsCallback<T>;
|
|
};
|
|
|
|
export const DataTable = <T,>({
|
|
data,
|
|
columns,
|
|
caption,
|
|
filterValues = [],
|
|
onNextClick,
|
|
onPreviousClick,
|
|
onFilterChange,
|
|
disableNext,
|
|
disablePrevious,
|
|
disablePagination,
|
|
rowProps,
|
|
}: DataTableProps<T>) => {
|
|
const { t } = useTranslation("leaderboard");
|
|
const { getHeaderGroups, getRowModel } = useReactTable<T>({
|
|
data,
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
});
|
|
|
|
const handleFilterChange = (value: FilterItem) => {
|
|
const idx = filterValues.findIndex((oldValue) => oldValue.id === value.id);
|
|
let newValues: FilterItem[] = [];
|
|
if (idx === -1) {
|
|
newValues = [...filterValues, value];
|
|
} else {
|
|
newValues = filterValues.map((oldValue) => (oldValue.id === value.id ? value : oldValue));
|
|
}
|
|
onFilterChange && onFilterChange(newValues);
|
|
};
|
|
return (
|
|
<>
|
|
{!disablePagination && (
|
|
<Flex mb="2">
|
|
<Button onClick={onPreviousClick} disabled={disablePrevious}>
|
|
{t("previous")}
|
|
</Button>
|
|
<Spacer />
|
|
<Button onClick={onNextClick} disabled={disableNext}>
|
|
{t("next")}
|
|
</Button>
|
|
</Flex>
|
|
)}
|
|
<TableContainer>
|
|
<Table variant="simple">
|
|
<TableCaption pb={0}>{caption}</TableCaption>
|
|
<Thead>
|
|
{getHeaderGroups().map((headerGroup) => (
|
|
<Tr key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => (
|
|
<Th key={header.id}>
|
|
<Box display="flex" alignItems="center">
|
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
|
{(header.column.columnDef as DataTableColumnDef<T>).filterable && (
|
|
<FilterModal
|
|
value={filterValues.find((value) => value.id === header.id)?.value ?? ""}
|
|
onChange={(value) => handleFilterChange({ id: header.id, value })}
|
|
label={flexRender(header.column.columnDef.header, header.getContext())}
|
|
></FilterModal>
|
|
)}
|
|
</Box>
|
|
</Th>
|
|
))}
|
|
</Tr>
|
|
))}
|
|
</Thead>
|
|
<Tbody>
|
|
{getRowModel().rows.map((row) => {
|
|
const props = typeof rowProps === "function" ? rowProps(row) : rowProps;
|
|
return (
|
|
<Tr key={row.id} {...props}>
|
|
<DataTableRow row={row}></DataTableRow>
|
|
</Tr>
|
|
);
|
|
})}
|
|
</Tbody>
|
|
</Table>
|
|
</TableContainer>
|
|
</>
|
|
);
|
|
};
|
|
|
|
type WithSpanCell<T> = Cell<T, unknown> & { span?: number };
|
|
|
|
const DataTableRow = <T,>({ row }: { row: Row<T> }) => {
|
|
const cells: WithSpanCell<T>[] = row.getVisibleCells();
|
|
const renderCells: WithSpanCell<T>[] = [];
|
|
|
|
for (let i = 0; i < cells.length; i++) {
|
|
const cell = cells[i];
|
|
const span = (cell.column.columnDef as DataTableColumnDef<T>).span;
|
|
const spanValue = typeof span === "function" ? span(cell) : span;
|
|
if (spanValue && spanValue > 1) {
|
|
i += spanValue - 1; // skip next `spanValue - 1` cell
|
|
}
|
|
cell.span = spanValue;
|
|
renderCells.push(cell);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{renderCells.map((cell) => {
|
|
return (
|
|
<Td key={cell.id} colSpan={cell.span}>
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</Td>
|
|
);
|
|
})}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const FilterModal = ({
|
|
label,
|
|
onChange,
|
|
value,
|
|
}: {
|
|
label: ReactNode;
|
|
onChange: (val: string) => void;
|
|
value: string;
|
|
}) => {
|
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
|
|
const handleInputChange = useDebouncedCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
onChange(e.target.value);
|
|
}, 500);
|
|
|
|
return (
|
|
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
|
<PopoverTrigger>
|
|
<Button variant={"unstyled"} ml="2">
|
|
<Filter size="1em"></Filter>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent w="fit-content">
|
|
<PopoverArrow />
|
|
<PopoverCloseButton />
|
|
<PopoverBody mt="4">
|
|
<FormControl>
|
|
<FormLabel>{label}</FormLabel>
|
|
<Input onChange={handleInputChange} defaultValue={value}></Input>
|
|
</FormControl>
|
|
</PopoverBody>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
};
|