hopefully finalizing table rewrite

This commit is contained in:
dekzter 2025-04-23 11:02:00 -04:00
parent 5eae8bd603
commit 3e2f91abf8
9 changed files with 490 additions and 498 deletions

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-04-19 12:08
# Generated by Django 5.1.6 on 2025-04-21 20:47
from django.db import migrations, models
@ -10,14 +10,9 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AlterField(
model_name='channel',
name='channel_number',
field=models.IntegerField(db_index=True),
),
migrations.AlterField(
model_name='channelgroup',
name='name',
field=models.CharField(db_index=True, max_length=100, unique=True),
field=models.TextField(db_index=True, unique=True),
),
]

View file

@ -27,7 +27,7 @@ def get_total_viewers(channel_id):
return 0
class ChannelGroup(models.Model):
name = models.CharField(max_length=100, unique=True, db_index=True)
name = models.TextField(unique=True, db_index=True)
def related_channels(self):
# local import if needed to avoid cyc. Usually fine in a single file though

View file

@ -58,18 +58,12 @@ import {
UnstyledButton,
CopyButton,
} from '@mantine/core';
import {
useReactTable,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
flexRender,
} from '@tanstack/react-table';
import { getCoreRowModel, flexRender } from '@tanstack/react-table';
import './table.css';
import useChannelsTableStore from '../../store/channelsTable';
import ChannelTableStreams from './ChannelTableStreams';
import useLocalStorage from '../../hooks/useLocalStorage';
import { CustomTable, useTable } from './CustomTable';
const m3uUrlBase = `${window.location.protocol}//${window.location.host}/output/m3u`;
const epgUrlBase = `${window.location.protocol}//${window.location.host}/output/epg`;
@ -292,6 +286,7 @@ const ChannelsTable = ({}) => {
const env_mode = useSettingsStore((s) => s.environment.env_mode);
const [allRowIds, setAllRowIds] = useState([]);
const [channel, setChannel] = useState(null);
const [channelModalOpen, setChannelModalOpen] = useState(false);
const [recordingModalOpen, setRecordingModalOpen] = useState(false);
@ -299,12 +294,9 @@ const ChannelsTable = ({}) => {
const [selectedProfile, setSelectedProfile] = useState(
profiles[selectedProfileId]
);
const pagination = useChannelsTableStore((s) => s.pagination);
const setPagination = useChannelsTableStore((s) => s.setPagination);
const [paginationString, setPaginationString] = useState('');
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: tablePrefs.pageSize,
});
const [initialDataCount, setInitialDataCount] = useState(null);
const [filters, setFilters] = useState({
name: '',
channel_group: '',
@ -312,10 +304,9 @@ const ChannelsTable = ({}) => {
const debouncedFilters = useDebounce(filters, 500);
const [isLoading, setIsLoading] = useState(true);
const [selectedChannelIds, setSelectedChannelIds] = useState([]);
const [sorting, setSorting] = useState([
{ id: 'channel_number', desc: false },
]);
const [expandedRowId, setExpandedRowId] = useState(null);
const sorting = useChannelsTableStore((s) => s.sorting);
const setSorting = useChannelsTableStore((s) => s.setSorting);
const [expandedRowIds, setExpandedRowIds] = useState([]);
const [hdhrUrl, setHDHRUrl] = useState(hdhrUrlBase);
const [epgUrl, setEPGUrl] = useState(epgUrlBase);
@ -339,6 +330,7 @@ const ChannelsTable = ({}) => {
});
const results = await API.queryChannels(params);
const ids = await API.getAllChannelIds(params);
const startItem = pagination.pageIndex * pagination.pageSize + 1; // +1 to start from 1, not 0
const endItem = Math.min(
@ -346,15 +338,12 @@ const ChannelsTable = ({}) => {
results.count
);
if (initialDataCount === null) {
setInitialDataCount(results.count);
}
// Generate the string
setPaginationString(`${startItem} to ${endItem} of ${results.count}`);
setTablePrefs({
pageSize: pagination.pageSize,
});
setAllRowIds(ids);
}, [pagination, sorting, debouncedFilters]);
useEffect(() => {
@ -386,10 +375,6 @@ const ChannelsTable = ({}) => {
...prev,
[name]: value,
}));
setPagination({
pageIndex: 0,
pageSize: pagination.pageSize,
});
}, []);
const handleGroupChange = (value) => {
@ -397,10 +382,6 @@ const ChannelsTable = ({}) => {
...prev,
channel_group: value ? value : '',
}));
setPagination({
pageIndex: 0,
pageSize: pagination.pageSize,
});
};
const hdhrUrlRef = useRef(null);
@ -440,49 +421,49 @@ const ChannelsTable = ({}) => {
showVideo(getChannelURL(channel));
}
const onRowSelectionChange = (updater) => {
setRowSelection((prevRowSelection) => {
const newRowSelection =
typeof updater === 'function' ? updater(prevRowSelection) : updater;
// const onRowSelectionChange = (updater) => {
// setRowSelection((prevRowSelection) => {
// const newRowSelection =
// typeof updater === 'function' ? updater(prevRowSelection) : updater;
const updatedSelected = new Set([...selectedChannelIds]);
getRowModel().rows.forEach((row) => {
if (newRowSelection[row.id] === undefined || !newRowSelection[row.id]) {
updatedSelected.delete(row.original.id);
} else {
updatedSelected.add(row.original.id);
}
});
const newSelection = [...updatedSelected];
setSelectedChannelIds(newSelection);
setSelectedTableIds(newSelection);
// const updatedSelected = new Set([...selectedChannelIds]);
// getRowModel().rows.forEach((row) => {
// if (newRowSelection[row.id] === undefined || !newRowSelection[row.id]) {
// updatedSelected.delete(row.original.id);
// } else {
// updatedSelected.add(row.original.id);
// }
// });
// const newSelection = [...updatedSelected];
// setSelectedChannelIds(newSelection);
// setSelectedTableIds(newSelection);
return newRowSelection;
});
};
// return newRowSelection;
// });
// };
const onSelectAllChange = async (e) => {
const selectAll = e.target.checked;
if (selectAll) {
// Get all channel IDs for current view
const params = new URLSearchParams();
Object.entries(debouncedFilters).forEach(([key, value]) => {
if (value) params.append(key, value);
});
const ids = await API.getAllChannelIds(params);
setSelectedTableIds(ids);
setSelectedChannelIds(ids);
} else {
setSelectedTableIds([]);
setSelectedChannelIds([]);
}
// const onSelectAllChange = async (e) => {
// const selectAll = e.target.checked;
// if (selectAll) {
// // Get all channel IDs for current view
// const params = new URLSearchParams();
// Object.entries(debouncedFilters).forEach(([key, value]) => {
// if (value) params.append(key, value);
// });
// const ids = await API.getAllChannelIds(params);
// setSelectedTableIds(ids);
// setSelectedChannelIds(ids);
// } else {
// setSelectedTableIds([]);
// setSelectedChannelIds([]);
// }
const newSelection = {};
getRowModel().rows.forEach((item, index) => {
newSelection[index] = selectAll;
});
setRowSelection(newSelection);
};
// const newSelection = {};
// getRowModel().rows.forEach((item, index) => {
// newSelection[index] = selectAll;
// });
// setRowSelection(newSelection);
// };
const onPageSizeChange = (e) => {
setPagination({
@ -798,50 +779,9 @@ const ChannelsTable = ({}) => {
enableSorting: false,
},
],
[selectedProfileId, data]
[selectedProfileId, data, channelGroups]
);
const { getHeaderGroups, getRowModel } = useReactTable({
data,
columns: columns,
defaultColumn: {
size: undefined,
minSize: 0,
},
pageCount,
state: {
data,
rowCount,
sorting,
filters,
pagination,
rowSelection,
},
manualPagination: true,
manualSorting: true,
manualFiltering: true,
enableRowSelection: true,
onRowSelectionChange: onRowSelectionChange,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
// debugTable: true,
});
const rows = getRowModel().rows;
const onRowExpansion = (row) => {
let isExpanded = false;
setExpandedRowId((prev) => {
isExpanded = prev === row.original.id ? null : row.original.id;
return isExpanded;
});
setRowSelection({ [row.index]: true });
setSelectedChannelIds([row.original.id]);
setSelectedTableIds([row.original.id]);
};
const renderHeaderCell = (header) => {
let sortingIcon = ArrowUpDown;
if (sorting[0]?.id == header.id) {
@ -853,11 +793,6 @@ const ChannelsTable = ({}) => {
}
switch (header.id) {
case 'select':
return ChannelRowSelectHeader({
selectedChannelIds,
});
case 'enabled':
if (selectedProfileId !== '0' && selectedChannelIds.length > 0) {
// return EnabledHeaderSwitch();
@ -923,22 +858,85 @@ const ChannelsTable = ({}) => {
}
};
const renderBodyCell = (cell) => {
switch (cell.column.id) {
case 'select':
return ChannelRowSelectCell({ row: cell.row });
const table = useTable({
data,
columns,
allRowIds,
defaultColumn: {
size: undefined,
minSize: 0,
},
pageCount,
// state: {
// data,
// rowCount,
// sorting,
// filters,
// pagination,
// rowSelection,
// },
filters,
pagination,
sorting,
expandedRowIds,
manualPagination: true,
manualSorting: true,
manualFiltering: true,
enableRowSelection: true,
// onRowSelectionChange: onRowSelectionChange,
getCoreRowModel: getCoreRowModel(),
// getFilteredRowModel: getFilteredRowModel(),
// getSortedRowModel: getSortedRowModel(),
// getPaginationRowModel: getPaginationRowModel(),
// debugTable: true,
expandedRowRenderer: ({ row }) => {
return (
<Box
key={row.id}
className="tr"
style={{ display: 'flex', width: '100%' }}
>
<ChannelTableStreams channel={row.original} isExpanded={true} />
</Box>
);
},
headerCellRenderFns: {
name: renderHeaderCell,
enabled: () => (
<Center style={{ width: '100%' }}>
<ScanEye size="16" />
</Center>
),
},
});
case 'expand':
return ChannelExpandCell({ row: cell.row });
default:
return flexRender(cell.column.columnDef.cell, cell.getContext());
}
const onRowExpansion = (row) => {
let isExpanded = false;
setExpandedRowIds((prev) => {
isExpanded = prev === row.original.id ? null : row.original.id;
return isExpanded;
});
setRowSelection({ [row.index]: true });
setSelectedChannelIds([row.original.id]);
setSelectedTableIds([row.original.id]);
};
// const renderBodyCell = (cell) => {
// switch (cell.column.id) {
// case 'select':
// return ChannelRowSelectCell({ row: cell.row });
// case 'expand':
// return ChannelExpandCell({ row: cell.row });
// default:
// return flexRender(cell.column.columnDef.cell, cell.getContext());
// }
// };
const ChannelExpandCell = useCallback(
({ row }) => {
const isExpanded = expandedRowId === row.original.id;
const isExpanded = expandedRowIds === row.original.id;
return (
<Center
@ -951,44 +949,44 @@ const ChannelsTable = ({}) => {
</Center>
);
},
[expandedRowId]
[expandedRowIds]
);
const ChannelRowSelectCell = useCallback(
({ row }) => {
return (
<Center style={{ width: '100%' }}>
<Checkbox
size="xs"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</Center>
);
},
[rows]
);
// const ChannelRowSelectCell = useCallback(
// ({ row }) => {
// return (
// <Center style={{ width: '100%' }}>
// <Checkbox
// size="xs"
// checked={row.getIsSelected()}
// onChange={row.getToggleSelectedHandler()}
// />
// </Center>
// );
// },
// [rows]
// );
const ChannelRowSelectHeader = useCallback(
({ selectedChannelIds }) => {
return (
<Center style={{ width: '100%' }}>
<Checkbox
size="xs"
checked={
rowCount == 0 ? false : selectedChannelIds.length == rowCount
}
indeterminate={
selectedChannelIds.length > 0 &&
selectedChannelIds.length !== rowCount
}
onChange={onSelectAllChange}
/>
</Center>
);
},
[rows]
);
// const ChannelRowSelectHeader = useCallback(
// ({ selectedChannelIds }) => {
// return (
// <Center style={{ width: '100%' }}>
// <Checkbox
// size="xs"
// checked={
// rowCount == 0 ? false : selectedChannelIds.length == rowCount
// }
// indeterminate={
// selectedChannelIds.length > 0 &&
// selectedChannelIds.length !== rowCount
// }
// onChange={onSelectAllChange}
// />
// </Center>
// );
// },
// [rows]
// );
return (
<Box>
@ -1218,7 +1216,7 @@ const ChannelsTable = ({}) => {
{/* Table or ghost empty state inside Paper */}
<Box>
{initialDataCount === 0 && data.length === 0 && (
{Object.keys(channels).length === 0 && (
<Box
style={{
paddingTop: 20,
@ -1296,7 +1294,7 @@ const ChannelsTable = ({}) => {
)}
</Box>
{data.length > 0 && (
{Object.keys(channels).length > 0 && (
<Box
style={{
display: 'flex',
@ -1313,114 +1311,7 @@ const ChannelsTable = ({}) => {
borderRadius: 'var(--mantine-radius-default)',
}}
>
<Box
className="divTable table-striped"
style={{
width: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<Box
className="thead"
style={{
position: 'sticky',
top: 0,
backgroundColor: '#3E3E45',
zIndex: 10,
}}
>
{getHeaderGroups().map((headerGroup) => (
<Box
className="tr"
key={headerGroup.id}
style={{ display: 'flex', width: '100%' }}
>
{headerGroup.headers.map((header) => {
const width = header.getSize();
return (
<Box
className="th"
key={header.id}
style={{
flex: header.column.columnDef.size
? '0 0 auto'
: '1 1 0',
width: header.column.columnDef.size
? header.getSize()
: undefined,
minWidth: 0,
}}
>
<Flex
align="center"
style={{
...(header.column.columnDef.style &&
header.column.columnDef.style),
height: '100%',
}}
>
{renderHeaderCell(header)}
</Flex>
</Box>
);
})}
</Box>
))}
</Box>
<Box className="tbody">
{getRowModel().rows.map((row) => (
<Box>
<Box
key={row.id}
className="tr"
style={{
display: 'flex',
width: '100%',
...(row.getIsSelected() && {
backgroundColor: '#163632',
}),
}}
>
{row.getVisibleCells().map((cell) => {
const width = cell.column.getSize();
return (
<Box
className="td"
key={cell.id}
style={{
flex: cell.column.columnDef.size
? '0 0 auto'
: '1 1 0',
width: cell.column.columnDef.size
? cell.column.getSize()
: undefined,
minWidth: 0,
}}
>
<Flex align="center" style={{ height: '100%' }}>
{renderBodyCell(cell)}
</Flex>
</Box>
);
})}
</Box>
{row.original.id === expandedRowId && (
<Box
key={row.id}
className="tr"
style={{ display: 'flex', width: '100%' }}
>
<ChannelTableStreams
channel={row.original}
isExpanded={true}
/>
</Box>
)}
</Box>
))}
</Box>
</Box>
<CustomTable table={table} />
</Box>
<Box

View file

@ -2,70 +2,10 @@ import { Box, Flex } from '@mantine/core';
import CustomTableHeader from './CustomTableHeader';
import { useCallback, useState } from 'react';
import { flexRender } from '@tanstack/react-table';
import table from '../../../helpers/table';
import CustomTableBody from './CustomTableBody';
const CustomTable = ({
table,
headerCellRenderer,
rowDetailRenderer,
bodyCellRenderFns,
rowCount,
}) => {
const [expandedRowId, setExpandedRowId] = useState(null);
const rows = table.getRowModel().rows;
const ChannelExpandCell = useCallback(
({ row }) => {
const isExpanded = expandedRowId === row.original.id;
return (
<Center
style={{ width: '100%', cursor: 'pointer' }}
onClick={() => {
setExpandedRowId((prev) =>
prev === row.original.id ? null : row.original.id
);
}}
>
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</Center>
);
},
[expandedRowId]
);
const ChannelRowSelectCell = useCallback(
({ row }) => {
return (
<Center style={{ width: '100%' }}>
<Checkbox
size="xs"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</Center>
);
},
[rows]
);
const bodyCellRenderer = (cell) => {
if (bodyCellRenderFns[cell.column.id]) {
return bodyCellRenderFns(cell);
}
switch (cell.column.id) {
case 'select':
return ChannelRowSelectCell({ row: cell.row });
case 'expand':
return ChannelExpandCell({ row: cell.row });
default:
return flexRender(cell.column.columnDef.cell, cell.getContext());
}
};
const CustomTable = ({ table }) => {
return (
<Box
className="divTable table-striped"
@ -76,57 +16,21 @@ const CustomTable = ({
}}
>
<CustomTableHeader
table={table}
headerCellRenderer={headerCellRenderer}
rowCount={rowCount}
onSelectAllChange={onSelectAllChange}
filters={table.filters}
getHeaderGroups={table.getHeaderGroups}
allRowIds={table.allRowIds}
headerCellRenderFns={table.headerCellRenderFns}
onSelectAllChange={
table.onSelectAllChange ? table.onSelectAllChange : null
}
selectedTableIds={table.selectedTableIds}
/>
<CustomTableBody
getRowModel={table.getRowModel}
bodyCellRenderFns={table.bodyCellRenderFns}
expandedRowIds={table.expandedRowIds}
expandedRowRenderer={table.expandedRowRenderer}
/>
<Box className="tbody">
{table.getRowModel().rows.map((row) => (
<Box>
<Box
key={row.id}
className="tr"
style={{
display: 'flex',
width: '100%',
...(row.getIsSelected() && {
backgroundColor: '#163632',
}),
}}
>
{row.getVisibleCells().map((cell) => {
return (
<Box
className="td"
key={cell.id}
style={{
flex: cell.column.columnDef.size ? '0 0 auto' : '1 1 0',
width: cell.column.columnDef.size
? cell.column.getSize()
: undefined,
minWidth: 0,
}}
>
<Flex align="center" style={{ height: '100%' }}>
{bodyCellRenderer(cell)}
</Flex>
</Box>
);
})}
</Box>
{row.original.id === expandedRowId && (
<Box
key={row.id}
className="tr"
style={{ display: 'flex', width: '100%' }}
>
<ChannelStreams channel={row.original} isExpanded={true} />
</Box>
)}
</Box>
))}
</Box>
</Box>
);
};

View file

@ -0,0 +1,65 @@
import { Box, Flex } from '@mantine/core';
import { flexRender } from '@tanstack/react-table';
const CustomTableBody = ({
getRowModel,
bodyCellRenderFns,
expandedRowIds,
expandedRowRenderer,
}) => {
const renderExpandedRow = (row) => {
if (expandedRowRenderer) {
return expandedRowRenderer({ row });
}
return <></>;
};
return (
<Box className="tbody">
{getRowModel().rows.map((row) => (
<Box>
<Box
key={row.id}
className="tr"
style={{
display: 'flex',
width: '100%',
...(row.getIsSelected() && {
backgroundColor: '#163632',
}),
}}
>
{row.getVisibleCells().map((cell) => {
return (
<Box
className="td"
key={cell.id}
style={{
flex: cell.column.columnDef.size ? '0 0 auto' : '1 1 0',
width: cell.column.columnDef.size
? cell.column.getSize()
: undefined,
minWidth: 0,
}}
>
<Flex align="center" style={{ height: '100%' }}>
{bodyCellRenderFns[cell.column.id]
? bodyCellRenderFns[cell.column.id](cell)
: flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</Flex>
</Box>
);
})}
</Box>
{expandedRowIds.includes(row.original.id) && renderExpandedRow(row)}
</Box>
))}
</Box>
);
};
export default CustomTableBody;

View file

@ -1,120 +1,39 @@
import { Box, Flex } from '@mantine/core';
import {
ArrowDownWideNarrow,
ArrowUpDown,
ArrowUpNarrowWide,
} from 'lucide-react';
import { Box, Center, Checkbox, Flex } from '@mantine/core';
import { flexRender } from '@tanstack/react-table';
import { useCallback } from 'react';
const CustomTableHeader = ({
table,
getHeaderGroups,
allRowIds,
selectedTableIds,
headerCellRenderFns,
rowCount,
onSelectAllChange,
}) => {
const ChannelRowSelectHeader = useCallback(
({ selectedChannelIds }) => {
return (
<Center style={{ width: '100%' }}>
<Checkbox
size="xs"
checked={
rowCount == 0 ? false : selectedChannelIds.length == rowCount
}
indeterminate={
selectedChannelIds.length > 0 &&
selectedChannelIds.length !== rowCount
}
onChange={onSelectAllChange}
/>
</Center>
);
},
[rows, rowCount]
);
const onSelectAll = (e) => {
if (onSelectAllChange) {
onSelectAllChange(e);
}
};
const headerCellRenderer = (header) => {
let sortingIcon = ArrowUpDown;
if (sorting[0]?.id == header.id) {
if (sorting[0].desc === false) {
sortingIcon = ArrowUpNarrowWide;
} else {
sortingIcon = ArrowDownWideNarrow;
}
const renderHeaderCell = (header) => {
if (headerCellRenderFns[header.id]) {
return headerCellRenderFns[header.id](header);
}
switch (header.id) {
case 'select':
return ChannelRowSelectHeader({
selectedChannelIds,
});
case 'enabled':
if (selectedProfileId !== '0' && selectedChannelIds.length > 0) {
// return EnabledHeaderSwitch();
}
return (
<Center style={{ width: '100%' }}>
<ScanEye size="16" />
<Checkbox
size="xs"
checked={
allRowIds.length == 0
? false
: selectedTableIds.length == allRowIds.length
}
indeterminate={
selectedTableIds.length > 0 &&
selectedTableIds.length !== allRowIds.length
}
onChange={onSelectAllChange}
/>
</Center>
);
// case 'channel_number':
// return (
// <Flex gap={2}>
// #
// {/* <Center>
// {React.createElement(sortingIcon, {
// onClick: () => onSortingChange('name'),
// size: 14,
// })}
// </Center> */}
// </Flex>
// );
// case 'name':
// return (
// <Flex gap="sm">
// <TextInput
// name="name"
// placeholder="Name"
// value={filters.name || ''}
// onClick={(e) => e.stopPropagation()}
// onChange={handleFilterChange}
// size="xs"
// variant="unstyled"
// className="table-input-header"
// />
// <Center>
// {React.createElement(sortingIcon, {
// onClick: () => onSortingChange('name'),
// size: 14,
// })}
// </Center>
// </Flex>
// );
// case 'channel_group':
// return (
// <MultiSelect
// placeholder="Group"
// variant="unstyled"
// data={groupOptions}
// size="xs"
// searchable
// clearable
// onClick={stopPropagation}
// onChange={handleGroupChange}
// style={{ width: '100%' }}
// />
// );
default:
return flexRender(header.column.columnDef.header, header.getContext());
}
@ -130,7 +49,7 @@ const CustomTableHeader = ({
zIndex: 10,
}}
>
{table.getHeaderGroups().map((headerGroup) => (
{getHeaderGroups().map((headerGroup) => (
<Box
className="tr"
key={headerGroup.id}
@ -157,7 +76,7 @@ const CustomTableHeader = ({
height: '100%',
}}
>
{headerCellRenderer(header)}
{renderHeaderCell(header)}
</Flex>
</Box>
);

View file

@ -0,0 +1,202 @@
import { Center, Checkbox } from '@mantine/core';
import CustomTable from './CustomTable';
import CustomTableHeader from './CustomTableHeader';
import {
useReactTable,
getCoreRowModel,
flexRender,
} from '@tanstack/react-table';
import { useCallback, useMemo, useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
const useTable = ({
allRowIds,
headerCellRenderFns = {},
filters = {},
pagination = {},
sorting = [],
expandedRowRenderer = () => <></>,
...options
}) => {
const [selectedTableIds, setSelectedTableIds] = useState([]);
const [expandedRowIds, setExpandedRowIds] = useState([]);
const rowCount = allRowIds.length;
const onRowSelectionChange = (updater) => {
const newRowSelection =
typeof updater === 'function' ? updater(rowSelection) : updater;
const updatedSelected = new Set(selectedTableIds);
const allChangedRowIds = new Set([
...Object.keys(rowSelection),
...Object.keys(newRowSelection),
]);
for (const rowId of allChangedRowIds) {
const wasSelected = !!rowSelection[rowId];
const isSelected = !!newRowSelection[rowId];
if (wasSelected !== isSelected) {
const row = table.getRow(rowId);
if (!row) continue;
const originalId = row.original.id;
if (isSelected) {
updatedSelected.add(originalId);
} else {
updatedSelected.delete(originalId);
}
}
}
setSelectedTableIds([...updatedSelected]);
};
const table = useReactTable({
...options,
state: {
data: options.data,
selectedTableIds,
},
onRowSelectionChange,
getCoreRowModel: options.getCoreRowModel ?? getCoreRowModel(),
});
const selectedTableIdsSet = useMemo(
() => new Set(selectedTableIds),
[selectedTableIds]
);
const rowSelection = useMemo(() => {
const selection = {};
table.getRowModel().rows.forEach((row) => {
if (selectedTableIdsSet.has(row.original.id)) {
selection[row.id] = true;
}
});
return selection;
}, [selectedTableIdsSet, table.getRowModel().rows]);
const onSelectAllChange = async (e) => {
const selectAll = e.target.checked;
if (selectAll) {
setSelectedTableIds(allRowIds);
} else {
setSelectedTableIds([]);
}
};
const rows = table.getRowModel().rows;
const onRowExpansion = (row) => {
let isExpanded = false;
setExpandedRowIds((prev) => {
isExpanded = prev.includes(row.original.id) ? [] : [row.original.id];
return isExpanded;
});
setSelectedTableIds([row.original.id]);
};
const renderHeaderCell = useCallback(
(header) => {
if (table.headerCellRenderFns && table.headerCellRenderFns[header.id]) {
return table.headerCellRenderFns[header.id](header);
}
switch (header.id) {
case 'select':
return (
<Center style={{ width: '100%' }}>
<Checkbox
size="xs"
checked={
rowCount == 0 ? false : selectedTableIds.length == rowCount
}
indeterminate={
selectedTableIds.length > 0 &&
selectedTableIds.length !== rowCount
}
onChange={onSelectAllChange}
/>
</Center>
);
default:
return flexRender(
header.column.columnDef.header,
header.getContext()
);
}
},
[filters, selectedTableIds, rowCount, onSelectAllChange, sorting]
);
const bodyCellRenderFns = {
select: useCallback(
({ row }) => {
return (
<Center style={{ width: '100%' }}>
<Checkbox
size="xs"
checked={selectedTableIdsSet.has(row.original.id)}
onChange={(e) => {
const newSet = new Set(selectedTableIds);
if (e.target.checked) {
newSet.add(row.original.id);
} else {
newSet.delete(row.original.id);
}
setSelectedTableIds([...newSet]);
}}
/>
</Center>
);
},
[rows, selectedTableIdsSet]
),
expand: useCallback(({ row }) => {
const isExpanded = expandedRowIds.includes(row.original.id);
return (
<Center
style={{ width: '100%', cursor: 'pointer' }}
onClick={() => {
onRowExpansion(row);
}}
>
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</Center>
);
}),
};
// Return both the table instance and your custom methods
const tableInstance = useMemo(
() => ({
...table,
...options,
sorting,
selectedTableIds,
setSelectedTableIds,
rowSelection,
allRowIds,
onSelectAllChange,
selectedTableIdsSet,
expandedRowIds,
expandedRowRenderer,
}),
[selectedTableIdsSet, expandedRowIds]
);
return {
...tableInstance,
headerCellRenderFns,
renderHeaderCell,
bodyCellRenderFns,
};
};
export { useTable, CustomTable, CustomTableHeader };

View file

@ -41,7 +41,7 @@
}
.td {
height: 21px;
height: 28px;
border-bottom: solid 1px rgb(68,68,68);
}

View file

@ -6,7 +6,11 @@ import API from '../api';
const useChannelsTableStore = create((set, get) => ({
channels: [],
count: 0,
pageCount: 0,
sorting: [{ id: 'channel_number', desc: false }],
pagination: {
pageIndex: 0,
pageCount: 50,
},
selectedChannelIds: [],
queryChannels: ({ results, count }, params) => {
@ -29,6 +33,18 @@ const useChannelsTableStore = create((set, get) => ({
const channel = get().channels.find((c) => c.id === id);
return channel?.streams ?? [];
},
setPagination: (pagination) => {
set((state) => ({
pagination,
}));
},
setSorting: (sorting) => {
set((state) => ({
sorting,
}));
},
}));
export default useChannelsTableStore;