mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 10:45:27 +00:00
hopefully finalizing table rewrite
This commit is contained in:
parent
5eae8bd603
commit
3e2f91abf8
9 changed files with 490 additions and 498 deletions
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
202
frontend/src/components/tables/CustomTable/index.jsx
Normal file
202
frontend/src/components/tables/CustomTable/index.jsx
Normal 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 };
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
}
|
||||
|
||||
.td {
|
||||
height: 21px;
|
||||
height: 28px;
|
||||
border-bottom: solid 1px rgb(68,68,68);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue