From cf3b53f8f11fee861b3db577dea47326a7be6b9b Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Fri, 15 May 2026 01:35:10 -0700 Subject: [PATCH 1/2] feat(web): row-click selection on playlist tracks in edit mode While the playlist detail page is in edit mode, clicking a track row toggles its selection in the bulk-actions context instead of playing the track. Holding shift while clicking extends the selection over the range between the previous click and the new one. - New `EditAwareTracksTable` wrapper around the standard `TracksTable`. It captures the global shift-key state with a window listener (TracksTable's onClickRow does not pass a MouseEvent) and rewrites `onClickRow` to call `selection.toggle(id, index, { shift })` when edit mode is active. - Outside of edit mode the wrapper is a transparent pass-through and the existing play-on-click behavior is preserved. - Desktop `CollectionPage` swaps its `TracksTable` usage for the new edit-aware wrapper. Combined with the bulk-actions bar from the previous PR, the user can now: shift-click a range, Cmd/Ctrl+A to select all, Escape to clear, Delete to remove, Cmd/Ctrl+Z/Y for undo/redo, and the bar's Copy URLs / Remove buttons for bulk operations. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../edit-mode/tracks/EditAwareTracksTable.tsx | 66 +++++++++++++++++++ .../components/desktop/CollectionPage.tsx | 5 +- 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 packages/web/src/components/collection/desktop/edit-mode/tracks/EditAwareTracksTable.tsx diff --git a/packages/web/src/components/collection/desktop/edit-mode/tracks/EditAwareTracksTable.tsx b/packages/web/src/components/collection/desktop/edit-mode/tracks/EditAwareTracksTable.tsx new file mode 100644 index 00000000000..b8b9289759b --- /dev/null +++ b/packages/web/src/components/collection/desktop/edit-mode/tracks/EditAwareTracksTable.tsx @@ -0,0 +1,66 @@ +import { ComponentProps, useCallback, useEffect, useRef } from 'react' + +import { ID } from '@audius/common/models' + +import { TracksTable } from 'components/tracks-table' + +import { usePlaylistEditMode } from '../PlaylistEditModeContext' + +import { useTrackSelection } from './TrackSelectionContext' + +type TrackLike = { track_id?: number | null } & Record + +type EditAwareTracksTableProps = ComponentProps & { + collectionId: ID +} + +/** + * Wraps the standard TracksTable for the playlist detail page so that, while + * the page is in edit mode, clicking a row toggles selection (shift to extend + * the range) instead of activating playback. Outside of edit mode the + * behavior is identical to the underlying TracksTable. + */ +export const EditAwareTracksTable = (props: EditAwareTracksTableProps) => { + const { collectionId, onClickRow, ...rest } = props + const editMode = usePlaylistEditMode() + const selection = useTrackSelection() + const isEditingThis = + editMode.isEditMode && editMode.collectionId === collectionId + + // Capture shift modifier state from keyboard so we can extend the selection + // even though TracksTable's onClickRow does not pass the MouseEvent. + const shiftRef = useRef(false) + useEffect(() => { + if (!isEditingThis) { + shiftRef.current = false + return + } + const down = (e: KeyboardEvent) => { + if (e.key === 'Shift') shiftRef.current = true + } + const up = (e: KeyboardEvent) => { + if (e.key === 'Shift') shiftRef.current = false + } + window.addEventListener('keydown', down) + window.addEventListener('keyup', up) + return () => { + window.removeEventListener('keydown', down) + window.removeEventListener('keyup', up) + } + }, [isEditingThis]) + + const handleClickRow = useCallback( + (track: TrackLike, index: number) => { + if (!isEditingThis) { + onClickRow?.(track, index) + return + } + const id = track.track_id + if (typeof id !== 'number') return + selection.toggle(id, index, { shift: shiftRef.current }) + }, + [isEditingThis, onClickRow, selection] + ) + + return +} diff --git a/packages/web/src/pages/collection-page/components/desktop/CollectionPage.tsx b/packages/web/src/pages/collection-page/components/desktop/CollectionPage.tsx index f4a99a34b57..9405da2238d 100644 --- a/packages/web/src/pages/collection-page/components/desktop/CollectionPage.tsx +++ b/packages/web/src/pages/collection-page/components/desktop/CollectionPage.tsx @@ -24,6 +24,7 @@ import { CollectionDogEar } from 'components/collection' import { CollectionHeader } from 'components/collection/desktop/CollectionHeader' import { PlaylistEditModeBar } from 'components/collection/desktop/edit-mode/PlaylistEditModeBar' import { PlaylistEditModeProvider } from 'components/collection/desktop/edit-mode/PlaylistEditModeContext' +import { EditAwareTracksTable } from 'components/collection/desktop/edit-mode/tracks/EditAwareTracksTable' import { TrackBulkActionsBar } from 'components/collection/desktop/edit-mode/tracks/TrackBulkActionsBar' import { TrackHistoryProvider } from 'components/collection/desktop/edit-mode/tracks/TrackHistoryContext' import { TrackSelectionProvider } from 'components/collection/desktop/edit-mode/tracks/TrackSelectionContext' @@ -31,7 +32,6 @@ import FilterInput from 'components/filter-input/FilterInput' import Page from 'components/page/Page' import { SuggestedTracks } from 'components/suggested-tracks' import { RESPONSIVE_TABLE_POLICIES } from 'components/table/responsivePolicies' -import { TracksTable } from 'components/tracks-table' import { useRequiresAccountCallback } from 'hooks/useRequiresAccount' import { useMainContentRef } from 'pages/MainContentContext' import { computeCollectionMetadataProps } from 'pages/collection-page/store/utils' @@ -349,7 +349,8 @@ const CollectionPage = ({ type }: CollectionPageProps) => { ) : (
- Date: Fri, 15 May 2026 01:37:36 -0700 Subject: [PATCH 2/2] feat(web): highlight selected playlist rows while in edit mode Builds on the row-click selection from this branch to give the user visible feedback for which tracks are currently selected. - `TracksTable` gains an optional `rowClassNameAddition(track, index)` prop that's composed with the table's existing per-row className (used internally for the locked/disabled states). The hook is ref-stable so external state changes don't force a full re-render of the table machinery. - `EditAwareTracksTable` passes a `rowClassNameAddition` that returns the new `selected` CSS class when the row's track id is in the selection set and the page is in edit mode. The class draws a surface-2 background fill and a 3px accent bar on the left edge so selected rows are immediately scannable. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tracks/EditAwareTracksTable.module.css | 5 ++++ .../edit-mode/tracks/EditAwareTracksTable.tsx | 19 ++++++++++++++- .../components/tracks-table/TracksTable.tsx | 23 +++++++++++++++---- 3 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 packages/web/src/components/collection/desktop/edit-mode/tracks/EditAwareTracksTable.module.css diff --git a/packages/web/src/components/collection/desktop/edit-mode/tracks/EditAwareTracksTable.module.css b/packages/web/src/components/collection/desktop/edit-mode/tracks/EditAwareTracksTable.module.css new file mode 100644 index 00000000000..00fb5bab21e --- /dev/null +++ b/packages/web/src/components/collection/desktop/edit-mode/tracks/EditAwareTracksTable.module.css @@ -0,0 +1,5 @@ +.selected, +.selected td { + background-color: var(--harmony-bg-surface-2) !important; + box-shadow: inset 3px 0 0 0 var(--harmony-secondary, var(--harmony-accent)); +} diff --git a/packages/web/src/components/collection/desktop/edit-mode/tracks/EditAwareTracksTable.tsx b/packages/web/src/components/collection/desktop/edit-mode/tracks/EditAwareTracksTable.tsx index b8b9289759b..6ac5698df9b 100644 --- a/packages/web/src/components/collection/desktop/edit-mode/tracks/EditAwareTracksTable.tsx +++ b/packages/web/src/components/collection/desktop/edit-mode/tracks/EditAwareTracksTable.tsx @@ -6,6 +6,7 @@ import { TracksTable } from 'components/tracks-table' import { usePlaylistEditMode } from '../PlaylistEditModeContext' +import styles from './EditAwareTracksTable.module.css' import { useTrackSelection } from './TrackSelectionContext' type TrackLike = { track_id?: number | null } & Record @@ -62,5 +63,21 @@ export const EditAwareTracksTable = (props: EditAwareTracksTableProps) => { [isEditingThis, onClickRow, selection] ) - return + const rowClassNameAddition = useCallback( + (track: TrackLike) => { + if (!isEditingThis) return undefined + const id = track.track_id + if (typeof id !== 'number') return undefined + return selection.isSelected(id) ? styles.selected : undefined + }, + [isEditingThis, selection] + ) + + return ( + + ) } diff --git a/packages/web/src/components/tracks-table/TracksTable.tsx b/packages/web/src/components/tracks-table/TracksTable.tsx index d433a503943..dae2c233625 100644 --- a/packages/web/src/components/tracks-table/TracksTable.tsx +++ b/packages/web/src/components/tracks-table/TracksTable.tsx @@ -184,6 +184,12 @@ type TracksTableProps = { showArtistInTrackNameColumn?: boolean onClickRow?: (track: any, index: number) => void trackActionsHeader?: ReactNode + /** + * Optional additional className applied per row. The result is appended + * to the table's own per-row className. Use this for things like a + * selected-row highlight while the page is in edit mode. + */ + rowClassNameAddition?: (track: any, rowIndex: number) => string | undefined } & Omit const defaultColumns: TracksTableColumn[] = [ @@ -214,6 +220,7 @@ export const TracksTable = ({ data, activeIndex, trackActionsHeader, + rowClassNameAddition, ...tableProps }: TracksTableProps) => { const { isVirtualized, onClickRow } = tableProps @@ -1046,6 +1053,9 @@ export const TracksTable = ({ [activateTrack] ) + const rowClassNameAdditionRef = useRef(rowClassNameAddition) + rowClassNameAdditionRef.current = rowClassNameAddition + const getRowClassName = useCallback((rowIndex: number) => { const track = dataRef.current[rowIndex] const { isFetchingNFTAccess, hasStreamAccess } = trackAccessMapRef.current[ @@ -1058,10 +1068,15 @@ export const TracksTable = ({ const deleted = track.is_delete || track._marked_deleted || !!track.user?.is_deactivated const isPremium = isContentUSDCPurchaseGated(track.stream_conditions) - return cn(styles.tableRow, { - [styles.disabled]: deleted, - [styles.lockedRow]: isLocked && !deleted && !isPremium - }) + const extra = rowClassNameAdditionRef.current?.(track, rowIndex) + return cn( + styles.tableRow, + { + [styles.disabled]: deleted, + [styles.lockedRow]: isLocked && !deleted && !isPremium + }, + extra + ) }, []) return (