Enhancement: implement Shift+click and Ctrl+click row selection in tables for improved user interaction
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run

This commit is contained in:
SergeantPanda 2026-05-20 17:27:32 -05:00
parent 821dccc380
commit e0bb2d925e
4 changed files with 36 additions and 1 deletions

View file

@ -41,6 +41,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`POST /api/channels/series-rules/preview/`** returns up to 25 (configurable, max 100) upcoming programs that a candidate rule would match within the standard 7-day evaluation horizon, without persisting anything. Used by the new rule editor to give live feedback as the user types.
- **`GET /api/epg/programs/search/?tvg_id=`** filter parameter for exact `epg.tvg_id` matches.
- **Series rule editor modal** with form (title + match mode, description + match mode, episodes mode, pinned channel) and a debounced (500ms) preview pane backed by an `AbortController` so per-keystroke calls don't pile up. A "Customize rule..." link in the program record-choice modal opens the editor pre-filled with the program's `tvg_id` and title; the series rules modal gains an "Add rule" button and an "Edit" button per existing rule.
- **Shift+click and Ctrl+click row selection in tables.** Clicking anywhere on a non-interactive area of a row now participates in selection:
- **Shift+click**: extends selection from the last-clicked row to the current row (range select), identical to shift+clicking the checkbox.
- **Ctrl+click** (Cmd+click on Mac): toggles the clicked row in or out of the current selection without disturbing other selected rows.
- Plain clicks on action buttons, checkboxes, inputs, links, and menus are unaffected. The expand chevron uses `stopPropagation` so expanding a row does not also trigger selection.
### Changed

View file

@ -76,6 +76,7 @@ const CustomTable = ({ table }) => {
tableCellProps={table.tableCellProps}
enableDragDrop={table.enableDragDrop}
selectedTableIdsSet={table.selectedTableIdsSet}
handleRowClickRef={table.handleRowClickRef}
/>
</Box>
);

View file

@ -17,6 +17,7 @@ const MemoizedTableRow = React.memo(
isSelected,
renderBodyCellRef,
expandedRowRendererRef,
handleRowClickRef,
getRowStyles,
tableCellProps,
enableDragDrop,
@ -38,6 +39,7 @@ const MemoizedTableRow = React.memo(
onMouseDown={(e) => {
if (e.shiftKey) e.preventDefault();
}}
onClick={(e) => handleRowClickRef?.current?.(row.original.id, e)}
style={{
display: 'flex',
width: '100%',
@ -101,6 +103,7 @@ const CustomTableBody = ({
tableCellProps,
enableDragDrop = false,
selectedTableIdsSet,
handleRowClickRef,
}) => {
// Store callbacks in refs so memoized rows always access the latest versions
// without the function references themselves triggering re-renders.
@ -127,6 +130,7 @@ const CustomTableBody = ({
}
renderBodyCellRef={renderBodyCellRef}
expandedRowRendererRef={expandedRowRendererRef}
handleRowClickRef={handleRowClickRef}
getRowStyles={getRowStyles}
tableCellProps={tableCellProps}
enableDragDrop={enableDragDrop}

View file

@ -34,6 +34,7 @@ const useTable = ({
lastClickedIdRef.current = lastClickedId;
const allRowIdsRef = useRef(allRowIds);
allRowIdsRef.current = allRowIds;
const handleRowClickRef = useRef(null);
// Use shared table preferences hook
const { headerPinned, setHeaderPinned, tableSize, setTableSize } =
@ -171,6 +172,29 @@ const useTable = ({
return true; // Return true to indicate we've handled it
};
const handleRowClick = (rowId, e) => {
if (
e.target.closest(
'button, a, input, select, textarea, [role="menuitem"], [role="option"], [role="button"]'
)
) {
return;
}
if (e.shiftKey) {
handleShiftSelect(rowId, true);
} else if (e.ctrlKey || e.metaKey) {
const newSet = new Set(selectedTableIdsRef.current);
if (newSet.has(rowId)) {
newSet.delete(rowId);
} else {
newSet.add(rowId);
setLastClickedId(rowId);
}
updateSelectedTableIds([...newSet]);
}
};
handleRowClickRef.current = handleRowClick;
const renderBodyCell = ({ row, cell }) => {
if (bodyCellRenderFns[cell.column.id]) {
return bodyCellRenderFns[cell.column.id]({ row, cell });
@ -209,7 +233,8 @@ const useTable = ({
return (
<Center
style={{ width: '100%', cursor: 'pointer' }}
onClick={() => {
onClick={(e) => {
e.stopPropagation();
onRowExpansion(row);
}}
>
@ -239,6 +264,7 @@ const useTable = ({
expandedRowIds,
expandedRowRenderer,
setSelectedTableIds,
handleRowClickRef,
headerPinned,
setHeaderPinned,
tableSize,