import { ArrowUpIcon, FilterIcon } from "lucide-react"; import { iconSize } from "../../icons/Icon"; import React, { RefObject, useCallback, useEffect, useState } from "react"; import { deepEqual as equal } from "fast-equals"; import { VirtualTableColumn, VirtualTableSort, VirtualTableWhereFilterOp } from "./VirtualTableProps"; import { ErrorBoundary } from "../ErrorBoundary"; import { Badge } from "../Badge"; import { Button } from "../Button"; import { IconButton } from "../IconButton"; import { Popover } from "../Popover"; import { defaultBorderMixin } from "../../styles"; import { cls } from "../../util/cls"; interface FilterFormProps { column: VirtualTableColumn; onFilterUpdate: (filter?: [VirtualTableWhereFilterOp, unknown], newOpenFilterState?: boolean) => void; filter?: [VirtualTableWhereFilterOp, unknown]; onHover: boolean, createFilterField: (props: FilterFormFieldProps) => React.ReactNode; hidden: boolean; setHidden: (hidden: boolean) => void; } export type FilterFormFieldProps = { id: React.Key, filterValue: [VirtualTableWhereFilterOp, unknown] | undefined, setFilterValue: (filterValue?: [VirtualTableWhereFilterOp, unknown]) => void; column: VirtualTableColumn; hidden: boolean; setHidden: (hidden: boolean) => void; }; type VirtualTableHeaderProps> = { resizeHandleRef: React.Ref; columnIndex: number; isResizingIndex: number; column: VirtualTableColumn; onColumnSort: (key: Extract, additive?: boolean) => void; filter?: [VirtualTableWhereFilterOp, unknown]; sort: VirtualTableSort; /** * This column's zero-based rank in a multi-key sort, or `undefined` when * only one column is sorted and a rank would say nothing. */ sortPosition?: number; onFilterUpdate: (column: VirtualTableColumn, filterForProperty?: [VirtualTableWhereFilterOp, unknown]) => void; onClickResizeColumn?: (columnIndex: number, column: VirtualTableColumn) => void; createFilterField?: (props: FilterFormFieldProps) => React.ReactNode; AdditionalHeaderWidget?: (props: { onHover: boolean }) => React.ReactNode; isDragging?: boolean; isDraggable?: boolean; }; /** * What the next click on this column's sort control will do, spelled out for * the tooltip and for screen readers. An unlabelled arrow that means three * different things depending on the state it is in is a control you have to * click to learn. */ function sortActionTitle(sort: VirtualTableSort, sortPosition?: number): string { const rank = sortPosition !== undefined ? ` (sort key ${sortPosition + 1})` : ""; const addKey = " — shift-click to add it under the current sort instead"; if (sort === "asc") return `Sorted ascending${rank}. Click to sort descending.${addKey}`; if (sort === "desc") return `Sorted descending${rank}. Click to remove this sort.${addKey}`; return `Click to sort ascending.${addKey}`; } export const VirtualTableHeader = React.memo>>( function VirtualTableHeader>({ resizeHandleRef, columnIndex, isResizingIndex, sort, sortPosition, onColumnSort, onFilterUpdate, filter, column, onClickResizeColumn, createFilterField, AdditionalHeaderWidget, isDragging, isDraggable }: VirtualTableHeaderProps) { const [onHover, setOnHover] = useState(false); const [openFilter, setOpenFilter] = React.useState(false); const [hidden, setHidden] = React.useState(false); const handleSettingsClick = useCallback((_event: React.MouseEvent) => { setOpenFilter(true); }, []); const update = useCallback((filterForProperty?: [VirtualTableWhereFilterOp, unknown], newOpenFilterState?: boolean) => { onFilterUpdate(column, filterForProperty); if (newOpenFilterState !== undefined) setOpenFilter(newOpenFilterState); }, [column, onFilterUpdate]); const thisColumnIsResizing = isResizingIndex === columnIndex; const anotherColumnIsResizing = isResizingIndex !== columnIndex && isResizingIndex > 0; const hovered = !anotherColumnIsResizing && (onHover || thisColumnIsResizing); return (
setOnHover(true)} onMouseMove={() => setOnHover(true)} onMouseLeave={() => setOnHover(false)} >
{column.icon && (
{React.isValidElement(column.icon) ? ( React.cloneElement(column.icon as React.ReactElement, { size: 14, className: cls((column.icon as React.ReactElement).props?.className, "flex-shrink-0") }) ) : ( column.icon )}
)}
{column.title}
<> {AdditionalHeaderWidget && } {column.sortable && (sort || hovered || openFilter) &&
{ onColumnSort(column.key as Extract, event.shiftKey); }} > {!sort && } {sort === "asc" && } {sort === "desc" && } {/* The rank, shown only once there is more than one key: two arrows with no numbers beside them say a table is sorted by both columns and nothing about which one decides. */} {sort && sortPosition !== undefined && ( {sortPosition + 1} )}
} {column.filter && createFilterField &&
} >
} {column.resizable &&
} data-no-dnd="true" className={cls( "absolute h-full w-[6px] top-0 right-0 cursor-col-resize", hovered && "bg-surface-300 dark:bg-surface-700" )} onPointerDown={(e) => { e.stopPropagation(); if (onClickResizeColumn) { onClickResizeColumn(columnIndex, column); } }} />}
); }, equal) as React.FunctionComponent>>; function FilterForm({ column, onFilterUpdate, filter, onHover, createFilterField, hidden, setHidden }: FilterFormProps) { const id = column.key; const [filterInternal, setFilterInternal] = useState<[VirtualTableWhereFilterOp, unknown] | undefined>(filter); useEffect(() => { setFilterInternal(filter); }, [filter]); if (!column.filter) return null; const submit = () => { onFilterUpdate(filterInternal, false); }; const reset = (_e: React.MouseEvent) => { onFilterUpdate(undefined, false); }; const filterIsSet = !!filter; const filterField = createFilterField({ id, filterValue: filterInternal, setFilterValue: setFilterInternal, column, hidden, setHidden }); if (!filterField) return null; return (
{ e.stopPropagation(); e.preventDefault(); submit(); }} className={"text-surface-900 dark:text-white"}>
{column.title ?? id}
{filterField &&
{filterField}
}
); }