mantine-datatable
Version:
The lightweight, dependency-free, dark-theme aware table component for your Mantine UI data-rich applications, featuring asynchronous data loading support, pagination, intuitive Gmail-style additive batch rows selection, column sorting, custom cell data r
1 lines • 244 kB
Source Map (JSON)
{"version":3,"sources":["../package/DataTable.tsx","../package/cssVariables.ts","../package/DataTableDragToggleProvider.tsx","../package/DataTableColumns.context.ts","../package/DataTableEmptyRow.tsx","../package/DataTableEmptyState.tsx","../package/icons/IconDatabaseOff.tsx","../package/DataTableFooter.tsx","../package/DataTableFooterCell.tsx","../package/hooks/useColumnResize.ts","../package/hooks/useDataTableColumnPinning.ts","../package/utils.ts","../package/hooks/useDataTableColumnReorder.ts","../package/hooks/useDataTableColumnResize.ts","../package/hooks/useDataTableColumns.ts","../package/hooks/useDataTableColumnToggle.ts","../package/hooks/useDataTableInjectCssVariables.ts","../package/hooks/useIsomorphicLayoutEffect.ts","../package/hooks/useStableValue.ts","../package/hooks/useDataTablePinnedColumns.ts","../package/hooks/useLastSelectionChangeIndex.ts","../package/hooks/useMediaQueries.ts","../package/hooks/useMediaQueriesStringOrFunction.ts","../package/hooks/useMediaQueryStringOrFunction.ts","../package/hooks/useRowExpansion.ts","../package/hooks/useRowExpansionStatus.ts","../package/utilityClasses.ts","../package/DataTableFooterSelectorPlaceholderCell.tsx","../package/DataTableHeader.tsx","../package/DataTableColumnGroupHeaderCell.tsx","../package/DataTableHeaderCell.tsx","../package/DataTableHeaderCellFilter.tsx","../package/icons/IconFilter.tsx","../package/icons/IconFilterFilled.tsx","../package/DataTablePinnableDropdown.tsx","../package/icons/IconArrowLeft.tsx","../package/icons/IconArrowRight.tsx","../package/icons/IconPin.tsx","../package/icons/IconX.tsx","../package/DataTableResizableHeaderHandle.tsx","../package/icons/IconArrowsVertical.tsx","../package/icons/IconArrowUp.tsx","../package/icons/IconGripVertical.tsx","../package/DataTableHeaderSelectorCell.tsx","../package/DataTableLoader.tsx","../package/DataTablePagination.tsx","../package/DataTablePageSizeSelector.tsx","../package/icons/IconSelector.tsx","../package/DataTableRow.tsx","../package/DataTableRowCell.tsx","../package/DataTableRowExpansion.tsx","../package/DataTableRowSelectorCell.tsx","../package/DataTableScrollArea.tsx","../package/DataTableDraggableRow.tsx"],"sourcesContent":["import { Box, type MantineSize, Table } from '@mantine/core';\nimport { useMergedRef } from '@mantine/hooks';\nimport clsx from 'clsx';\nimport type { RefObject } from 'react';\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';\nimport { getTableCssVariables } from './cssVariables';\nimport { DataTableColumnsProvider } from './DataTableDragToggleProvider';\nimport { DataTableEmptyRow } from './DataTableEmptyRow';\nimport { DataTableEmptyState } from './DataTableEmptyState';\nimport { DataTableFooter } from './DataTableFooter';\nimport { DataTableHeader } from './DataTableHeader';\nimport { DataTableLoader } from './DataTableLoader';\nimport { DataTablePagination } from './DataTablePagination';\nimport { DataTableRow } from './DataTableRow';\nimport { DataTableScrollArea } from './DataTableScrollArea';\nimport {\n useDataTableColumns,\n useDataTableInjectCssVariables,\n useDataTablePinnedColumns,\n useLastSelectionChangeIndex,\n useRowExpansion,\n} from './hooks';\nimport type { DataTableProps } from './types';\nimport { TEXT_SELECTION_DISABLED } from './utilityClasses';\nimport { differenceBy, flattenColumns, getRecordId, uniqBy } from './utils';\n\nexport function DataTable<T>({\n withTableBorder,\n borderRadius,\n textSelectionDisabled,\n height = '100%',\n minHeight,\n maxHeight,\n shadow,\n verticalAlign = 'center',\n fetching,\n columns,\n storeColumnsKey,\n groups,\n pinFirstColumn,\n pinLastColumn,\n defaultColumnProps,\n defaultColumnRender,\n idAccessor = 'id',\n records,\n selectionTrigger = 'checkbox',\n selectedRecords,\n onSelectedRecordsChange,\n selectionColumnClassName,\n selectionColumnStyle,\n isRecordSelectable,\n selectionCheckboxProps,\n allRecordsSelectionCheckboxProps = { 'aria-label': 'Select all records' },\n getRecordSelectionCheckboxProps = (_, index) => ({ 'aria-label': `Select record ${index + 1}` }),\n sortStatus,\n sortIcons,\n onSortStatusChange,\n horizontalSpacing,\n page,\n onPageChange,\n totalRecords,\n recordsPerPage,\n onRecordsPerPageChange,\n recordsPerPageOptions,\n recordsPerPageLabel = 'Records per page',\n paginationWithEdges,\n paginationWithControls,\n paginationActiveTextColor,\n paginationActiveBackgroundColor,\n paginationSize = 'sm',\n paginationText = ({ from, to, totalRecords }) => `${from} - ${to} / ${totalRecords}`,\n paginationWrapBreakpoint = 'sm',\n getPaginationControlProps = (control) => {\n if (control === 'previous') {\n return { 'aria-label': 'Previous page' };\n } else if (control === 'next') {\n return { 'aria-label': 'Next page' };\n }\n return {};\n },\n getPaginationItemProps,\n renderPagination,\n loaderBackgroundBlur,\n customLoader,\n loaderSize,\n loaderType,\n loaderColor,\n loadingText = '...',\n emptyState,\n noRecordsText = 'No records',\n noRecordsIcon,\n highlightOnHover,\n striped,\n noHeader,\n onRowClick,\n onRowDoubleClick,\n onRowContextMenu,\n onCellClick,\n onCellDoubleClick,\n onCellContextMenu,\n onScroll,\n onScrollToTop,\n onScrollToBottom,\n onScrollToLeft,\n onScrollToRight,\n c,\n backgroundColor,\n borderColor,\n rowBorderColor,\n stripedColor,\n highlightOnHoverColor,\n rowColor,\n rowBackgroundColor,\n rowExpansion,\n rowClassName,\n rowStyle,\n customRowAttributes,\n scrollViewportRef,\n scrollAreaProps,\n tableRef,\n bodyRef,\n m,\n my,\n mx,\n mt,\n mb,\n ml,\n mr,\n className,\n classNames,\n style,\n styles,\n rowFactory,\n tableWrapper,\n ...otherProps\n}: DataTableProps<T>) {\n const flatColumns = useMemo(() => {\n return groups ? flattenColumns(groups) : columns!;\n }, [columns, groups]);\n\n const { refs, onScroll: handleScrollPositionChange } = useDataTableInjectCssVariables({\n scrollCallbacks: {\n onScroll,\n onScrollToTop,\n onScrollToBottom,\n onScrollToLeft,\n onScrollToRight,\n },\n withRowBorders: otherProps.withRowBorders,\n });\n\n const dragToggle = useDataTableColumns({\n key: storeColumnsKey,\n columns: flatColumns,\n headerRef: refs.header as RefObject<HTMLTableSectionElement | null>,\n scrollViewportRef: refs.scrollViewport as RefObject<HTMLElement | null>,\n });\n\n // Use the columns enriched with order/visibility/width from the hook so\n // resize widths actually reach the rendered <th>/<td> cells.\n const effectiveColumns = dragToggle.effectiveColumns;\n\n const mergedTableRef = useMergedRef(refs.table, tableRef);\n const mergedViewportRef = useMergedRef(refs.scrollViewport, scrollViewportRef);\n const internalBodyRef = useRef<HTMLTableSectionElement>(null);\n const mergedBodyRef = useMergedRef(internalBodyRef, bodyRef);\n const rowExpansionInfo = useRowExpansion<T>({ rowExpansion, records, idAccessor });\n\n const { pinnedMap, hasLeftPinned, hasRightPinned } = useDataTablePinnedColumns({\n columns: effectiveColumns,\n theadRef: refs.header as RefObject<HTMLTableSectionElement | null>,\n tbodyRef: internalBodyRef,\n selectionColumnHeaderRef: refs.selectionColumnHeader as RefObject<HTMLTableCellElement | null>,\n selectionVisible: !!selectedRecords,\n pinFirstColumn,\n pinLastColumn,\n });\n\n // Track when we should reset scroll due to pagination, but defer until data is rendered\n const resetScrollPending = useRef(false);\n const prevPageRef = useRef(page);\n const recordsAtPageChangeRef = useRef<typeof records | undefined>(records);\n\n const handlePageChange = useCallback(\n (newPage: number) => {\n resetScrollPending.current = true;\n recordsAtPageChangeRef.current = records;\n onPageChange!(newPage);\n },\n [onPageChange, records]\n );\n\n // Handle externally-driven page changes\n useEffect(() => {\n if (prevPageRef.current !== page) {\n resetScrollPending.current = true;\n recordsAtPageChangeRef.current = records;\n prevPageRef.current = page;\n }\n }, [page, records]);\n\n const recordsLength = records?.length;\n\n // Reset scroll position when changing pages (sync) or when records change (async)\n useLayoutEffect(() => {\n if (!resetScrollPending.current) return;\n if (fetching) return;\n if (records === recordsAtPageChangeRef.current) return;\n\n const viewport = refs.scrollViewport.current;\n if (!viewport) return;\n\n const raf = requestAnimationFrame(() => {\n viewport.scrollTo({ top: 0, left: 0 });\n resetScrollPending.current = false;\n });\n\n return () => cancelAnimationFrame(raf);\n }, [fetching, records, refs.scrollViewport]);\n\n const recordIds = records?.map((record) => getRecordId(record, idAccessor));\n const selectionColumnVisible = !!selectedRecords;\n const selectedRecordIds = selectedRecords?.map((record) => getRecordId(record, idAccessor));\n const hasRecordsAndSelectedRecords =\n recordIds !== undefined && selectedRecordIds !== undefined && selectedRecordIds.length > 0;\n\n const selectableRecords = isRecordSelectable ? records?.filter(isRecordSelectable) : records;\n const selectableRecordIds = selectableRecords?.map((record) => getRecordId(record, idAccessor));\n\n const allSelectableRecordsSelected =\n hasRecordsAndSelectedRecords && selectableRecordIds!.every((id) => selectedRecordIds.includes(id));\n const someRecordsSelected =\n hasRecordsAndSelectedRecords && selectableRecordIds!.some((id) => selectedRecordIds.includes(id));\n\n const handleHeaderSelectionChange = useCallback(() => {\n if (selectedRecords && onSelectedRecordsChange) {\n onSelectedRecordsChange(\n allSelectableRecordsSelected\n ? selectedRecords.filter((record) => !selectableRecordIds!.includes(getRecordId(record, idAccessor)))\n : uniqBy([...selectedRecords, ...selectableRecords!], (record) => getRecordId(record, idAccessor))\n );\n }\n }, [\n allSelectableRecordsSelected,\n idAccessor,\n onSelectedRecordsChange,\n selectableRecordIds,\n selectableRecords,\n selectedRecords,\n ]);\n\n const { lastSelectionChangeIndex, setLastSelectionChangeIndex } = useLastSelectionChangeIndex(recordIds);\n const selectorCellShadowVisible = selectionColumnVisible && !hasLeftPinned;\n\n const marginProperties = { m, my, mx, mt, mb, ml, mr };\n\n const TableWrapper = useCallback(\n ({ children }: { children: React.ReactNode }) => {\n if (tableWrapper) return tableWrapper({ children });\n return children;\n },\n [tableWrapper]\n );\n\n return (\n <DataTableColumnsProvider {...dragToggle} pinnedMap={pinnedMap}>\n <Box\n ref={refs.root}\n {...marginProperties}\n className={clsx(\n 'mantine-datatable',\n { 'mantine-datatable-with-border': withTableBorder },\n className,\n classNames?.root\n )}\n style={[\n (theme) => ({\n ...getTableCssVariables({\n theme,\n c,\n backgroundColor,\n borderColor,\n rowBorderColor,\n stripedColor,\n highlightOnHoverColor,\n }),\n borderRadius: theme.radius[borderRadius as MantineSize] || borderRadius,\n boxShadow: theme.shadows[shadow as MantineSize] || shadow,\n height,\n minHeight,\n maxHeight,\n }),\n style,\n styles?.root,\n {\n position: 'relative',\n },\n ]}\n >\n <DataTableScrollArea\n viewportRef={mergedViewportRef}\n leftShadowBehind={selectionColumnVisible || hasLeftPinned}\n rightShadowBehind={hasRightPinned}\n onScrollPositionChange={handleScrollPositionChange}\n scrollAreaProps={scrollAreaProps}\n >\n <TableWrapper>\n <Table\n ref={mergedTableRef}\n horizontalSpacing={horizontalSpacing}\n className={clsx(\n 'mantine-datatable-table',\n {\n [TEXT_SELECTION_DISABLED]: textSelectionDisabled,\n 'mantine-datatable-vertical-align-top': verticalAlign === 'top',\n 'mantine-datatable-vertical-align-bottom': verticalAlign === 'bottom',\n 'mantine-datatable-selection-column-visible': selectionColumnVisible,\n 'mantine-datatable-resizable-columns': dragToggle.hasResizableColumns,\n 'mantine-datatable-resize-locked': dragToggle.isLocked,\n 'mantine-datatable-resizing': dragToggle.isResizing,\n },\n classNames?.table\n )}\n style={{\n ...styles?.table,\n ...(dragToggle.isLocked ? { tableLayout: 'fixed' } : null),\n ...(dragToggle.tableWidth != null ? { width: `${dragToggle.tableWidth}px` } : null),\n }}\n data-striped={(recordsLength && striped) || undefined}\n data-highlight-on-hover={highlightOnHover || undefined}\n {...otherProps}\n >\n {noHeader ? null : (\n <DataTableColumnsProvider {...dragToggle} pinnedMap={pinnedMap}>\n <DataTableHeader<T>\n ref={refs.header}\n selectionColumnHeaderRef={refs.selectionColumnHeader}\n className={classNames?.header}\n style={styles?.header}\n columns={effectiveColumns}\n defaultColumnProps={defaultColumnProps}\n groups={groups}\n pinnedMap={pinnedMap}\n sortStatus={sortStatus}\n sortIcons={sortIcons}\n onSortStatusChange={onSortStatusChange}\n selectionTrigger={selectionTrigger}\n selectionVisible={selectionColumnVisible}\n selectionChecked={allSelectableRecordsSelected}\n selectionIndeterminate={someRecordsSelected && !allSelectableRecordsSelected}\n onSelectionChange={handleHeaderSelectionChange}\n selectionCheckboxProps={{ ...selectionCheckboxProps, ...allRecordsSelectionCheckboxProps }}\n selectorCellShadowVisible={selectorCellShadowVisible}\n selectionColumnClassName={selectionColumnClassName}\n selectionColumnStyle={selectionColumnStyle}\n withColumnBorders={otherProps.withColumnBorders}\n />\n </DataTableColumnsProvider>\n )}\n <tbody ref={mergedBodyRef}>\n {recordsLength ? (\n records.map((record, index) => {\n const recordId = getRecordId(record, idAccessor);\n const isSelected = selectedRecordIds?.includes(recordId) || false;\n\n let handleSelectionChange: React.MouseEventHandler | undefined;\n\n if (onSelectedRecordsChange && selectedRecords) {\n handleSelectionChange = (e) => {\n if (e.nativeEvent.shiftKey && lastSelectionChangeIndex !== null) {\n const targetRecords = records.filter(\n index > lastSelectionChangeIndex\n ? (rec, idx) =>\n idx >= lastSelectionChangeIndex &&\n idx <= index &&\n (isRecordSelectable ? isRecordSelectable(rec, idx) : true)\n : (rec, idx) =>\n idx >= index &&\n idx <= lastSelectionChangeIndex &&\n (isRecordSelectable ? isRecordSelectable(rec, idx) : true)\n );\n onSelectedRecordsChange(\n isSelected\n ? differenceBy(selectedRecords, targetRecords, (r) => getRecordId(r, idAccessor))\n : uniqBy([...selectedRecords, ...targetRecords], (r) => getRecordId(r, idAccessor))\n );\n } else {\n onSelectedRecordsChange(\n isSelected\n ? selectedRecords.filter((rec) => getRecordId(rec, idAccessor) !== recordId)\n : uniqBy([...selectedRecords, record], (rec) => getRecordId(rec, idAccessor))\n );\n }\n setLastSelectionChangeIndex(index);\n };\n }\n\n return (\n <DataTableRow<T>\n key={recordId as React.Key}\n record={record}\n index={index}\n columns={effectiveColumns}\n defaultColumnProps={defaultColumnProps}\n pinnedMap={pinnedMap}\n defaultColumnRender={defaultColumnRender}\n selectionTrigger={selectionTrigger}\n selectionVisible={selectionColumnVisible}\n selectionChecked={isSelected}\n onSelectionChange={handleSelectionChange}\n isRecordSelectable={isRecordSelectable}\n selectionCheckboxProps={selectionCheckboxProps}\n getSelectionCheckboxProps={getRecordSelectionCheckboxProps}\n onClick={onRowClick}\n onDoubleClick={onRowDoubleClick}\n onCellClick={onCellClick}\n onCellDoubleClick={onCellDoubleClick}\n onContextMenu={onRowContextMenu}\n onCellContextMenu={onCellContextMenu}\n expansion={rowExpansionInfo}\n color={rowColor}\n backgroundColor={rowBackgroundColor}\n className={rowClassName}\n style={rowStyle}\n customAttributes={customRowAttributes}\n selectorCellShadowVisible={selectorCellShadowVisible}\n selectionColumnClassName={selectionColumnClassName}\n selectionColumnStyle={selectionColumnStyle}\n idAccessor={idAccessor as string}\n rowFactory={rowFactory}\n />\n );\n })\n ) : (\n <DataTableEmptyRow />\n )}\n </tbody>\n\n {effectiveColumns.some(({ footer }) => footer) && (\n <DataTableFooter<T>\n ref={refs.footer}\n className={classNames?.footer}\n style={styles?.footer}\n columns={effectiveColumns}\n defaultColumnProps={defaultColumnProps}\n pinnedMap={pinnedMap}\n selectionVisible={selectionColumnVisible}\n selectorCellShadowVisible={selectorCellShadowVisible}\n />\n )}\n </Table>\n </TableWrapper>\n </DataTableScrollArea>\n {!!(page && recordsLength) && (\n <DataTablePagination\n className={classNames?.pagination}\n style={styles?.pagination}\n horizontalSpacing={horizontalSpacing}\n fetching={fetching}\n page={page}\n onPageChange={handlePageChange}\n totalRecords={totalRecords}\n recordsPerPage={recordsPerPage}\n onRecordsPerPageChange={onRecordsPerPageChange}\n recordsPerPageOptions={recordsPerPageOptions}\n recordsPerPageLabel={recordsPerPageLabel}\n paginationWithEdges={paginationWithEdges}\n paginationWithControls={paginationWithControls}\n paginationActiveTextColor={paginationActiveTextColor}\n paginationActiveBackgroundColor={paginationActiveBackgroundColor}\n paginationSize={paginationSize}\n paginationText={paginationText}\n paginationWrapBreakpoint={paginationWrapBreakpoint}\n getPaginationControlProps={getPaginationControlProps}\n getPaginationItemProps={getPaginationItemProps}\n noRecordsText={noRecordsText}\n loadingText={loadingText}\n recordsLength={recordsLength}\n renderPagination={renderPagination}\n />\n )}\n <DataTableLoader\n fetching={fetching}\n backgroundBlur={loaderBackgroundBlur}\n customContent={customLoader}\n size={loaderSize}\n type={loaderType}\n color={loaderColor}\n />\n <DataTableEmptyState icon={noRecordsIcon} text={noRecordsText} active={!fetching && !recordsLength}>\n {emptyState}\n </DataTableEmptyState>\n </Box>\n </DataTableColumnsProvider>\n );\n}\n","import { type MantineColor, type MantineTheme, parseThemeColor } from '@mantine/core';\nimport type { DataTableProps } from './types';\n\nexport function getColor(\n color: MantineColor | undefined | { light: MantineColor; dark: MantineColor },\n theme: MantineTheme,\n colorScheme: 'light' | 'dark'\n) {\n return color\n ? parseThemeColor({\n color: typeof color === 'object' ? color[colorScheme] : color,\n theme,\n }).value\n : undefined;\n}\n\nexport function getTableCssVariables({\n theme,\n c,\n backgroundColor,\n borderColor,\n rowBorderColor,\n stripedColor,\n highlightOnHoverColor,\n}: Pick<\n DataTableProps,\n 'c' | 'backgroundColor' | 'borderColor' | 'rowBorderColor' | 'stripedColor' | 'highlightOnHoverColor'\n> & {\n theme: MantineTheme;\n}) {\n return {\n '--mantine-datatable-color-light': getColor(c, theme, 'light'),\n '--mantine-datatable-color-dark': getColor(c, theme, 'dark'),\n\n '--mantine-datatable-background-color-light': getColor(backgroundColor, theme, 'light'),\n '--mantine-datatable-background-color-dark': getColor(backgroundColor, theme, 'dark'),\n\n '--mantine-datatable-border-color-light': getColor(borderColor, theme, 'light'),\n '--mantine-datatable-border-color-dark': getColor(borderColor, theme, 'dark'),\n\n '--mantine-datatable-row-border-color-light': getColor(rowBorderColor, theme, 'light'),\n '--mantine-datatable-row-border-color-dark': getColor(rowBorderColor, theme, 'dark'),\n\n '--mantine-datatable-striped-color-light': getColor(stripedColor, theme, 'light'),\n '--mantine-datatable-striped-color-dark': getColor(stripedColor, theme, 'dark'),\n\n '--mantine-datatable-highlight-on-hover-color-light': getColor(highlightOnHoverColor, theme, 'light'),\n '--mantine-datatable-highlight-on-hover-color-dark': getColor(highlightOnHoverColor, theme, 'dark'),\n };\n}\n\nexport function getPaginationCssVariables({\n theme,\n paginationActiveTextColor,\n paginationActiveBackgroundColor,\n}: Pick<DataTableProps, 'paginationActiveTextColor' | 'paginationActiveBackgroundColor'> & {\n theme: MantineTheme;\n}) {\n return {\n '--mantine-datatable-pagination-active-text-color-light': getColor(paginationActiveTextColor, theme, 'light'),\n '--mantine-datatable-pagination-active-text-color-dark': getColor(paginationActiveTextColor, theme, 'dark'),\n\n '--mantine-datatable-pagination-active-background-color-light': getColor(\n paginationActiveBackgroundColor,\n theme,\n 'light'\n ),\n '--mantine-datatable-pagination-active-background-color-dark': getColor(\n paginationActiveBackgroundColor,\n theme,\n 'dark'\n ),\n };\n}\n\nexport function getRowCssVariables({\n theme,\n color,\n backgroundColor,\n}: {\n theme: MantineTheme;\n color: MantineColor | undefined | { light: MantineColor; dark: MantineColor };\n backgroundColor: MantineColor | undefined | { light: MantineColor; dark: MantineColor };\n}) {\n return {\n '--mantine-datatable-row-color-light': getColor(color, theme, 'light'),\n '--mantine-datatable-row-color-dark': getColor(color, theme, 'dark'),\n '--mantine-datatable-row-background-color-light': getColor(backgroundColor, theme, 'light'),\n '--mantine-datatable-row-background-color-dark': getColor(backgroundColor, theme, 'dark'),\n };\n}\n","'use client';\n\nimport { type Dispatch, type PropsWithChildren, type SetStateAction, useState } from 'react';\nimport { DataTableColumnsContextProvider } from './DataTableColumns.context';\nimport type { DataTableColumnPinning, DataTableColumnToggle, PinnedColumnInfo } from './hooks';\n\ntype DataTableColumnsProviderProps = PropsWithChildren<{\n columnsOrder: string[];\n setColumnsOrder: (order: string[]) => void;\n resetColumnsOrder: () => void;\n\n columnsToggle: DataTableColumnToggle[];\n setColumnsToggle: (toggle: DataTableColumnToggle[]) => void;\n resetColumnsToggle: () => void;\n\n columnsPinning: DataTableColumnPinning[];\n setColumnsPinning: Dispatch<SetStateAction<DataTableColumnPinning[]>>;\n resetColumnsPinning: () => void;\n\n setColumnWidth: (accessor: string, width: string | number) => void;\n setMultipleColumnWidths: (updates: Array<{ accessor: string; width: string | number }>) => void;\n resetColumnsWidth: () => void;\n\n beginResize: () => void;\n endResize: () => void;\n\n pinnedMap: Map<string, PinnedColumnInfo>;\n}>;\n\nexport const DataTableColumnsProvider = (props: DataTableColumnsProviderProps) => {\n const {\n children,\n columnsOrder,\n setColumnsOrder,\n columnsToggle,\n setColumnsToggle,\n columnsPinning,\n setColumnsPinning,\n\n resetColumnsOrder,\n resetColumnsToggle,\n resetColumnsPinning,\n\n setColumnWidth,\n setMultipleColumnWidths,\n resetColumnsWidth,\n\n beginResize,\n endResize,\n\n pinnedMap,\n } = props;\n\n const [sourceColumn, setSourceColumn] = useState('');\n const [targetColumn, setTargetColumn] = useState('');\n\n const swapColumns = () => {\n if (!columnsOrder || !setColumnsOrder || !sourceColumn || !targetColumn) {\n return;\n }\n\n const sourceInfo = pinnedMap.get(sourceColumn);\n const targetInfo = pinnedMap.get(targetColumn);\n if (sourceInfo?.logicalSide !== targetInfo?.logicalSide) return;\n\n const sourceIndex = columnsOrder.indexOf(sourceColumn);\n const targetIndex = columnsOrder.indexOf(targetColumn);\n\n if (sourceIndex !== -1 && targetIndex !== -1) {\n const newOrder = [...columnsOrder];\n const [removed] = newOrder.splice(sourceIndex, 1);\n newOrder.splice(targetIndex, 0, removed);\n setColumnsOrder(newOrder);\n }\n };\n\n return (\n <DataTableColumnsContextProvider\n value={{\n sourceColumn,\n setSourceColumn,\n targetColumn,\n setTargetColumn,\n columnsToggle,\n setColumnsToggle,\n columnsPinning,\n setColumnsPinning,\n swapColumns,\n resetColumnsOrder,\n resetColumnsToggle,\n resetColumnsPinning,\n\n setColumnWidth,\n setMultipleColumnWidths,\n resetColumnsWidth,\n\n beginResize,\n endResize,\n }}\n >\n {children}\n </DataTableColumnsContextProvider>\n );\n};\n","import { createSafeContext } from '@mantine/core';\nimport type { Dispatch, SetStateAction } from 'react';\nimport type { DataTableColumnPinning, DataTableColumnToggle } from './hooks';\n\ninterface DataTableColumnsContext {\n // accessor of the column which is currently dragged\n sourceColumn: string;\n setSourceColumn: Dispatch<SetStateAction<string>>;\n\n // accessor of the column which is currently hovered\n targetColumn: string;\n setTargetColumn: Dispatch<SetStateAction<string>>;\n\n // swap the source column with the target column\n swapColumns: () => void;\n\n // reset to the default columns order\n resetColumnsOrder: () => void;\n\n columnsToggle: DataTableColumnToggle[];\n setColumnsToggle: (toggle: DataTableColumnToggle[]) => void;\n resetColumnsToggle: () => void;\n\n columnsPinning: DataTableColumnPinning[];\n setColumnsPinning: Dispatch<SetStateAction<DataTableColumnPinning[]>>;\n resetColumnsPinning: () => void;\n\n setColumnWidth: (accessor: string, width: string | number) => void;\n setMultipleColumnWidths: (updates: Array<{ accessor: string; width: string | number }>) => void;\n resetColumnsWidth: () => void;\n\n // Drag lifecycle: snapshot DOM widths, lock the table, then persist on release\n beginResize: () => void;\n endResize: () => void;\n}\n\nexport const [DataTableColumnsContextProvider, useDataTableColumnsContext] = createSafeContext<DataTableColumnsContext>(\n 'useDataTableColumnsContext must be used within DataTableColumnProvider'\n);\n","export function DataTableEmptyRow() {\n return (\n <tr className=\"mantine-datatable-empty-row\">\n <td />\n </tr>\n );\n}\n","import { Center, Text } from '@mantine/core';\nimport { IconDatabaseOff } from './icons/IconDatabaseOff';\n\ntype DataTableEmptyStateProps = React.PropsWithChildren<{\n icon: React.ReactNode | undefined;\n text: string;\n active: boolean;\n}>;\n\nexport function DataTableEmptyState({ icon, text, active, children }: DataTableEmptyStateProps) {\n return (\n <Center className=\"mantine-datatable-empty-state\" data-active={active || undefined}>\n {children || (\n <>\n {icon || (\n <div className=\"mantine-datatable-empty-state-icon\">\n <IconDatabaseOff />\n </div>\n )}\n <Text component=\"div\" size=\"sm\" c=\"dimmed\">\n {text}\n </Text>\n </>\n )}\n </Center>\n );\n}\n","export function IconDatabaseOff() {\n return (\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n strokeWidth=\"2\"\n stroke=\"currentColor\"\n fill=\"none\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n <path d=\"M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74\" />\n <path d=\"M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6\" />\n <path d=\"M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4\" />\n <path d=\"M3 3l18 18\" />\n </svg>\n );\n}\n","import { type MantineStyleProp, TableTfoot, TableTr } from '@mantine/core';\nimport clsx from 'clsx';\nimport { DataTableFooterCell } from './DataTableFooterCell';\nimport { DataTableFooterSelectorPlaceholderCell } from './DataTableFooterSelectorPlaceholderCell';\nimport type { PinnedColumnInfo } from './hooks';\nimport type { DataTableColumn, DataTableDefaultColumnProps } from './types';\n\ntype DataTableFooterProps<T> = {\n className: string | undefined;\n style: MantineStyleProp | undefined;\n columns: DataTableColumn<T>[];\n defaultColumnProps: DataTableDefaultColumnProps<T> | undefined;\n pinnedMap: Map<string, PinnedColumnInfo>;\n selectionVisible: boolean;\n selectorCellShadowVisible: boolean;\n ref: React.Ref<HTMLTableSectionElement>;\n};\n\nexport function DataTableFooter<T>({\n className,\n style,\n columns,\n defaultColumnProps,\n pinnedMap,\n selectionVisible,\n selectorCellShadowVisible,\n ref,\n}: DataTableFooterProps<T>) {\n return (\n <TableTfoot ref={ref} className={clsx('mantine-datatable-footer', className)} style={style}>\n <TableTr>\n {selectionVisible && <DataTableFooterSelectorPlaceholderCell shadowVisible={selectorCellShadowVisible} />}\n {columns.map(({ hidden, ...columnProps }) => {\n if (hidden) return null;\n\n const {\n accessor,\n visibleMediaQuery,\n textAlign,\n width,\n footer,\n footerClassName,\n footerStyle,\n noWrap,\n ellipsis,\n } = { ...defaultColumnProps, ...columnProps };\n\n return (\n <DataTableFooterCell<T>\n key={accessor as React.Key}\n pinnedInfo={pinnedMap.get(String(accessor))}\n className={footerClassName}\n style={footerStyle}\n visibleMediaQuery={visibleMediaQuery}\n textAlign={textAlign}\n width={width}\n title={footer}\n noWrap={noWrap}\n ellipsis={ellipsis}\n />\n );\n })}\n </TableTr>\n </TableTfoot>\n );\n}\n","import { type MantineStyleProp, type MantineTheme, TableTh } from '@mantine/core';\nimport clsx from 'clsx';\nimport type { PinnedColumnInfo } from './hooks';\nimport { useMediaQueryStringOrFunction } from './hooks';\nimport type { DataTableColumn } from './types';\nimport { ELLIPSIS, NOWRAP, TEXT_ALIGN_CENTER, TEXT_ALIGN_LEFT, TEXT_ALIGN_RIGHT } from './utilityClasses';\n\ntype DataTableFooterCellProps<T> = {\n className: string | undefined;\n style: MantineStyleProp | undefined;\n pinnedInfo: PinnedColumnInfo | undefined;\n visibleMediaQuery: string | ((theme: MantineTheme) => string) | undefined;\n title: React.ReactNode | undefined;\n} & Pick<DataTableColumn<T>, 'noWrap' | 'ellipsis' | 'textAlign' | 'width'>;\n\nexport function DataTableFooterCell<T>({\n className,\n style,\n pinnedInfo,\n visibleMediaQuery,\n title,\n noWrap,\n ellipsis,\n textAlign,\n width,\n}: DataTableFooterCellProps<T>) {\n if (!useMediaQueryStringOrFunction(visibleMediaQuery)) return null;\n return (\n <TableTh\n data-pinned={pinnedInfo?.position}\n data-pinned-shadow={pinnedInfo?.isBoundary ? pinnedInfo.position : undefined}\n className={clsx(\n {\n [NOWRAP]: noWrap || ellipsis,\n [ELLIPSIS]: ellipsis,\n [TEXT_ALIGN_LEFT]: textAlign === 'left',\n [TEXT_ALIGN_CENTER]: textAlign === 'center',\n [TEXT_ALIGN_RIGHT]: textAlign === 'right',\n },\n className\n )}\n style={[\n {\n width,\n minWidth: width,\n maxWidth: width,\n },\n style,\n pinnedInfo && {\n position: 'sticky',\n [pinnedInfo.position]: pinnedInfo.offset,\n overflow: 'visible',\n },\n ]}\n >\n {title}\n </TableTh>\n );\n}\n","import { useCallback, useRef, useState } from 'react';\n\ninterface UseColumnResizeProps {\n onColumnResize: (updates: Array<{ accessor: string; width: string | number }>) => void;\n minColumnWidth?: number;\n}\n\ninterface ResizeState {\n isResizing: boolean;\n startX: number;\n originalWidths: { current: number; next: number };\n currentAccessor: string;\n nextAccessor: string | null;\n}\n\nexport const useColumnResize = ({ onColumnResize, minColumnWidth = 50 }: UseColumnResizeProps) => {\n const [resizeState, setResizeState] = useState<ResizeState | null>(null);\n const currentColumnRef = useRef<HTMLTableCellElement | null>(null);\n const nextColumnRef = useRef<HTMLTableCellElement | null>(null);\n\n const startResize = useCallback(\n (event: React.MouseEvent, currentColumn: HTMLTableCellElement, currentAccessor: string) => {\n event.preventDefault();\n event.stopPropagation();\n\n const nextColumn = currentColumn.nextElementSibling as HTMLTableCellElement | null;\n if (!nextColumn) {\n return false;\n }\n\n const nextAccessor = nextColumn.getAttribute('data-accessor');\n if (!nextAccessor) {\n return false;\n }\n\n const currentWidth = currentColumn.getBoundingClientRect().width;\n const nextWidth = nextColumn.getBoundingClientRect().width;\n\n currentColumnRef.current = currentColumn;\n nextColumnRef.current = nextColumn;\n\n setResizeState({\n isResizing: true,\n startX: event.clientX,\n originalWidths: { current: currentWidth, next: nextWidth },\n currentAccessor,\n nextAccessor,\n });\n\n // Global styles for better UX\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n\n return true;\n },\n []\n );\n\n const updateResize = useCallback(\n (clientX: number) => {\n if (!resizeState || !currentColumnRef.current || !nextColumnRef.current) return;\n\n const deltaX = clientX - resizeState.startX;\n\n // Calculate the actual delta we can apply based on constraints\n const actualDelta = Math.min(\n deltaX,\n resizeState.originalWidths.next - minColumnWidth // Don't shrink next below minimum\n );\n\n const finalCurrentWidth = resizeState.originalWidths.current + actualDelta;\n const finalNextWidth = resizeState.originalWidths.next - actualDelta;\n\n // Apply to DOM for immediate visual feedback\n currentColumnRef.current.style.width = `${finalCurrentWidth}px`;\n nextColumnRef.current.style.width = `${finalNextWidth}px`;\n\n return { finalCurrentWidth, finalNextWidth };\n },\n [resizeState, minColumnWidth]\n );\n\n const endResize = useCallback(() => {\n if (!resizeState || !currentColumnRef.current || !nextColumnRef.current) return;\n\n // Reset global styles\n document.body.style.cursor = 'initial';\n document.body.style.userSelect = 'initial';\n\n // Get final widths\n const currentWidth = currentColumnRef.current.getBoundingClientRect().width;\n const nextWidth = nextColumnRef.current.getBoundingClientRect().width;\n\n // Update through callback\n const updates = [{ accessor: resizeState.currentAccessor, width: `${currentWidth}px` }];\n\n if (resizeState.nextAccessor) {\n updates.push({ accessor: resizeState.nextAccessor, width: `${nextWidth}px` });\n }\n\n onColumnResize(updates);\n\n // Clean up\n setResizeState(null);\n currentColumnRef.current = null;\n nextColumnRef.current = null;\n }, [resizeState, onColumnResize]);\n\n const cancelResize = useCallback(() => {\n if (!resizeState) return;\n\n // Reset global styles\n document.body.style.cursor = 'initial';\n document.body.style.userSelect = 'initial';\n\n // Reset column widths to original values\n if (currentColumnRef.current) {\n currentColumnRef.current.style.width = `${resizeState.originalWidths.current}px`;\n }\n if (nextColumnRef.current) {\n nextColumnRef.current.style.width = `${resizeState.originalWidths.next}px`;\n }\n\n // Clean up\n setResizeState(null);\n currentColumnRef.current = null;\n nextColumnRef.current = null;\n }, [resizeState]);\n\n const resetColumnWidths = useCallback(\n (currentAccessor: string, nextAccessor?: string) => {\n const updates = [{ accessor: currentAccessor, width: 'initial' }];\n if (nextAccessor) {\n updates.push({ accessor: nextAccessor, width: 'initial' });\n }\n onColumnResize(updates);\n },\n [onColumnResize]\n );\n\n return {\n isResizing: !!resizeState,\n startResize,\n updateResize,\n endResize,\n cancelResize,\n resetColumnWidths,\n };\n};\n","import { useLocalStorage } from '@mantine/hooks';\nimport { useEffect, useMemo } from 'react';\nimport type { DataTableColumn } from '../types/DataTableColumn';\nimport { hasStringAccessor, sanitizeStoredArray } from '../utils';\n\nexport type DataTableColumnPinning = {\n accessor: string;\n defaultPinned: 'left' | 'right' | undefined;\n pinnable: boolean;\n pinned: 'left' | 'right' | undefined;\n};\n\n/**\n * Hook to handle column pinning with localStorage persistence.\n * @see https://icflorescu.github.io/mantine-datatable/examples/interactive-column-pinning/\n */\nexport function useDataTableColumnPinning<T>({\n key,\n columns = [],\n getInitialValueInEffect = true,\n}: {\n /**\n * The key to use in localStorage to store the columns pinning state.\n */\n key: string | undefined;\n /**\n * Columns definitions.\n */\n columns: DataTableColumn<T>[];\n /**\n * If set to true, value will be updated in useEffect after mount.\n * @default true\n */\n getInitialValueInEffect?: boolean;\n}) {\n // Default columns pinning state\n const defaultColumnsPinning = useMemo(\n () =>\n columns.map((column) => ({\n accessor: column.accessor as string,\n defaultPinned: column.pinned,\n pinnable: !!column.pinnable,\n pinned: column.pinned,\n })),\n [columns]\n );\n\n const [storedColumnsPinning, _setColumnsPinning] = useLocalStorage<DataTableColumnPinning[]>({\n key: key ? `${key}-columns-pinning` : '',\n defaultValue: key ? defaultColumnsPinning : undefined,\n getInitialValueInEffect,\n });\n\n // Guard against malformed persisted state (e.g. a non-array value propagated\n // across tabs via the `storage` event) before any `.map()` / `.forEach()` call.\n const columnsPinning = sanitizeStoredArray<DataTableColumnPinning>(\n storedColumnsPinning,\n defaultColumnsPinning,\n hasStringAccessor\n );\n\n function setColumnsPinning(\n pinning: DataTableColumnPinning[] | ((prev: DataTableColumnPinning[]) => DataTableColumnPinning[])\n ) {\n if (!key) return;\n if (typeof pinning === 'function') {\n _setColumnsPinning((prev) => pinning(prev ?? defaultColumnsPinning));\n } else {\n _setColumnsPinning(pinning);\n }\n }\n\n // Align stored pinning state with current column definitions: drop accessors\n // that no longer exist, append accessors added since last persisted state.\n const alignedColumnsPinning = useMemo(() => {\n if (!columnsPinning) return defaultColumnsPinning;\n const aligned: DataTableColumnPinning[] = [];\n columnsPinning.forEach((col) => {\n if (columns.find((c) => c.accessor === col.accessor)) {\n aligned.push(col);\n }\n });\n columns.forEach((col) => {\n if (!aligned.find((c) => c.accessor === col.accessor)) {\n aligned.push({\n accessor: col.accessor as string,\n defaultPinned: col.pinned,\n pinnable: !!col.pinnable,\n pinned: col.pinned,\n });\n }\n });\n return aligned;\n }, [columnsPinning, columns, defaultColumnsPinning]);\n\n // Persist alignment in an effect — never set state during render\n useEffect(() => {\n if (!key) return;\n if (!columnsPinning) return;\n if (JSON.stringify(alignedColumnsPinning) !== JSON.stringify(columnsPinning)) {\n _setColumnsPinning(alignedColumnsPinning);\n }\n }, [key, alignedColumnsPinning, columnsPinning, _setColumnsPinning]);\n\n const resetColumnsPinning = () => {\n setColumnsPinning(defaultColumnsPinning);\n };\n\n if (!key) {\n return {\n columnsPinning: defaultColumnsPinning,\n setColumnsPinning,\n resetColumnsPinning,\n } as const;\n }\n\n return {\n columnsPinning: alignedColumnsPinning,\n setColumnsPinning,\n resetColumnsPinning,\n } as const;\n}\n","import type { DropResult } from '@hello-pangea/dnd';\nimport type { DataTableColumn, DataTableColumnGroup } from './types';\n\n/**\n * Utility function that returns a humanized version of a string, e.g. \"camelCase\" -> \"Camel Case\"\n */\nexport function humanize(value: string) {\n const str = value\n .replace(/([a-z\\d])([A-Z]+)/g, '$1 $2')\n .replace(/\\W|_/g, ' ')\n .trim()\n .toLowerCase();\n return `${str.charAt(0).toUpperCase()}${str.slice(1)}`;\n}\n\n/**\n * Sanitizes a value read from `localStorage` before the column hooks call array\n * methods on it.\n *\n * Persisted column state is synchronized across browser tabs through the native\n * `storage` event (e.g. when a tab is duplicated). A malformed or unexpected\n * value — written by another tab, by a different DataTable instance sharing the\n * same `storeColumnsKey`, or by manual tampering — is delivered verbatim to\n * every listening table. Without this guard such a value flows straight into\n * `.map()` / `.forEach()` and crashes the app with\n * `TypeError: <x>.map is not a function`.\n *\n * Returns `value` when it is an array whose items all pass the optional\n * `isValidItem` predicate; otherwise returns `fallback`.\n */\nexport function sanitizeStoredArray<T>(value: unknown, fallback: T[], isValidItem?: (item: unknown) => boolean): T[] {\n if (!Array.isArray(value)) return fallback;\n if (isValidItem && !value.every(isValidItem)) return fallback;\n return value as T[];\n}\n\n/**\n * Predicate matching the `{ accessor: string, ... }` shape persisted for column\n * toggle and pinning state.\n */\nexport function hasStringAccessor(item: unknown): boolean {\n return typeof item === 'object' && item !== null && typeof (item as { accessor?: unknown }).accessor === 'string';\n}\n\n/**\n * Utility function that returns an array of values that are present in the first array but not in the second\n */\nexport function differenceBy<T>(arr1: T[], arr2: T[], iteratee: (value: T) => unknown) {\n return arr1.filter((c) => !arr2.map(iteratee).includes(iteratee(c)));\n}\n\n/**\n * Utility function that returns an array of unique values from a given array\n */\nexport function uniqBy<T>(arr: T[], iteratee: (value: T) => unknown) {\n return arr.filter((x, i, self) => i === self.findIndex((y) => iteratee(x) === iteratee(y)));\n}\n\n/**\n * Utility function that returns the value at a given path in an object\n */\nexport function getValueAtPath<T>(obj: T, path: keyof T | (string & NonNullable<unknown>)) {\n if (!path) return undefined;\n const pathArray = (path as string).match(/([^[.\\]])+/g) as string[];\n return pathArray.reduce((prevObj: unknown, key) => prevObj && (prevObj as Record<string, unknown>)[key], obj);\n}\n\n/**\n * Utility function that returns the record id using idAccessor\n */\nexport function getRecordId<T>(\n record: T,\n idAccessor: keyof T | (string & NonNullable<unknown>) | ((record: T) => React.Key)\n) {\n return typeof idAccessor === 'string'\n ? getValueAtPath(record, idAccessor)\n : (idAccessor as (record: T) => React.Key)(record);\n}\n\n/**\n * Utility function that reorders an array of records by a given field used for drag'n'drop functionality.\n * @see https://github.com/hello-pangea/dnd\n */\nexport function reorderRecords<T>(dropResult: DropResult, records: T[]): T[] {\n const draft = structuredClone(records);\n const prev = draft[dropResult.source.index];\n\n if (dropResult.destination) {\n draft.splice(dropResult.source.index, 1);\n draft.splice(dropResult.destination.index, 0, prev);\n }\n\n return draft;\n}\n\n/**\n * Utility function that swaps elements of an array, by a given result from drag'n'drop functionality.\n * @see https://github.com/hello-pangea/dnd\n */\nexport function swapRecords<T>(dropResult: DropResult, records: T[]): T[] {\n const draft = structuredClone(records);\n\n const destination = dropResult.destination;\n\n if (!destination) return draft;\n\n const sourceEl = draft[dropResult.source.index];\n const destEl = draft[destination.index];\n\n draft.splice(destination.index, 1, sourceEl);\n draft.splice(dropResult.source.index, 1, destEl);\n\n return draft;\n}\n\n/**\n * Calculates the maximum depth of nested column groups\n */\nexport function getMaxGroupDepth<T>(groups: readonly DataTableColumnGroup<T>[]): number {\n if (!groups || groups.length === 0) return 0;\n\n return Math.max(\n ...groups.map((group) => {\n if (group.groups && group.groups.length > 0) {\n return 1 + getMaxGroupDepth(group.groups);\n }\n return 1;\n })\n );\n}\n\n/**\n * Flattens nested column groups to extract all columns at the deepest level\n */\nexport function flattenColumns<T>(groups: DataTableColumnGroup<T>[]): DataTableColumn<T>[] {\n const columns: DataTableColumn<T>[] = [];\n\n for (const group of groups) {\n if (group.columns && group.columns.length > 0) {\n columns.push(...group.columns.filter((col) => col != null));\n } else if (group.groups && group.groups.length > 0) {\n columns.push(...flattenColumns(group.groups));\n }\n }\n\n return columns.filter((col) => col != null);\n}\n\n/**\n * Calculates the column span for a group based on visible columns\n */\nexport function calculateColSpan<T>(group: DataTableColumnGroup<T>, visibles?: (boolean | undefined)[]): number {\n if (group.columns && group.columns.length > 0) {\n return group.columns.filter((column, index) => {\n if (column.hidden) return false;\n return visibles ? visibles[index] !== false : true;\n }).length;\n }\n\n if (group.groups && group.groups.length > 0) {\n return group.groups.reduce((sum, subGroup) => {\n return sum + calculateColSpan(subGroup, visibles);\n }, 0);\n }\n\n return 0;\n}\n\n/**\n * Gets all groups at a specific depth level\n */\nexport function getGroupsAtDepth<T>(\n groups: readonly DataTableColumnGroup<T>[],\n targetDepth: number,\n currentDepth: number = 0\n): DataTableColumnGroup<T>[] {\n if (currentDepth === targetDepth) {\n return [...groups];\n }\n\n const result: DataTableColumnGroup<T>[] = [];\n for (const group of groups) {\n if (group.groups && group.groups.length > 0) {\n result.push(...getGroupsAtDepth(group.groups, targetDepth, currentDept