| 
									
										
										
										
											2025-08-22 03:22:30 +08:00
										 |  |  | import { useState, useMemo, useCallback, useRef, useLayoutEffect, RefObject, CSSProperties, useEffect } from 'react'; | 
					
						
							| 
									
										
										
										
											2025-07-11 20:50:25 +08:00
										 |  |  | import { Column, DataGridHandle, DataGridProps, SortColumn } from 'react-data-grid'; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-06 03:46:07 +08:00
										 |  |  | import { compareArrayValues, Field, FieldType, formattedValueToString, reduceField, ReducerID } from '@grafana/data'; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  | import { TableColumnResizeActionCallback } from '../types'; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | import { TABLE } from './constants'; | 
					
						
							| 
									
										
										
										
											2025-09-06 03:46:07 +08:00
										 |  |  | import { FilterType, FooterFieldState, TableRow, TableSortByFieldState, TableSummaryRow, TypographyCtx } from './types'; | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | import { | 
					
						
							|  |  |  |   getDisplayName, | 
					
						
							|  |  |  |   processNestedTableRows, | 
					
						
							|  |  |  |   applySort, | 
					
						
							|  |  |  |   getColumnTypes, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |   getRowHeight, | 
					
						
							| 
									
										
										
										
											2025-08-22 03:22:30 +08:00
										 |  |  |   computeColWidths, | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   buildHeaderHeightMeasurers, | 
					
						
							|  |  |  |   buildCellHeightMeasurers, | 
					
						
							| 
									
										
										
										
											2025-10-01 04:33:11 +08:00
										 |  |  |   IS_SAFARI_26, | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | } from './utils'; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | // Helper function to get displayed value
 | 
					
						
							|  |  |  | const getDisplayedValue = (row: TableRow, key: string, fields: Field[]) => { | 
					
						
							|  |  |  |   const field = fields.find((field) => getDisplayName(field) === key); | 
					
						
							|  |  |  |   if (!field || !field.display) { | 
					
						
							|  |  |  |     return ''; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const displayedValue = formattedValueToString(field.display(row[key])); | 
					
						
							|  |  |  |   return displayedValue; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export interface FilteredRowsResult { | 
					
						
							|  |  |  |   rows: TableRow[]; | 
					
						
							|  |  |  |   filter: FilterType; | 
					
						
							|  |  |  |   setFilter: React.Dispatch<React.SetStateAction<FilterType>>; | 
					
						
							|  |  |  |   crossFilterOrder: string[]; | 
					
						
							|  |  |  |   crossFilterRows: Record<string, TableRow[]>; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export interface FilteredRowsOptions { | 
					
						
							|  |  |  |   hasNestedFrames: boolean; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function useFilteredRows( | 
					
						
							|  |  |  |   rows: TableRow[], | 
					
						
							|  |  |  |   fields: Field[], | 
					
						
							|  |  |  |   { hasNestedFrames }: FilteredRowsOptions | 
					
						
							|  |  |  | ): FilteredRowsResult { | 
					
						
							|  |  |  |   // TODO: allow persisted filter selection via url
 | 
					
						
							|  |  |  |   const [filter, setFilter] = useState<FilterType>({}); | 
					
						
							|  |  |  |   const filterValues = useMemo(() => Object.entries(filter), [filter]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const crossFilterOrder: FilteredRowsResult['crossFilterOrder'] = useMemo( | 
					
						
							|  |  |  |     () => Array.from(new Set(filterValues.map(([key]) => key))), | 
					
						
							|  |  |  |     [filterValues] | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const [filteredRows, crossFilterRows] = useMemo(() => { | 
					
						
							|  |  |  |     const crossFilterRows: FilteredRowsResult['crossFilterRows'] = {}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const filterRows = (row: TableRow): boolean => { | 
					
						
							|  |  |  |       for (const [key, value] of filterValues) { | 
					
						
							|  |  |  |         const displayedValue = getDisplayedValue(row, key, fields); | 
					
						
							|  |  |  |         if (!value.filteredSet.has(displayedValue)) { | 
					
						
							|  |  |  |           return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         // collect rows for crossFilter
 | 
					
						
							|  |  |  |         crossFilterRows[key] = crossFilterRows[key] ?? []; | 
					
						
							|  |  |  |         crossFilterRows[key].push(row); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return true; | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const filteredRows = hasNestedFrames | 
					
						
							|  |  |  |       ? processNestedTableRows(rows, (parents) => parents.filter(filterRows)) | 
					
						
							|  |  |  |       : rows.filter(filterRows); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return [filteredRows, crossFilterRows]; | 
					
						
							|  |  |  |   }, [filterValues, rows, fields, hasNestedFrames]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     rows: filteredRows, | 
					
						
							|  |  |  |     filter, | 
					
						
							|  |  |  |     setFilter, | 
					
						
							|  |  |  |     crossFilterOrder, | 
					
						
							|  |  |  |     crossFilterRows, | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export interface SortedRowsOptions { | 
					
						
							|  |  |  |   hasNestedFrames: boolean; | 
					
						
							|  |  |  |   initialSortBy?: TableSortByFieldState[]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export interface SortedRowsResult { | 
					
						
							|  |  |  |   rows: TableRow[]; | 
					
						
							|  |  |  |   sortColumns: SortColumn[]; | 
					
						
							|  |  |  |   setSortColumns: React.Dispatch<React.SetStateAction<SortColumn[]>>; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function useSortedRows( | 
					
						
							|  |  |  |   rows: TableRow[], | 
					
						
							|  |  |  |   fields: Field[], | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |   { initialSortBy, hasNestedFrames }: SortedRowsOptions | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | ): SortedRowsResult { | 
					
						
							|  |  |  |   const initialSortColumns = useMemo<SortColumn[]>( | 
					
						
							|  |  |  |     () => | 
					
						
							|  |  |  |       initialSortBy?.flatMap(({ displayName, desc }) => { | 
					
						
							|  |  |  |         if (!fields.some((f) => getDisplayName(f) === displayName)) { | 
					
						
							|  |  |  |           return []; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return [ | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             columnKey: displayName, | 
					
						
							|  |  |  |             direction: desc ? ('DESC' as const) : ('ASC' as const), | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |         ]; | 
					
						
							|  |  |  |       }) ?? [], | 
					
						
							|  |  |  |     [] // eslint-disable-line react-hooks/exhaustive-deps
 | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  |   const [sortColumns, setSortColumns] = useState<SortColumn[]>(initialSortColumns); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |   const columnTypes = useMemo(() => getColumnTypes(fields), [fields]); | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   const sortedRows = useMemo( | 
					
						
							|  |  |  |     () => applySort(rows, fields, sortColumns, columnTypes, hasNestedFrames), | 
					
						
							|  |  |  |     [rows, fields, sortColumns, hasNestedFrames, columnTypes] | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     rows: sortedRows, | 
					
						
							|  |  |  |     sortColumns, | 
					
						
							|  |  |  |     setSortColumns, | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export interface PaginatedRowsOptions { | 
					
						
							|  |  |  |   height: number; | 
					
						
							|  |  |  |   width: number; | 
					
						
							| 
									
										
										
										
											2025-08-02 07:56:12 +08:00
										 |  |  |   rowHeight: NonNullable<CSSProperties['height']> | ((row: TableRow) => number); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |   headerHeight: number; | 
					
						
							|  |  |  |   footerHeight: number; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |   paginationHeight?: number; | 
					
						
							| 
									
										
										
										
											2025-07-04 10:46:03 +08:00
										 |  |  |   enabled: boolean; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export interface PaginatedRowsResult { | 
					
						
							|  |  |  |   rows: TableRow[]; | 
					
						
							|  |  |  |   page: number; | 
					
						
							|  |  |  |   setPage: React.Dispatch<React.SetStateAction<number>>; | 
					
						
							|  |  |  |   numPages: number; | 
					
						
							|  |  |  |   rowsPerPage: number; | 
					
						
							|  |  |  |   pageRangeStart: number; | 
					
						
							|  |  |  |   pageRangeEnd: number; | 
					
						
							|  |  |  |   smallPagination: boolean; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // hand-measured. pagination height is 30px, plus 8px top margin
 | 
					
						
							|  |  |  | const PAGINATION_HEIGHT = 38; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function usePaginatedRows( | 
					
						
							|  |  |  |   rows: TableRow[], | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |   { height, width, headerHeight, footerHeight, rowHeight, enabled }: PaginatedRowsOptions | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | ): PaginatedRowsResult { | 
					
						
							|  |  |  |   // TODO: allow persisted page selection via url
 | 
					
						
							|  |  |  |   const [page, setPage] = useState(0); | 
					
						
							|  |  |  |   const numRows = rows.length; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // calculate average row height if row height is variable.
 | 
					
						
							|  |  |  |   const avgRowHeight = useMemo(() => { | 
					
						
							| 
									
										
										
										
											2025-07-04 10:46:03 +08:00
										 |  |  |     if (!enabled) { | 
					
						
							|  |  |  |       return 0; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     if (typeof rowHeight === 'number') { | 
					
						
							|  |  |  |       return rowHeight; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-07-04 10:46:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-02 07:56:12 +08:00
										 |  |  |     // when using auto-sized rows, we're just going to have to pick a number. the alternative
 | 
					
						
							|  |  |  |     // is to measure each row, which we could do but would be expensive.
 | 
					
						
							|  |  |  |     if (typeof rowHeight === 'string') { | 
					
						
							|  |  |  |       return TABLE.MAX_CELL_HEIGHT; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-04 10:46:03 +08:00
										 |  |  |     // we'll just measure 100 rows to estimate
 | 
					
						
							|  |  |  |     return rows.slice(0, 100).reduce((avg, row, _, { length }) => avg + rowHeight(row) / length, 0); | 
					
						
							|  |  |  |   }, [rows, rowHeight, enabled]); | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-22 03:22:30 +08:00
										 |  |  |   const smallPagination = useMemo(() => enabled && width < TABLE.PAGINATION_LIMIT, [enabled, width]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |   // using dimensions of the panel, calculate pagination parameters
 | 
					
						
							| 
									
										
										
										
											2025-08-22 03:22:30 +08:00
										 |  |  |   const { numPages, rowsPerPage, pageRangeStart, pageRangeEnd } = useMemo((): { | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     numPages: number; | 
					
						
							|  |  |  |     rowsPerPage: number; | 
					
						
							|  |  |  |     pageRangeStart: number; | 
					
						
							|  |  |  |     pageRangeEnd: number; | 
					
						
							|  |  |  |   } => { | 
					
						
							|  |  |  |     if (!enabled) { | 
					
						
							| 
									
										
										
										
											2025-08-22 03:22:30 +08:00
										 |  |  |       return { numPages: 0, rowsPerPage: 0, pageRangeStart: 1, pageRangeEnd: numRows }; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // calculate number of rowsPerPage based on height stack
 | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |     const rowAreaHeight = height - headerHeight - footerHeight - PAGINATION_HEIGHT; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     const heightPerRow = Math.floor(rowAreaHeight / (avgRowHeight || 1)); | 
					
						
							|  |  |  |     // ensure at least one row per page is displayed
 | 
					
						
							|  |  |  |     let rowsPerPage = heightPerRow > 1 ? heightPerRow : 1; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // calculate row range for pagination summary display
 | 
					
						
							|  |  |  |     const pageRangeStart = page * rowsPerPage + 1; | 
					
						
							|  |  |  |     let pageRangeEnd = pageRangeStart + rowsPerPage - 1; | 
					
						
							|  |  |  |     if (pageRangeEnd > numRows) { | 
					
						
							|  |  |  |       pageRangeEnd = numRows; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-08-22 03:22:30 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     const numPages = Math.ceil(numRows / rowsPerPage); | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       numPages, | 
					
						
							|  |  |  |       rowsPerPage, | 
					
						
							|  |  |  |       pageRangeStart, | 
					
						
							|  |  |  |       pageRangeEnd, | 
					
						
							|  |  |  |     }; | 
					
						
							| 
									
										
										
										
											2025-08-22 03:22:30 +08:00
										 |  |  |   }, [height, headerHeight, footerHeight, avgRowHeight, enabled, numRows, page]); | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   // safeguard against page overflow on panel resize or other factors
 | 
					
						
							| 
									
										
										
										
											2025-08-02 07:56:12 +08:00
										 |  |  |   useLayoutEffect(() => { | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     if (!enabled) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (page > numPages) { | 
					
						
							|  |  |  |       // resets pagination to end
 | 
					
						
							|  |  |  |       setPage(numPages - 1); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }, [numPages, enabled, page, setPage]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // apply pagination to the sorted rows
 | 
					
						
							|  |  |  |   const paginatedRows = useMemo(() => { | 
					
						
							|  |  |  |     if (!enabled) { | 
					
						
							|  |  |  |       return rows; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const pageOffset = page * rowsPerPage; | 
					
						
							|  |  |  |     return rows.slice(pageOffset, pageOffset + rowsPerPage); | 
					
						
							|  |  |  |   }, [page, rowsPerPage, rows, enabled]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     rows: paginatedRows, | 
					
						
							|  |  |  |     page: enabled ? page : -1, | 
					
						
							|  |  |  |     setPage, | 
					
						
							|  |  |  |     numPages, | 
					
						
							|  |  |  |     rowsPerPage, | 
					
						
							|  |  |  |     pageRangeStart, | 
					
						
							|  |  |  |     pageRangeEnd, | 
					
						
							|  |  |  |     smallPagination, | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | const ICON_WIDTH = 16; | 
					
						
							|  |  |  | const ICON_GAP = 4; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | interface UseHeaderHeightOptions { | 
					
						
							|  |  |  |   enabled: boolean; | 
					
						
							|  |  |  |   fields: Field[]; | 
					
						
							|  |  |  |   columnWidths: number[]; | 
					
						
							|  |  |  |   sortColumns: SortColumn[]; | 
					
						
							|  |  |  |   typographyCtx: TypographyCtx; | 
					
						
							|  |  |  |   showTypeIcons?: boolean; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function useHeaderHeight({ | 
					
						
							|  |  |  |   fields, | 
					
						
							|  |  |  |   enabled, | 
					
						
							|  |  |  |   columnWidths, | 
					
						
							|  |  |  |   sortColumns, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |   typographyCtx, | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |   showTypeIcons = false, | 
					
						
							|  |  |  | }: UseHeaderHeightOptions): number { | 
					
						
							|  |  |  |   const perIconSpace = ICON_WIDTH + ICON_GAP; | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   const measurers = useMemo(() => buildHeaderHeightMeasurers(fields, typographyCtx), [fields, typographyCtx]); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |   const columnAvailableWidths = useMemo( | 
					
						
							|  |  |  |     () => | 
					
						
							|  |  |  |       columnWidths.map((c, idx) => { | 
					
						
							| 
									
										
										
										
											2025-08-22 04:49:53 +08:00
										 |  |  |         if (idx >= fields.length) { | 
					
						
							|  |  |  |           return 0; // no width available for this column yet
 | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |         let width = c - 2 * TABLE.CELL_PADDING - TABLE.BORDER_RIGHT; | 
					
						
							| 
									
										
										
										
											2025-08-22 04:49:53 +08:00
										 |  |  |         const field = fields[idx]; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |         // filtering icon
 | 
					
						
							| 
									
										
										
										
											2025-08-22 04:49:53 +08:00
										 |  |  |         if (field.config?.custom?.filterable) { | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |           width -= perIconSpace; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         // sorting icon
 | 
					
						
							| 
									
										
										
										
											2025-08-22 04:49:53 +08:00
										 |  |  |         if (sortColumns.some((col) => col.columnKey === getDisplayName(field))) { | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |           width -= perIconSpace; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         // type icon
 | 
					
						
							|  |  |  |         if (showTypeIcons) { | 
					
						
							|  |  |  |           width -= perIconSpace; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |         // sadly, the math for this is off by exactly 1 pixel. shrug.
 | 
					
						
							|  |  |  |         return Math.floor(width) - 1; | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |       }), | 
					
						
							|  |  |  |     [fields, columnWidths, sortColumns, showTypeIcons, perIconSpace] | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const headerHeight = useMemo(() => { | 
					
						
							|  |  |  |     if (!enabled) { | 
					
						
							|  |  |  |       return 0; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-08-22 02:02:06 +08:00
										 |  |  |     return getRowHeight( | 
					
						
							|  |  |  |       fields, | 
					
						
							|  |  |  |       -1, | 
					
						
							|  |  |  |       columnAvailableWidths, | 
					
						
							|  |  |  |       TABLE.HEADER_HEIGHT, | 
					
						
							|  |  |  |       measurers, | 
					
						
							|  |  |  |       TABLE.LINE_HEIGHT, | 
					
						
							|  |  |  |       TABLE.CELL_PADDING | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   }, [fields, enabled, columnAvailableWidths, measurers]); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return headerHeight; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | interface UseRowHeightOptions { | 
					
						
							|  |  |  |   columnWidths: number[]; | 
					
						
							|  |  |  |   fields: Field[]; | 
					
						
							|  |  |  |   hasNestedFrames: boolean; | 
					
						
							| 
									
										
										
										
											2025-08-02 07:56:12 +08:00
										 |  |  |   defaultHeight: NonNullable<CSSProperties['height']>; | 
					
						
							| 
									
										
										
										
											2025-07-17 08:11:28 +08:00
										 |  |  |   expandedRows: Set<number>; | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |   typographyCtx: TypographyCtx; | 
					
						
							| 
									
										
										
										
											2025-08-30 03:10:17 +08:00
										 |  |  |   maxHeight?: number; | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function useRowHeight({ | 
					
						
							|  |  |  |   columnWidths, | 
					
						
							|  |  |  |   fields, | 
					
						
							|  |  |  |   hasNestedFrames, | 
					
						
							|  |  |  |   defaultHeight, | 
					
						
							|  |  |  |   expandedRows, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |   typographyCtx, | 
					
						
							| 
									
										
										
										
											2025-08-30 03:10:17 +08:00
										 |  |  |   maxHeight, | 
					
						
							| 
									
										
										
										
											2025-08-02 07:56:12 +08:00
										 |  |  | }: UseRowHeightOptions): NonNullable<CSSProperties['height']> | ((row: TableRow) => number) { | 
					
						
							| 
									
										
										
										
											2025-08-30 03:10:17 +08:00
										 |  |  |   const measurers = useMemo( | 
					
						
							|  |  |  |     () => buildCellHeightMeasurers(fields, typographyCtx, maxHeight), | 
					
						
							|  |  |  |     [fields, typographyCtx, maxHeight] | 
					
						
							|  |  |  |   ); | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   const hasWrappedCols = useMemo(() => measurers?.length ?? 0 > 0, [measurers]); | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |   const colWidths = useMemo(() => { | 
					
						
							|  |  |  |     const columnWidthAffordance = 2 * TABLE.CELL_PADDING + TABLE.BORDER_RIGHT; | 
					
						
							|  |  |  |     return columnWidths.map((c) => c - columnWidthAffordance); | 
					
						
							|  |  |  |   }, [columnWidths]); | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   const rowHeight = useMemo(() => { | 
					
						
							|  |  |  |     // row height is only complicated when there are nested frames or wrapped columns.
 | 
					
						
							| 
									
										
										
										
											2025-08-02 07:56:12 +08:00
										 |  |  |     if ((!hasNestedFrames && !hasWrappedCols) || typeof defaultHeight === 'string') { | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |       return defaultHeight; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     // this cache should get blown away on resize, data refresh, updated fields, etc.
 | 
					
						
							|  |  |  |     // caching by __index is ok because sorting does not modify the __index.
 | 
					
						
							|  |  |  |     const cache: Array<number | undefined> = Array(fields[0].values.length); | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     return (row: TableRow) => { | 
					
						
							|  |  |  |       // nested rows
 | 
					
						
							| 
									
										
										
										
											2025-07-17 08:11:28 +08:00
										 |  |  |       if (row.__depth > 0) { | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |         // if unexpanded, height === 0
 | 
					
						
							| 
									
										
										
										
											2025-07-17 08:11:28 +08:00
										 |  |  |         if (!expandedRows.has(row.__index)) { | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |           return 0; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const rowCount = row.data?.length ?? 0; | 
					
						
							| 
									
										
										
										
											2025-07-10 02:54:48 +08:00
										 |  |  |         if (rowCount === 0) { | 
					
						
							|  |  |  |           return TABLE.NESTED_NO_DATA_HEIGHT + TABLE.CELL_PADDING * 2; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const nestedHeaderHeight = row.data?.meta?.custom?.noHeader ? 0 : defaultHeight; | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |         return defaultHeight * rowCount + nestedHeaderHeight + TABLE.CELL_PADDING * 2; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // regular rows
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       let result = cache[row.__index]; | 
					
						
							|  |  |  |       if (!result) { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |         result = cache[row.__index] = getRowHeight(fields, row.__index, colWidths, defaultHeight, measurers); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       } | 
					
						
							|  |  |  |       return result; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     }; | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   }, [hasNestedFrames, hasWrappedCols, defaultHeight, fields, colWidths, measurers, expandedRows]); | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return rowHeight; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * react-data-grid is a little unwieldy when it comes to column resize events. | 
					
						
							|  |  |  |  * we want to detect a few different column resize signals: | 
					
						
							|  |  |  |  *   - dragging the handle (only want to dispatch when handle is released) | 
					
						
							|  |  |  |  *   - double-clicking the handle (sets the column to the minimum width to fit content) | 
					
						
							|  |  |  |  * `onColumnResize` dispatches events throughout a dragged resize, and `onColumnWidthsChanged` doesn't | 
					
						
							|  |  |  |  * emit an event when double-click resizing occurs, so we have to build something custom on top of these | 
					
						
							|  |  |  |  * behaviors in order to get everything working. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | interface UseColumnResizeState { | 
					
						
							|  |  |  |   columnKey: string | undefined; | 
					
						
							|  |  |  |   width: number; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const INITIAL_COL_RESIZE_STATE = Object.freeze({ columnKey: undefined, width: 0 }) satisfies UseColumnResizeState; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function useColumnResize( | 
					
						
							|  |  |  |   onColumnResize: TableColumnResizeActionCallback = () => {} | 
					
						
							|  |  |  | ): DataGridProps<TableRow, TableSummaryRow>['onColumnResize'] { | 
					
						
							|  |  |  |   // these must be refs. if we used setState, we would run into race conditions with these event listeners
 | 
					
						
							|  |  |  |   const colResizeState = useRef<UseColumnResizeState>({ ...INITIAL_COL_RESIZE_STATE }); | 
					
						
							|  |  |  |   const pointerIsDown = useRef(false); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // to detect whether we got a double-click resize, we track whether the pointer is currently down
 | 
					
						
							|  |  |  |   useLayoutEffect(() => { | 
					
						
							|  |  |  |     function pointerDown(_event: PointerEvent) { | 
					
						
							|  |  |  |       pointerIsDown.current = true; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function pointerUp(_event: PointerEvent) { | 
					
						
							|  |  |  |       pointerIsDown.current = false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     window.addEventListener('pointerdown', pointerDown); | 
					
						
							|  |  |  |     window.addEventListener('pointerup', pointerUp); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return () => { | 
					
						
							|  |  |  |       window.removeEventListener('pointerdown', pointerDown); | 
					
						
							|  |  |  |       window.removeEventListener('pointerup', pointerUp); | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const dispatchEvent = useCallback(() => { | 
					
						
							|  |  |  |     if (colResizeState.current.columnKey) { | 
					
						
							|  |  |  |       onColumnResize(colResizeState.current.columnKey, Math.floor(colResizeState.current.width)); | 
					
						
							|  |  |  |       colResizeState.current = { ...INITIAL_COL_RESIZE_STATE }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     window.removeEventListener('click', dispatchEvent, { capture: true }); | 
					
						
							|  |  |  |   }, [onColumnResize]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // this is the callback that gets passed to react-data-grid
 | 
					
						
							|  |  |  |   const dataGridResizeHandler = useCallback( | 
					
						
							|  |  |  |     (column: Column<TableRow, TableSummaryRow>, width: number) => { | 
					
						
							|  |  |  |       if (!colResizeState.current.columnKey) { | 
					
						
							|  |  |  |         window.addEventListener('click', dispatchEvent, { capture: true }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       colResizeState.current.columnKey = column.key; | 
					
						
							|  |  |  |       colResizeState.current.width = width; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // when double clicking to resize, this handler will fire, but the pointer will not be down,
 | 
					
						
							|  |  |  |       // meaning that we should immediately flush the new width
 | 
					
						
							|  |  |  |       if (!pointerIsDown.current) { | 
					
						
							|  |  |  |         dispatchEvent(); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     [dispatchEvent] | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return dataGridResizeHandler; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-07-09 02:24:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-06 21:26:19 +08:00
										 |  |  | export function useScrollbarWidth(ref: RefObject<DataGridHandle>, height: number) { | 
					
						
							| 
									
										
										
										
											2025-07-11 20:50:25 +08:00
										 |  |  |   const [scrollbarWidth, setScrollbarWidth] = useState(0); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   useLayoutEffect(() => { | 
					
						
							|  |  |  |     const el = ref.current?.element; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-01 04:33:11 +08:00
										 |  |  |     if (!el || IS_SAFARI_26) { | 
					
						
							| 
									
										
										
										
											2025-08-06 21:26:19 +08:00
										 |  |  |       return; | 
					
						
							| 
									
										
										
										
											2025-07-11 20:50:25 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-08-06 21:26:19 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     const updateScrollbarDimensions = () => { | 
					
						
							|  |  |  |       setScrollbarWidth(el.offsetWidth - el.clientWidth); | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     updateScrollbarDimensions(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const resizeObserver = new ResizeObserver(updateScrollbarDimensions); | 
					
						
							|  |  |  |     resizeObserver.observe(el); | 
					
						
							|  |  |  |     return () => { | 
					
						
							|  |  |  |       resizeObserver.disconnect(); | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   }, [ref, height]); | 
					
						
							| 
									
										
										
										
											2025-07-11 20:50:25 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return scrollbarWidth; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-08-22 03:22:30 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | const numIsEqual = (a: number, b: number) => a === b; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function useColWidths( | 
					
						
							|  |  |  |   visibleFields: Field[], | 
					
						
							|  |  |  |   availableWidth: number, | 
					
						
							|  |  |  |   frozenColumns?: number | 
					
						
							|  |  |  | ): [number[], number] { | 
					
						
							|  |  |  |   const [widths, setWidths] = useState<number[]>(computeColWidths(visibleFields, availableWidth)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // only replace the widths array if something actually changed
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     const newWidths = computeColWidths(visibleFields, availableWidth); | 
					
						
							|  |  |  |     if (!compareArrayValues(widths, newWidths, numIsEqual)) { | 
					
						
							|  |  |  |       setWidths(newWidths); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }, [availableWidth, widths, visibleFields]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // this is to avoid buggy situations where all visible columns are frozen
 | 
					
						
							|  |  |  |   const numFrozenColsFullyInView = useMemo(() => { | 
					
						
							|  |  |  |     if (!frozenColumns || frozenColumns <= 0) { | 
					
						
							|  |  |  |       return -1; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const fullyVisibleCols = widths.reduce( | 
					
						
							|  |  |  |       ([count, remainingWidth], nextWidth) => { | 
					
						
							|  |  |  |         if (remainingWidth - nextWidth >= 0) { | 
					
						
							|  |  |  |           return [count + 1, remainingWidth - nextWidth]; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return [count, 0]; | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       [0, availableWidth] | 
					
						
							|  |  |  |     )[0]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // de-noise memoized changes to the columns array, and only change this
 | 
					
						
							|  |  |  |     // number when the number of frozen columns changes or once there are fewer
 | 
					
						
							|  |  |  |     // visible columns than the number of frozen columns.
 | 
					
						
							|  |  |  |     return Math.min(fullyVisibleCols, frozenColumns); | 
					
						
							|  |  |  |   }, [widths, availableWidth, frozenColumns]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return [widths, numFrozenColsFullyInView]; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-09-06 03:46:07 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | const isReducer = (maybeReducer: string): maybeReducer is ReducerID => maybeReducer in ReducerID; | 
					
						
							|  |  |  | const nonMathReducers = new Set<ReducerID>([ | 
					
						
							|  |  |  |   ReducerID.allValues, | 
					
						
							|  |  |  |   ReducerID.changeCount, | 
					
						
							|  |  |  |   ReducerID.count, | 
					
						
							|  |  |  |   ReducerID.countAll, | 
					
						
							|  |  |  |   ReducerID.distinctCount, | 
					
						
							|  |  |  |   ReducerID.first, | 
					
						
							|  |  |  |   ReducerID.firstNotNull, | 
					
						
							|  |  |  |   ReducerID.last, | 
					
						
							|  |  |  |   ReducerID.lastNotNull, | 
					
						
							|  |  |  |   ReducerID.uniqueValues, | 
					
						
							|  |  |  | ]); | 
					
						
							|  |  |  | const isNonMathReducer = (reducer: string) => isReducer(reducer) && nonMathReducers.has(reducer); | 
					
						
							|  |  |  | const noFormattingReducers = new Set<ReducerID>([ReducerID.count, ReducerID.countAll]); | 
					
						
							|  |  |  | const shouldReducerSkipFormatting = (reducer: string) => isReducer(reducer) && noFormattingReducers.has(reducer); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const useReducerEntries = ( | 
					
						
							|  |  |  |   field: Field, | 
					
						
							|  |  |  |   rows: TableRow[], | 
					
						
							|  |  |  |   displayName: string, | 
					
						
							|  |  |  |   colIdx: number | 
					
						
							|  |  |  | ): Array<[string, string | null]> => { | 
					
						
							|  |  |  |   return useMemo(() => { | 
					
						
							|  |  |  |     const reducers: string[] = field.config.custom?.footer?.reducers ?? []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if ( | 
					
						
							|  |  |  |       reducers.length === 0 || | 
					
						
							|  |  |  |       (field.type !== FieldType.number && reducers.every((reducerId) => !isNonMathReducer(reducerId))) | 
					
						
							|  |  |  |     ) { | 
					
						
							|  |  |  |       return []; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Create a new state object that matches the original behavior exactly
 | 
					
						
							|  |  |  |     const newState: FooterFieldState = { | 
					
						
							|  |  |  |       lastProcessedRowCount: 0, | 
					
						
							|  |  |  |       ...(field.state || {}), // Preserve any existing state properties
 | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Assign back to field
 | 
					
						
							|  |  |  |     field.state = newState; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const currentRowCount = rows.length; | 
					
						
							|  |  |  |     const lastRowCount = newState.lastProcessedRowCount; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Check if we need to invalidate the cache
 | 
					
						
							|  |  |  |     if (lastRowCount !== currentRowCount) { | 
					
						
							|  |  |  |       // Cache should be invalidated as row count has changed
 | 
					
						
							|  |  |  |       if (newState.calcs) { | 
					
						
							|  |  |  |         delete newState.calcs; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       // Update the row count tracker
 | 
					
						
							|  |  |  |       newState.lastProcessedRowCount = currentRowCount; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Calculate all specified reducers
 | 
					
						
							|  |  |  |     const results: Record<string, number | null> = reduceField({ | 
					
						
							|  |  |  |       field: { | 
					
						
							|  |  |  |         ...field, | 
					
						
							|  |  |  |         values: rows.map((row) => row[displayName]), | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       reducers, | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return reducers.map((reducerId) => { | 
					
						
							|  |  |  |       if ( | 
					
						
							|  |  |  |         results[reducerId] === undefined || | 
					
						
							|  |  |  |         // For non-number fields, only show special count reducers
 | 
					
						
							|  |  |  |         (field.type !== FieldType.number && !isNonMathReducer(reducerId)) || | 
					
						
							|  |  |  |         // for countAll, only show the reducer in the first column
 | 
					
						
							|  |  |  |         (reducerId === ReducerID.countAll && colIdx !== 0) | 
					
						
							|  |  |  |       ) { | 
					
						
							|  |  |  |         return [reducerId, null]; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const value = results[reducerId]; | 
					
						
							|  |  |  |       let result = null; | 
					
						
							|  |  |  |       if (!shouldReducerSkipFormatting(reducerId) && field.display) { | 
					
						
							|  |  |  |         result = formattedValueToString(field.display(value)); | 
					
						
							|  |  |  |       } else if (value != null) { | 
					
						
							|  |  |  |         result = String(value); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return [reducerId, result]; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }, [field, rows, displayName, colIdx]); | 
					
						
							|  |  |  | }; |