import "./table.scss"; import * as React from "react"; import { observer } from "mobx-react"; import { computed, observable } from "mobx"; import { autobind, cssNames, noop } from "../../utils"; import { TableRow, TableRowElem, TableRowProps } from "./table-row"; import { TableHead, TableHeadElem, TableHeadProps } from "./table-head"; import { TableCellElem } from "./table-cell"; import { VirtualList } from "../virtual-list"; import { navigation, setQueryParams } from "../../navigation"; import orderBy from "lodash/orderBy" import { ItemObject } from "../../item.store"; // todo: refactor + decouple search from location export type SortBy = string; export type OrderBy = "asc" | "desc" | string; export type SortParams = { sortBy: SortBy; orderBy: OrderBy } export type SortingCallback = (data: D) => string | number | (string | number)[]; export interface TableProps extends React.DOMAttributes { items?: ItemObject[]; // Raw items data className?: string; autoSize?: boolean; // Setup auto-sizing for all columns (flex: 1 0) selectable?: boolean; // Highlight rows on hover scrollable?: boolean; // Use scrollbar if content is bigger than parent's height storageKey?: string; // Keep some data in localStorage & restore on page reload, e.g sorting params sortable?: { // Define sortable callbacks for every column in // @sortItem argument in the callback is an object, provided in [sortBy: string]: SortingCallback; }; sortSyncWithUrl?: boolean; // sorting state is managed globally from url params sortByDefault?: Partial; // default sorting params onSort?: (params: SortParams) => void; // callback on sort change, default: global sync with url noItems?: React.ReactNode; // Show no items state table list is empty selectedItemId?: string; // Allows to scroll list to selected item virtual?: boolean; // Use virtual list component to render only visible rows rowPadding?: string; rowLineHeight?: string; customRowHeights?: (item: object, lineHeight: number, paddings: number) => number; getTableRow?: (uid: string) => React.ReactElement; } @observer export class Table extends React.Component { static defaultProps: TableProps = { scrollable: true, autoSize: true, rowPadding: "8px", rowLineHeight: "17px", sortSyncWithUrl: true, } @observable sortParamsLocal = this.props.sortByDefault; @computed get sortParams(): Partial { if (this.props.sortSyncWithUrl) { const sortBy = navigation.searchParams.get("sortBy") const orderBy = navigation.searchParams.get("orderBy") return { sortBy, orderBy }; } return this.sortParamsLocal || {}; } renderHead() { const { sortable, children } = this.props; const content = React.Children.toArray(children) as (TableRowElem | TableHeadElem)[]; const headElem: React.ReactElement = content.find(elem => elem.type === TableHead); if (headElem) { if (sortable) { const columns = React.Children.toArray(headElem.props.children) as TableCellElem[]; return React.cloneElement(headElem, { children: columns.map(elem => { if (elem.props.checkbox) { return elem; } const title = elem.props.title || ( // copy cell content to title if it's a string // usable if part of TableCell's content is hidden when there is not enough space typeof elem.props.children === "string" ? elem.props.children : undefined ); return React.cloneElement(elem, { title: title, _sort: this.sort, _sorting: this.sortParams, _nowrap: headElem.props.nowrap, }) }) }); } return headElem; } } getSorted(items: any[]) { const { sortParams } = this; const sortingCallback = this.props.sortable[sortParams.sortBy] || noop; return orderBy( items, sortingCallback, sortParams.orderBy as any ); } @autobind() protected onSort(params: SortParams) { const { sortSyncWithUrl, onSort } = this.props; if (sortSyncWithUrl) { setQueryParams(params) } else { this.sortParamsLocal = params; } if (onSort) { onSort(params) } } @autobind() sort(colName: SortBy) { const { sortBy, orderBy } = this.sortParams; const sameColumn = sortBy == colName; const newSortBy: SortBy = colName; const newOrderBy: OrderBy = (!orderBy || !sameColumn || orderBy === "desc") ? "asc" : "desc"; this.onSort({ sortBy: String(newSortBy), orderBy: newOrderBy, }); } renderRows() { const { sortable, noItems, children, virtual, customRowHeights, rowLineHeight, rowPadding, items, getTableRow, selectedItemId, className } = this.props; const content = React.Children.toArray(children) as (TableRowElem | TableHeadElem)[]; let rows: React.ReactElement[] = content.filter(elem => elem.type === TableRow); let sortedItems = rows.length ? rows.map(row => row.props.sortItem) : [...items]; if (sortable) { sortedItems = this.getSorted(sortedItems); if (rows.length) { rows = sortedItems.map(item => rows.find(row => { return item == row.props.sortItem })) } } if (!rows.length && !items.length && noItems) { return noItems; } if (virtual) { const lineHeight = parseFloat(rowLineHeight); const padding = parseFloat(rowPadding); let rowHeights: number[] = Array(items.length).fill(lineHeight + padding * 2); if (customRowHeights) { rowHeights = sortedItems.map(item => { return customRowHeights(item, lineHeight, padding * 2); }); } return ( ); } return rows; } render() { const { selectable, scrollable, sortable, autoSize, virtual } = this.props; let { className } = this.props; className = cssNames("Table flex column", className, { selectable, scrollable, sortable, autoSize, virtual, }); return (
{this.renderHead()} {this.renderRows()}
) } }