diff --git a/CHANGELOG.md b/CHANGELOG.md index f0ee26a9..054d160b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/frontend/src/components/tables/CustomTable/CustomTable.jsx b/frontend/src/components/tables/CustomTable/CustomTable.jsx index 8b0a9f0c..d0dcdbd8 100644 --- a/frontend/src/components/tables/CustomTable/CustomTable.jsx +++ b/frontend/src/components/tables/CustomTable/CustomTable.jsx @@ -76,6 +76,7 @@ const CustomTable = ({ table }) => { tableCellProps={table.tableCellProps} enableDragDrop={table.enableDragDrop} selectedTableIdsSet={table.selectedTableIdsSet} + handleRowClickRef={table.handleRowClickRef} /> ); diff --git a/frontend/src/components/tables/CustomTable/CustomTableBody.jsx b/frontend/src/components/tables/CustomTable/CustomTableBody.jsx index 02aa08e7..170d51e6 100644 --- a/frontend/src/components/tables/CustomTable/CustomTableBody.jsx +++ b/frontend/src/components/tables/CustomTable/CustomTableBody.jsx @@ -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} diff --git a/frontend/src/components/tables/CustomTable/index.jsx b/frontend/src/components/tables/CustomTable/index.jsx index 0065b4c2..e691dc6e 100644 --- a/frontend/src/components/tables/CustomTable/index.jsx +++ b/frontend/src/components/tables/CustomTable/index.jsx @@ -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 (