Something went wrong. Try again.
A lexicon-driven AppView for ATProto.
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317import { type ColumnFiltersState, getCoreRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, type PaginationState, type RowSelectionState, type SortingState, type TableOptions, type TableState, type Updater, useReactTable, type VisibilityState,} from "@tanstack/react-table";import { parseAsArrayOf, parseAsInteger, parseAsString, type SingleParser, type UseQueryStateOptions, useQueryState, useQueryStates,} from "nuqs";import * as React from "react";
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";import { getSortingStateParser } from "@/lib/parsers";import type { ExtendedColumnSort, QueryKeys } from "@/types/data-table";
const PAGE_KEY = "page";const PER_PAGE_KEY = "perPage";const SORT_KEY = "sort";const FILTERS_KEY = "filters";const JOIN_OPERATOR_KEY = "joinOperator";const ARRAY_SEPARATOR = ",";const DEBOUNCE_MS = 300;const THROTTLE_MS = 50;
interface UseDataTableProps<TData> extends Omit< TableOptions<TData>, | "state" | "pageCount" | "getCoreRowModel" | "manualFiltering" | "manualPagination" | "manualSorting" >, Required<Pick<TableOptions<TData>, "pageCount">> { initialState?: Omit<Partial<TableState>, "sorting"> & { sorting?: ExtendedColumnSort<TData>[]; }; queryKeys?: Partial<QueryKeys>; history?: "push" | "replace"; debounceMs?: number; throttleMs?: number; clearOnDefault?: boolean; enableAdvancedFilter?: boolean; scroll?: boolean; shallow?: boolean; startTransition?: React.TransitionStartFunction;}
export function useDataTable<TData>(props: UseDataTableProps<TData>) { const { columns, pageCount = -1, initialState, queryKeys, history = "replace", debounceMs = DEBOUNCE_MS, throttleMs = THROTTLE_MS, clearOnDefault = false, enableAdvancedFilter = false, scroll = false, shallow = true, startTransition, ...tableProps } = props; const pageKey = queryKeys?.page ?? PAGE_KEY; const perPageKey = queryKeys?.perPage ?? PER_PAGE_KEY; const sortKey = queryKeys?.sort ?? SORT_KEY; const filtersKey = queryKeys?.filters ?? FILTERS_KEY; const joinOperatorKey = queryKeys?.joinOperator ?? JOIN_OPERATOR_KEY;
const queryStateOptions = React.useMemo< Omit<UseQueryStateOptions<string>, "parse"> >( () => ({ history, scroll, shallow, throttleMs, debounceMs, clearOnDefault, startTransition, }), [ history, scroll, shallow, throttleMs, debounceMs, clearOnDefault, startTransition, ], );
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>( initialState?.rowSelection ?? {}, ); const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(initialState?.columnVisibility ?? {});
const [page, setPage] = useQueryState( pageKey, parseAsInteger.withOptions(queryStateOptions).withDefault(1), ); const [perPage, setPerPage] = useQueryState( perPageKey, parseAsInteger .withOptions(queryStateOptions) .withDefault(initialState?.pagination?.pageSize ?? 10), );
const pagination: PaginationState = React.useMemo(() => { return { pageIndex: page - 1, // zero-based index -> one-based index pageSize: perPage, }; }, [page, perPage]);
const onPaginationChange = React.useCallback( (updaterOrValue: Updater<PaginationState>) => { if (typeof updaterOrValue === "function") { const newPagination = updaterOrValue(pagination); void setPage(newPagination.pageIndex + 1); void setPerPage(newPagination.pageSize); } else { void setPage(updaterOrValue.pageIndex + 1); void setPerPage(updaterOrValue.pageSize); } }, [pagination, setPage, setPerPage], );
const columnIds = React.useMemo(() => { return new Set( columns.map((column) => column.id).filter(Boolean) as string[], ); }, [columns]);
const [sorting, setSorting] = useQueryState( sortKey, getSortingStateParser<TData>(columnIds) .withOptions(queryStateOptions) .withDefault(initialState?.sorting ?? []), );
const onSortingChange = React.useCallback( (updaterOrValue: Updater<SortingState>) => { if (typeof updaterOrValue === "function") { const newSorting = updaterOrValue(sorting); setSorting(newSorting as ExtendedColumnSort<TData>[]); } else { setSorting(updaterOrValue as ExtendedColumnSort<TData>[]); } }, [sorting, setSorting], );
const filterableColumns = React.useMemo(() => { if (enableAdvancedFilter) return [];
return columns.filter((column) => column.enableColumnFilter); }, [columns, enableAdvancedFilter]);
const filterParsers = React.useMemo(() => { if (enableAdvancedFilter) return {};
return filterableColumns.reduce< Record<string, SingleParser<string> | SingleParser<string[]>> >((acc, column) => { if (column.meta?.options) { acc[column.id ?? ""] = parseAsArrayOf( parseAsString, ARRAY_SEPARATOR, ).withOptions(queryStateOptions); } else { acc[column.id ?? ""] = parseAsString.withOptions(queryStateOptions); } return acc; }, {}); }, [filterableColumns, queryStateOptions, enableAdvancedFilter]);
const [filterValues, setFilterValues] = useQueryStates(filterParsers);
const debouncedSetFilterValues = useDebouncedCallback( (values: typeof filterValues) => { void setPage(1); void setFilterValues(values); }, debounceMs, );
const initialColumnFilters: ColumnFiltersState = React.useMemo(() => { if (enableAdvancedFilter) return [];
return Object.entries(filterValues).reduce<ColumnFiltersState>( (filters, [key, value]) => { if (value !== null) { const processedValue = Array.isArray(value) ? value : typeof value === "string" && /[^a-zA-Z0-9]/.test(value) ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean) : [value];
filters.push({ id: key, value: processedValue, }); } return filters; }, [], ); }, [filterValues, enableAdvancedFilter]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(initialColumnFilters);
const onColumnFiltersChange = React.useCallback( (updaterOrValue: Updater<ColumnFiltersState>) => { if (enableAdvancedFilter) return;
setColumnFilters((prev) => { const next = typeof updaterOrValue === "function" ? updaterOrValue(prev) : updaterOrValue;
const filterUpdates = next.reduce< Record<string, string | string[] | null> >((acc, filter) => { if (filterableColumns.find((column) => column.id === filter.id)) { acc[filter.id] = filter.value as string | string[]; } return acc; }, {});
for (const prevFilter of prev) { if (!next.some((filter) => filter.id === prevFilter.id)) { filterUpdates[prevFilter.id] = null; } }
debouncedSetFilterValues(filterUpdates); return next; }); }, [debouncedSetFilterValues, filterableColumns, enableAdvancedFilter], );
const table = useReactTable({ ...tableProps, columns, initialState, pageCount, state: { pagination, sorting, columnVisibility, rowSelection, columnFilters, }, defaultColumn: { ...tableProps.defaultColumn, enableColumnFilter: false, }, enableRowSelection: true, onRowSelectionChange: setRowSelection, onPaginationChange, onSortingChange, onColumnFiltersChange, onColumnVisibilityChange: setColumnVisibility, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), getFacetedMinMaxValues: getFacetedMinMaxValues(), manualPagination: true, manualSorting: true, manualFiltering: true, meta: { ...tableProps.meta, queryKeys: { page: pageKey, perPage: perPageKey, sort: sortKey, filters: filtersKey, joinOperator: joinOperatorKey, }, }, });
return React.useMemo( () => ({ table, shallow, debounceMs, throttleMs }), [table, shallow, debounceMs, throttleMs], );}