import { Button, Menu, MenuDivider, MenuItem, MenuList, MenuPopover, MenuTrigger, TableRowData as RowStateBase, SortDirection, Table, TableBody, TableCell, TableCellLayout, TableColumnDefinition, TableColumnId, TableColumnSizingOptions, TableHeader, TableHeaderCell, TableRow, TableRowId, TableSelectionCell, tokens, useArrowNavigationGroup, useTableColumnSizing_unstable, useTableFeatures, useTableSelection, useTableSort, } from "@fluentui/react-components"; import { ArrowClockwise16Regular, ArrowResetRegular, DeleteRegular, EditRegular, MoreHorizontalRegular, TableResizeColumnRegular, TextSortAscendingRegular, TextSortDescendingRegular, } from "@fluentui/react-icons"; import { NormalizedEventKey } from "Common/Constants"; import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane"; import { ColumnSizesMap, ColumnSort, deleteDocumentsTabSubComponentState, readDocumentsTabSubComponentState, saveDocumentsTabSubComponentState, SubComponentName, } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil"; import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper"; import { LayoutConstants } from "Explorer/Theme/ThemeUtil"; import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils"; import { useSidePanel } from "hooks/useSidePanel"; import React, { useCallback, useMemo } from "react"; import { FixedSizeList as List, ListChildComponentProps } from "react-window"; import * as ViewModels from "../../../Contracts/ViewModels"; export type DocumentsTableComponentItem = { id: string; } & Record; export type ColumnDefinition = { id: string; label: string; isPartitionKey: boolean; }; export interface IDocumentsTableComponentProps { onRefreshTable: () => void; items: DocumentsTableComponentItem[]; onItemClicked: (index: number) => void; onSelectedRowsChange: (selectedItemsIndices: Set) => void; selectedRows: Set; size: { height: number; width: number }; selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[]; style?: React.CSSProperties; isRowSelectionDisabled?: boolean; collection: ViewModels.CollectionBase; onColumnSelectionChange?: (newSelectedColumnIds: string[]) => void; defaultColumnSelection?: string[]; isColumnSelectionDisabled?: boolean; } interface TableRowData extends RowStateBase { onClick: (e: React.MouseEvent) => void; onKeyDown: (e: React.KeyboardEvent) => void; selected: boolean; appearance: "brand" | "none"; } interface ReactWindowRenderFnProps extends ListChildComponentProps { data: TableRowData[]; } const COLUMNS_MENU_NAME = "columnsMenu"; const defaultSize = { idealWidth: 200, minWidth: 50, }; export const DocumentsTableComponent: React.FC = ({ onRefreshTable, items, onSelectedRowsChange, selectedRows, style, size, selectedColumnIds, columnDefinitions, isRowSelectionDisabled: isSelectionDisabled, collection, onColumnSelectionChange, defaultColumnSelection, isColumnSelectionDisabled, }: IDocumentsTableComponentProps) => { const styles = useDocumentsTabStyles(); const [columnSizingOptions, setColumnSizingOptions] = React.useState(() => { const columnSizesMap: ColumnSizesMap = readDocumentsTabSubComponentState( SubComponentName.ColumnSizes, collection, {}, ); const columnSizesPx: TableColumnSizingOptions = {}; selectedColumnIds.forEach((columnId) => { if ( !columnSizesMap || !columnSizesMap[columnId] || columnSizesMap[columnId].widthPx === undefined || isNaN(columnSizesMap[columnId].widthPx) ) { columnSizesPx[columnId] = defaultSize; } else { columnSizesPx[columnId] = { idealWidth: columnSizesMap[columnId].widthPx, minWidth: 50, }; } }); return columnSizesPx; }); const [sortState, setSortState] = React.useState<{ sortDirection: "ascending" | "descending"; sortColumn: TableColumnId | undefined; }>(() => { const sort = readDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection, undefined); if (!sort) { return { sortDirection: undefined, sortColumn: undefined, }; } return { sortDirection: sort.direction, sortColumn: sort.columnId, }; }); const onColumnResize = React.useCallback((_, { columnId, width }: { columnId: string; width: number }) => { setColumnSizingOptions((state) => { const newSizingOptions = { ...state, [columnId]: { ...state[columnId], idealWidth: width, }, }; const persistentSizes = Object.keys(newSizingOptions).reduce((acc, key) => { acc[key] = { widthPx: newSizingOptions[key].idealWidth, }; return acc; }, {} as ColumnSizesMap); saveDocumentsTabSubComponentState( SubComponentName.ColumnSizes, collection, persistentSizes, true, ); return newSizingOptions; }); }, []); // const restoreFocusTargetAttribute = useRestoreFocusTarget(); const onSortClick = (event: React.SyntheticEvent, columnId: string, direction: SortDirection) => { setColumnSort(event, columnId, direction); if (columnId === undefined || direction === undefined) { deleteDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection); return; } saveDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection, { columnId, direction, }); }; // Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes const columns: TableColumnDefinition[] = useMemo( () => columnDefinitions .filter((column) => selectedColumnIds.includes(column.id)) .map((column) => ({ columnId: column.id, compare: (a, b) => { if (typeof a[column.id] === "string") { return (a[column.id] as string).localeCompare(b[column.id] as string); } else if (typeof a[column.id] === "number") { return (a[column.id] as number) - (b[column.id] as number); } else { // Should not happen return 0; } }, renderHeaderCell: () => ( <> {column.label} ), renderCell: (item) => ( {item[column.id]} ), })), [columnDefinitions, onColumnSelectionChange, selectedColumnIds], ); const [selectionStartIndex, setSelectionStartIndex] = React.useState(INITIAL_SELECTED_ROW_INDEX); const onTableCellClicked = useCallback( (e: React.MouseEvent, index: number) => { if (isSelectionDisabled) { // Only allow click onSelectedRowsChange(new Set([index])); setSelectionStartIndex(index); return; } const result = selectionHelper( selectedRows as Set, index, isEnvironmentShiftPressed(e), isEnvironmentCtrlPressed(e), selectionStartIndex, ); onSelectedRowsChange(result.selection); if (result.selectionStartIndex !== undefined) { setSelectionStartIndex(result.selectionStartIndex); } }, [isSelectionDisabled, selectedRows, selectionStartIndex, onSelectedRowsChange], ); /** * Callback for when: * - a key has been pressed on the cell * - a key is down and the cell is clicked by the mouse */ const onIdClicked = useCallback( (e: React.KeyboardEvent, index: number) => { if (e.key === NormalizedEventKey.Enter || e.key === NormalizedEventKey.Space) { onSelectedRowsChange(new Set([index])); } }, [onSelectedRowsChange], ); const RenderRow = ({ index, style, data }: ReactWindowRenderFnProps) => { const { item, selected, appearance, onClick, onKeyDown } = data[index]; return ( {!isSelectionDisabled && ( { setSelectionStartIndex(index); onClick(e); }} onKeyDown={onKeyDown} /> )} {columns.map((column) => ( ) => onTableCellClicked(e, index)} onKeyPress={(e: React.KeyboardEvent) => onIdClicked(e, index)} {...columnSizing.getTableCellProps(column.columnId)} tabIndex={column.columnId === "id" ? 0 : -1} > {column.renderCell(item)} ))} ); }; const { getRows, columnSizing_unstable: columnSizing, tableRef, selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, sort: { getSortDirection, setColumnSort, sort }, } = useTableFeatures( { columns, items, }, [ useTableColumnSizing_unstable({ columnSizingOptions, onColumnResize }), useTableSelection({ selectionMode: isSelectionDisabled ? "single" : "multiselect", selectedItems: selectedRows, // eslint-disable-next-line react/prop-types onSelectionChange: (e, data) => onSelectedRowsChange(data.selectedItems), }), useTableSort({ sortState, onSortChange: (e, nextSortState) => setSortState(nextSortState), }), ], ); const headerSortProps = (columnId: TableColumnId) => ({ // onClick: (e: React.MouseEvent) => toggleColumnSort(e, columnId), sortDirection: getSortDirection(columnId), }); const rows: TableRowData[] = sort( getRows((row) => { const selected = isRowSelected(row.rowId); return { ...row, onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId), onKeyDown: (e: React.KeyboardEvent) => { if (e.key === " ") { e.preventDefault(); toggleRow(e, row.rowId); } }, selected, appearance: selected ? ("brand" as const) : ("none" as const), }; }), ); const toggleAllKeydown = React.useCallback( (e: React.KeyboardEvent) => { if (e.key === " ") { toggleAllRows(e); e.preventDefault(); } }, [toggleAllRows], ); // Cell keyboard navigation const keyboardNavAttr = useArrowNavigationGroup({ axis: "grid" }); // TODO: Bug in fluent UI typings that requires any here // eslint-disable-next-line @typescript-eslint/no-explicit-any const tableProps: any = { "aria-label": "Filtered documents table", role: "grid", ...columnSizing.getTableProps(), ...keyboardNavAttr, size: "small", ref: tableRef, ...style, }; const checkedValues: { [COLUMNS_MENU_NAME]: string[] } = { [COLUMNS_MENU_NAME]: [], }; columnDefinitions.forEach( (columnDefinition) => selectedColumnIds.includes(columnDefinition.id) && checkedValues[COLUMNS_MENU_NAME].push(columnDefinition.id), ); const openColumnSelectionPane = (): void => { useSidePanel .getState() .openSidePanel( "Select columns", , ); }; return ( {!isSelectionDisabled && ( )} {columns.map((column) => ( {column.renderHeaderCell()} ))}
{RenderRow}
); };