import { useLayoutEffect, useMemo, useRef, useState } from "react"; import { Table } from "antd"; import Icon, { FilterFilled, FilterOutlined } from "@ant-design/icons"; import type { ColumnGroupType, ColumnsType, ColumnType } from "antd/es/table"; import type { TableProps } from "antd"; import { CustomFilterDropdown } from "./CustomFilterDropdown"; import { Highlight } from "./Highlight"; import { isGroup, LeafColumnDef, type ColumnFilters, type FilterOption, type SortState, type TableConfig, } from "./tableTypes"; type ExtraColumn = ColumnType & { position?: number | "start" | "end"; afterKey?: Extract; }; interface DataTableProps> { config: TableConfig; rows: T[]; allRows: T[]; loading?: boolean; visibleKeys: Array>; globalQuery?: string; columnFilters: ColumnFilters; onColumnFiltersChange: (next: ColumnFilters) => void; sortState: SortState; onSortChange: (next: SortState) => void; extraColumns?: ColumnsType; rowSelection?: TableProps["rowSelection"]; scrollY?: number | string; onRow?: TableProps["onRow"]; } export function DataTable>({ config, rows, allRows, loading, visibleKeys, globalQuery = "", columnFilters, onColumnFiltersChange, sortState, onSortChange, extraColumns = [], rowSelection, scrollY = 480, onRow, }: DataTableProps) { const { columns: defs, searchableKeys = [], rowKey } = config; const normalizedQuery = globalQuery.trim().toLowerCase(); const hasGlobalSearch = searchableKeys.length > 0 && normalizedQuery.length > 0; const getOptions = (key: Extract): FilterOption[] => { const seen = new Set(); const opts: FilterOption[] = []; for (const r of allRows) { const v = r[key]; if (!seen.has(v)) { seen.add(v); opts.push({ label: String(v), value: v }); } } return opts; }; const buildLeaf = (def: LeafColumnDef): ColumnType => { const { key, title, features = {}, render, width, fixed } = def; const hasDropdown = features.multiselect || features.search || features.sort; const isSearchable = searchableKeys.includes(key); return { key, title, dataIndex: key, width, fixed, render: (value: T[keyof T], row: T) => { if (render) return render(value, row); const text = String(value ?? ""); return isSearchable && hasGlobalSearch ? ( ) : ( text ); }, ...(hasDropdown ? { filterIcon: () => { const active = (columnFilters[key]?.length ?? 0) > 0 || sortState?.key === key; return ( ); }, filterDropdown: ({ confirm, clearFilters }) => ( onColumnFiltersChange({ ...columnFilters, [key]: keys as (string | number)[], }) } confirm={confirm} clearFilters={clearFilters} features={features} options={ features.multiselect || features.search ? getOptions(key) : [] } sortDirection={sortState?.key === key ? sortState.dir : null} onSort={(dir) => onSortChange(dir ? { key, dir } : null)} /> ), } : {}), }; }; const columns: ColumnsType = useMemo(() => { const built: ColumnsType = []; for (const def of defs) { if (isGroup(def)) { const visibleChildren = def.children.filter((c) => visibleKeys.includes(c.key), ); if (visibleChildren.length === 0) continue; built.push({ key: def.groupKey, title: def.title, fixed: def.fixed, children: visibleChildren.map(buildLeaf), } as ColumnGroupType); } else { if (!visibleKeys.includes(def.key)) continue; built.push(buildLeaf(def)); } } for (const col of extraColumns) { const { position, afterKey, ...rest } = col as ExtraColumn & { position?: any; afterKey?: any; }; let idx: number; if (afterKey) { const f = built.findIndex( (c: any) => c.key === afterKey || c.dataIndex === afterKey, ); idx = f === -1 ? built.length : f + 1; } else if (position === "start") idx = 0; else if (position === "end" || position === undefined) idx = built.length; else idx = Math.max(0, Math.min(position, built.length)); built.splice(idx, 0, rest as ColumnsType[number]); } return built; }, [ defs, visibleKeys, sortState, allRows, searchableKeys, globalQuery, hasGlobalSearch, columnFilters, onColumnFiltersChange, onSortChange, extraColumns, ]); const wrapperRef = useRef(null); const [appliedScrollY, setAppliedScrollY] = useState(scrollY); useLayoutEffect(() => { const wrapper = wrapperRef.current; if (!wrapper) return; const update = () => { const total = wrapper.clientHeight; const header = wrapper.querySelector(".ant-table-thead"); const headerH = header?.offsetHeight ?? 55; setAppliedScrollY(Math.max(0, total - headerH)); }; update(); const ro = new ResizeObserver(update); ro.observe(wrapper); return () => ro.disconnect(); }, []); return (
rowKey={rowKey} loading={loading} columns={columns} dataSource={rows} rowSelection={rowSelection} pagination={false} scroll={{ x: "max-content", y: appliedScrollY }} onRow={onRow} />
); }