fix(favorites): artist link no-play + inline bulk action chips (#746)

* fix(favorites): artist link in songs table no longer triggers playback

The artist cell's click handler navigated without `stopPropagation`, so
the row's onClick still fired and started the track. Matches the album
cell's behaviour and the other tracklists (SongRow, PlaylistTracklist,
PlaylistSuggestions, SongCard, AlbumCard) — all of which already guard
the navigation click.

* fix(favorites): inline bulk action chips in section header

The full-width bulk-action bar above the column header pushed every row
down by ~36 px when an item was selected, making it harder to pick
adjacent items. Move the "X selected / Add to playlist / Clear" cluster
into the existing action-buttons row (right-aligned via margin-left:
auto), matching the album toolbar pattern. Selection mode no longer
shifts the rows. Clear-selection button uses `btn-surface` to match the
other secondary actions on the page (post-#745 convention).

* docs(changelog): note Favorites artist-link + bulk-bar fixes (#746)
This commit is contained in:
Frank Stellmacher
2026-05-17 12:55:31 +02:00
committed by GitHub
parent 3b94368ffa
commit 2beb30f871
4 changed files with 63 additions and 40 deletions
+7
View File
@@ -638,6 +638,13 @@ Foundational work: faster reviews, narrower diffs, and a safety net under the pa
* Hero pills stay visible against light-toned cover art (opaque fill), and the pagination dots are readable on every backdrop (brighter inactive dot with a dark outline, accent-coloured active dot).
* Composers grid no longer reserves ~200 px per virtual row for ~78 px text-only tiles. The Tracks "browse all" header now lives inside the scroll container so columns line up with the rows under wider fonts like **OpenDyslexic**, and the header stays pinned while scrolling.
### Favorites — artist link no longer triggers playback, bulk selection no longer shifts the rows
**By [@Psychotoxical](https://github.com/Psychotoxical), thanks to zunoz for the report on the Psysonic Discord, PR [#746](https://github.com/Psychotoxical/psysonic/pull/746)**
* Clicking the **artist** in the Favorites songs table opened the artist page _and_ started the song — the cell was missing the click guard the album cell already had. Now matches every other tracklist in the app.
* Selecting a song no longer pushes the column header and every row down by one line. The "X selected / Add to playlist / Clear" cluster moved out of the full-width bar into the existing action-buttons row (right-aligned), matching the album toolbar, so the next item stays under the same cursor position.
## [1.45.0] - 2026-05-04
## Added
@@ -3,7 +3,9 @@ import { useTranslation } from 'react-i18next';
import { ListPlus, Play, SlidersHorizontal, X } from 'lucide-react';
import type { SubsonicSong } from '../../api/subsonicTypes';
import { usePlayerStore } from '../../store/playerStore';
import { useSelectionStore } from '../../store/selectionStore';
import { songToTrack } from '../../utils/playback/songToTrack';
import { AddToPlaylistSubmenu } from '../ContextMenu';
import GenreFilterBar from '../GenreFilterBar';
interface Props {
@@ -25,6 +27,10 @@ interface Props {
starredOverrides: Record<string, boolean>;
minYear: number;
currentYear: number;
inSelectMode: boolean;
selectedCount: number;
showPlPicker: boolean;
setShowPlPicker: React.Dispatch<React.SetStateAction<boolean>>;
}
export default function FavoritesSongsSectionHeader({
@@ -32,6 +38,7 @@ export default function FavoritesSongsSectionHeader({
selectedGenres, setSelectedGenres, yearRange, setYearRange,
showFilters, setShowFilters, setSortKey, setSortClickCount,
playTrack, enqueue, starredOverrides, minYear, currentYear,
inSelectMode, selectedCount, showPlPicker, setShowPlPicker,
}: Props) {
const { t } = useTranslation();
@@ -100,6 +107,39 @@ export default function FavoritesSongsSectionHeader({
{t('common.clearAll')}
</button>
)}
{/* Bulk action chips — inline at row end so a selection does not
push the column header / rows downward (matches Album toolbar). */}
{inSelectMode && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginLeft: 'auto' }}>
<span className="bulk-action-count">
{t('common.bulkSelected', { count: selectedCount })}
</span>
<div className="bulk-pl-picker-wrap">
<button
className="btn btn-surface btn-sm"
onClick={() => setShowPlPicker(v => !v)}
>
<ListPlus size={14} />
{t('common.bulkAddToPlaylist')}
</button>
{showPlPicker && (
<AddToPlaylistSubmenu
songIds={[...useSelectionStore.getState().selectedIds]}
onDone={() => { setShowPlPicker(false); useSelectionStore.getState().clearAll(); }}
dropDown
/>
)}
</div>
<button
className="btn btn-surface btn-sm"
onClick={() => useSelectionStore.getState().clearAll()}
>
<X size={13} />
{t('common.bulkClear')}
</button>
</div>
)}
</div>
{/* Filters Panel */}
@@ -2,7 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import {
AudioLines, Check, ChevronDown, ChevronRight, ListPlus, Play, RotateCcw,
AudioLines, Check, ChevronDown, ChevronRight, Play, RotateCcw,
Square, X,
} from 'lucide-react';
import type { ColDef } from '../../utils/useTracklistColumns';
@@ -16,7 +16,6 @@ import { songToTrack } from '../../utils/playback/songToTrack';
import { formatTrackTime } from '../../utils/format/formatDuration';
import { formatLastSeen } from '../../utils/componentHelpers/userMgmtHelpers';
import i18n from '../../i18n';
import { AddToPlaylistSubmenu } from '../ContextMenu';
import StarRating from '../StarRating';
const SORTABLE_COLUMNS = new Set(['title', 'artist', 'album', 'rating', 'duration', 'playCount', 'lastPlayed', 'bpm']);
@@ -27,8 +26,6 @@ interface Props {
selectedCount: number;
inSelectMode: boolean;
toggleSelect: (id: string, idx: number, shift: boolean) => void;
showPlPicker: boolean;
setShowPlPicker: React.Dispatch<React.SetStateAction<boolean>>;
allColumns: readonly ColDef[];
visibleCols: ColDef[];
gridStyle: React.CSSProperties;
@@ -50,7 +47,6 @@ interface Props {
export default function FavoritesSongsTracklist({
visibleSongs, selectedIds, selectedCount, inSelectMode, toggleSelect,
showPlPicker, setShowPlPicker,
allColumns, visibleCols, gridStyle, colVisible, toggleColumn, resetColumns,
pickerOpen, setPickerOpen, pickerRef, tracklistRef,
startResize, handleSortClick, getSortIndicator,
@@ -73,38 +69,6 @@ export default function FavoritesSongsTracklist({
if (inSelectMode && e.target === e.currentTarget) useSelectionStore.getState().clearAll();
}}>
{/* ── Bulk action bar ── */}
{inSelectMode && (
<div className="bulk-action-bar">
<span className="bulk-action-count">
{t('common.bulkSelected', { count: selectedCount })}
</span>
<div className="bulk-pl-picker-wrap">
<button
className="btn btn-surface btn-sm"
onClick={() => setShowPlPicker(v => !v)}
>
<ListPlus size={14} />
{t('common.bulkAddToPlaylist')}
</button>
{showPlPicker && (
<AddToPlaylistSubmenu
songIds={[...useSelectionStore.getState().selectedIds]}
onDone={() => { setShowPlPicker(false); useSelectionStore.getState().clearAll(); }}
dropDown
/>
)}
</div>
<button
className="btn btn-ghost btn-sm"
onClick={() => useSelectionStore.getState().clearAll()}
>
<X size={13} />
{t('common.bulkClear')}
</button>
</div>
)}
{/* Column visibility picker */}
<div className="tracklist-col-picker-wrapper" ref={pickerRef}>
<div className="tracklist-col-picker">
@@ -306,7 +270,17 @@ export default function FavoritesSongsTracklist({
);
case 'artist': return (
<div key="artist" className="track-artist-cell">
<span className={`track-artist${song.artistId ? ' track-artist-link' : ''}`} style={{ cursor: song.artistId ? 'pointer' : 'default' }} onClick={() => song.artistId && navigate(`/artist/${song.artistId}`)}>{song.artist}</span>
<span
className={`track-artist${song.artistId ? ' track-artist-link' : ''}`}
style={{ cursor: song.artistId ? 'pointer' : 'default' }}
onClick={(e) => {
if (!song.artistId) return;
e.stopPropagation();
navigate(`/artist/${song.artistId}`);
}}
>
{song.artist}
</span>
</div>
);
case 'album': return (
+4 -2
View File
@@ -194,6 +194,10 @@ export default function Favorites() {
starredOverrides={starredOverrides}
minYear={MIN_YEAR}
currentYear={CURRENT_YEAR}
inSelectMode={inSelectMode}
selectedCount={selectedCount}
showPlPicker={showPlPicker}
setShowPlPicker={setShowPlPicker}
/>
<FavoritesSongsTracklist
visibleSongs={visibleSongs}
@@ -201,8 +205,6 @@ export default function Favorites() {
selectedCount={selectedCount}
inSelectMode={inSelectMode}
toggleSelect={toggleSelect}
showPlPicker={showPlPicker}
setShowPlPicker={setShowPlPicker}
allColumns={FAV_COLUMNS}
visibleCols={visibleCols}
gridStyle={gridStyle}