Dispatcharr/frontend/src/components/tables/CustomTable/CustomTableHeader.jsx
SergeantPanda 10169b96c0 feat: Implement table header pin toggle and refactor table preferences management (Closes #663)
- Added functionality to pin/unpin table headers, maintaining visibility while scrolling. This feature is accessible via the channel table menu and UI Settings page, with persistence across sessions.
- Refactored table preferences management by migrating `table-size` preference to a centralized `useTablePreferences` hook, enhancing maintainability and consistency across table components.
2026-01-15 13:40:13 -06:00

166 lines
5.4 KiB
JavaScript

import { Box, Center, Checkbox, Flex } from '@mantine/core';
import { flexRender } from '@tanstack/react-table';
import { useCallback, useMemo } from 'react';
const CustomTableHeader = ({
getHeaderGroups,
allRowIds,
selectedTableIds,
headerCellRenderFns,
onSelectAllChange,
tableCellProps,
headerPinned = true,
}) => {
const renderHeaderCell = (header) => {
if (headerCellRenderFns[header.id]) {
return headerCellRenderFns[header.id](header);
}
switch (header.id) {
case 'select':
return (
<Center style={{ width: '100%' }}>
<Checkbox
size="xs"
checked={
allRowIds.length == 0
? false
: selectedTableIds.length == allRowIds.length
}
indeterminate={
selectedTableIds.length > 0 &&
selectedTableIds.length !== allRowIds.length
}
onChange={onSelectAllChange}
/>
</Center>
);
default:
return flexRender(header.column.columnDef.header, header.getContext());
}
};
// Get header groups for dependency tracking
const headerGroups = getHeaderGroups();
// Calculate minimum width based only on fixed-size columns
const minTableWidth = useMemo(() => {
if (!headerGroups || headerGroups.length === 0) return 0;
const width =
headerGroups[0]?.headers.reduce((total, header) => {
// Only count columns with fixed sizes, flexible columns will expand
const columnSize = header.column.columnDef.size
? header.getSize()
: header.column.columnDef.minSize || 150; // Default min for flexible columns
return total + columnSize;
}, 0) || 0;
return width;
}, [headerGroups]);
// Memoize the style object to ensure it updates when headerPinned changes
const headerStyle = useMemo(
() => ({
position: headerPinned ? 'sticky' : 'relative',
top: headerPinned ? 0 : 'auto',
backgroundColor: '#3E3E45',
zIndex: headerPinned ? 10 : 1,
}),
[headerPinned]
);
return (
<Box
className="thead"
style={headerStyle}
data-header-pinned={headerPinned ? 'true' : 'false'}
>
{getHeaderGroups().map((headerGroup) => (
<Box
className="tr"
key={headerGroup.id}
style={{
display: 'flex',
width: '100%',
minWidth: '100%', // Force full width
}}
>
{headerGroup.headers.map((header) => {
return (
<Box
className="th"
key={header.id}
style={{
...(header.column.columnDef.grow
? {
flex: '1 1 0%',
minWidth: 0,
}
: {
flex: `0 0 ${header.getSize ? header.getSize() : 150}px`,
width: `${header.getSize ? header.getSize() : 150}px`,
maxWidth: `${header.getSize ? header.getSize() : 150}px`,
}),
position: 'relative',
// ...(tableCellProps && tableCellProps({ cell: header })),
}}
>
<Flex
align="center"
style={{
...(header.column.columnDef.style &&
header.column.columnDef.style),
height: '100%',
width: '100%',
paddingRight: header.column.getCanResize() ? '8px' : '0px', // Add padding for resize handle
}}
>
{renderHeaderCell(header)}
</Flex>
{header.column.getCanResize() && (
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={`resizer ${
header.column.getIsResizing() ? 'isResizing' : ''
}`}
style={{
position: 'absolute',
right: 0,
top: 0,
height: '100%',
width: '8px', // Make it slightly wider
cursor: 'col-resize',
userSelect: 'none',
touchAction: 'none',
backgroundColor: header.column.getIsResizing()
? '#3b82f6'
: 'transparent',
opacity: header.column.getIsResizing() ? 1 : 0.3, // Make it more visible by default
transition: 'opacity 0.2s',
zIndex: 1000, // Ensure it's on top
}}
onMouseEnter={(e) => {
e.target.style.opacity = '1';
e.target.style.backgroundColor = '#6b7280';
}}
onMouseLeave={(e) => {
if (!header.column.getIsResizing()) {
e.target.style.opacity = '0.5';
e.target.style.backgroundColor = 'transparent';
}
}}
/>
)}
</Box>
);
})}
</Box>
))}
</Box>
);
};
export default CustomTableHeader;