refactor(webui): key singles import state by file

- replace index-based singles selection and search state with stable staging file keys\n- keep refreshes from shifting selected rows or open search panels when files are inserted or reordered\n- add a regression test that proves selection stays attached to the intended file across refreshes
This commit is contained in:
Antti Kettunen 2026-05-16 19:01:43 +03:00
parent 89252cf6e4
commit aa86bedc6e
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7
5 changed files with 177 additions and 92 deletions

View file

@ -11,6 +11,10 @@ import type {
export const IMPORT_PLACEHOLDER_IMAGE = '/static/placeholder.png'; export const IMPORT_PLACEHOLDER_IMAGE = '/static/placeholder.png';
export function getStagingFileKey(file: ImportStagingFile): string {
return file.full_path;
}
export function formatImportBytes(bytes: number): string { export function formatImportBytes(bytes: number): string {
if (bytes > 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`; if (bytes > 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`;
if (bytes > 1_048_576) return `${(bytes / 1_048_576).toFixed(0)} MB`; if (bytes > 1_048_576) return `${(bytes / 1_048_576).toFixed(0)} MB`;

View file

@ -7,8 +7,10 @@ import type {
ImportAlbumResult, ImportAlbumResult,
ImportQueueEntry, ImportQueueEntry,
ImportQueueJob, ImportQueueJob,
ImportStagingFile,
ImportTrackResult, ImportTrackResult,
} from './-import.types'; } from './-import.types';
import { getStagingFileKey } from './-import.helpers';
export type SingleSearchState = { export type SingleSearchState = {
query: string; query: string;
@ -37,10 +39,10 @@ function createInitialWorkflowState() {
albumMatchError: null as string | null, albumMatchError: null as string | null,
albumMatchLoading: false, albumMatchLoading: false,
matchOverrides: {} as Record<number, number>, matchOverrides: {} as Record<number, number>,
selectedSingles: new Set<number>(), selectedSingles: new Set<string>(),
singlesManualMatches: {} as Record<number, ImportTrackResult>, singlesManualMatches: {} as Record<string, ImportTrackResult>,
openSingleSearch: null as number | null, openSingleSearch: null as string | null,
singleSearches: {} as Record<number, SingleSearchState>, singleSearches: {} as Record<string, SingleSearchState>,
}; };
} }
@ -111,34 +113,53 @@ export const useImportWorkflowStore = create(
setMatchOverrides: (updater: StateUpdater<Record<number, number>>) => { setMatchOverrides: (updater: StateUpdater<Record<number, number>>) => {
set((state) => ({ matchOverrides: resolveState(state.matchOverrides, updater) })); set((state) => ({ matchOverrides: resolveState(state.matchOverrides, updater) }));
}, },
toggleSingle: (index: number) => { toggleSingle: (fileKey: string) => {
set((state) => { set((state) => {
const selectedSingles = new Set(state.selectedSingles); const selectedSingles = new Set(state.selectedSingles);
if (selectedSingles.has(index)) selectedSingles.delete(index); if (selectedSingles.has(fileKey)) selectedSingles.delete(fileKey);
else selectedSingles.add(index); else selectedSingles.add(fileKey);
return { selectedSingles }; return { selectedSingles };
}); });
}, },
toggleAllSingles: (fileCount: number) => { toggleAllSingles: (stagingFiles: ImportStagingFile[]) => {
set((state) => ({ set((state) => ({
selectedSingles: selectedSingles: (() => {
state.selectedSingles.size === fileCount const fileKeys = stagingFiles.map(getStagingFileKey);
? new Set<number>() return state.selectedSingles.size === fileKeys.length &&
: new Set(Array.from({ length: fileCount }, (_, index) => index)), fileKeys.every((key) => state.selectedSingles.has(key))
? new Set<string>()
: new Set(fileKeys);
})(),
})); }));
}, },
clearSinglesSelection: () => { clearSinglesSelection: () => {
set({ set({
selectedSingles: new Set<number>(), selectedSingles: new Set<string>(),
singlesManualMatches: {}, singlesManualMatches: {},
}); });
}, },
setOpenSingleSearch: (openSingleSearch: number | null) => set({ openSingleSearch }), syncSinglesWorkflow: (stagingFiles: ImportStagingFile[]) => {
ensureSingleSearch: (index: number, query: string) => { const validKeys = new Set(stagingFiles.map(getStagingFileKey));
set((state) => ({
selectedSingles: new Set([...state.selectedSingles].filter((key) => validKeys.has(key))),
singlesManualMatches: Object.fromEntries(
Object.entries(state.singlesManualMatches).filter(([key]) => validKeys.has(key)),
),
openSingleSearch:
state.openSingleSearch && validKeys.has(state.openSingleSearch)
? state.openSingleSearch
: null,
singleSearches: Object.fromEntries(
Object.entries(state.singleSearches).filter(([key]) => validKeys.has(key)),
),
}));
},
setOpenSingleSearch: (openSingleSearch: string | null) => set({ openSingleSearch }),
ensureSingleSearch: (fileKey: string, query: string) => {
set((state) => ({ set((state) => ({
singleSearches: { singleSearches: {
...state.singleSearches, ...state.singleSearches,
[index]: state.singleSearches[index] ?? { [fileKey]: state.singleSearches[fileKey] ?? {
query, query,
loading: false, loading: false,
error: null, error: null,
@ -147,9 +168,9 @@ export const useImportWorkflowStore = create(
}, },
})); }));
}, },
setSingleSearch: (index: number, updater: StateUpdater<SingleSearchState>) => { setSingleSearch: (fileKey: string, updater: StateUpdater<SingleSearchState>) => {
set((state) => { set((state) => {
const current = state.singleSearches[index] ?? { const current = state.singleSearches[fileKey] ?? {
query: '', query: '',
loading: false, loading: false,
error: null, error: null,
@ -158,15 +179,15 @@ export const useImportWorkflowStore = create(
return { return {
singleSearches: { singleSearches: {
...state.singleSearches, ...state.singleSearches,
[index]: resolveState(current, updater), [fileKey]: resolveState(current, updater),
}, },
}; };
}); });
}, },
selectSingleMatch: (fileIndex: number, track: ImportTrackResult) => { selectSingleMatch: (fileKey: string, track: ImportTrackResult) => {
set((state) => ({ set((state) => ({
singlesManualMatches: { ...state.singlesManualMatches, [fileIndex]: track }, singlesManualMatches: { ...state.singlesManualMatches, [fileKey]: track },
selectedSingles: new Set(state.selectedSingles).add(fileIndex), selectedSingles: new Set(state.selectedSingles).add(fileKey),
openSingleSearch: null, openSingleSearch: null,
})); }));
}, },
@ -229,6 +250,7 @@ export function useSinglesImportWorkflow() {
setSingleSearch: state.setSingleSearch, setSingleSearch: state.setSingleSearch,
singleSearches: state.singleSearches, singleSearches: state.singleSearches,
singlesManualMatches: state.singlesManualMatches, singlesManualMatches: state.singlesManualMatches,
syncSinglesWorkflow: state.syncSinglesWorkflow,
toggleAllSingles: state.toggleAllSingles, toggleAllSingles: state.toggleAllSingles,
toggleSingleInStore: state.toggleSingle, toggleSingleInStore: state.toggleSingle,
})), })),

View file

@ -7,6 +7,7 @@ import type { ShellBridge, ShellPageId } from '@/platform/shell/bridge';
import { createAppQueryClient } from '@/app/query-client'; import { createAppQueryClient } from '@/app/query-client';
import { AppRouterProvider, createAppRouter } from '@/app/router'; import { AppRouterProvider, createAppRouter } from '@/app/router';
import type { ImportStagingFile } from './-import.types';
import { resetImportWorkflowStore } from './-import.store'; import { resetImportWorkflowStore } from './-import.store';
function createResponse(body: unknown, status = 200) { function createResponse(body: unknown, status = 200) {
@ -54,9 +55,30 @@ function getFetchUrls() {
describe('import route', () => { describe('import route', () => {
let albumMatchBodies: Record<string, unknown>[]; let albumMatchBodies: Record<string, unknown>[];
let stagingFilesPayload: ImportStagingFile[];
beforeEach(() => { beforeEach(() => {
albumMatchBodies = []; albumMatchBodies = [];
stagingFilesPayload = [
{
filename: '01-track.flac',
rel_path: 'Album/01-track.flac',
full_path: '/music/Staging/Album/01-track.flac',
title: 'Track One',
artist: 'Artist A',
album: 'Album A',
extension: '.flac',
},
{
filename: '02-track.flac',
rel_path: 'Album/02-track.flac',
full_path: '/music/Staging/Album/02-track.flac',
title: 'Track Two',
artist: 'Artist A',
album: 'Album A',
extension: '.flac',
},
];
resetImportWorkflowStore(); resetImportWorkflowStore();
window.SoulSyncWebShellBridge = createShellBridge(); window.SoulSyncWebShellBridge = createShellBridge();
window.showToast = vi.fn(); window.showToast = vi.fn();
@ -71,17 +93,7 @@ describe('import route', () => {
return createResponse({ return createResponse({
success: true, success: true,
staging_path: '/music/Staging', staging_path: '/music/Staging',
files: [ files: stagingFilesPayload,
{
filename: '01-track.flac',
rel_path: 'Album/01-track.flac',
full_path: '/music/Staging/Album/01-track.flac',
title: 'Track One',
artist: 'Artist A',
album: 'Album A',
extension: '.flac',
},
],
}); });
} }
@ -243,6 +255,38 @@ describe('import route', () => {
expect(await screen.findByDisplayValue('half matched album')).toBeInTheDocument(); expect(await screen.findByDisplayValue('half matched album')).toBeInTheDocument();
}); });
it('keeps singles selection tied to file identity across refreshes', async () => {
renderImportRoute(['/import/singles']);
const secondTrack = await screen.findByLabelText('Select 02-track.flac');
fireEvent.click(secondTrack);
stagingFilesPayload = [
{
filename: '00-intro.flac',
rel_path: 'Album/00-intro.flac',
full_path: '/music/Staging/Album/00-intro.flac',
title: 'Intro',
artist: 'Artist A',
album: 'Album A',
extension: '.flac',
},
...stagingFilesPayload,
];
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }));
await waitFor(() =>
expect((screen.getByLabelText('Select 02-track.flac') as HTMLInputElement).checked).toBe(
true,
),
);
expect((screen.getByLabelText('Select 01-track.flac') as HTMLInputElement).checked).toBe(
false,
);
expect(screen.getByText('Process Selected (1)')).toBeInTheDocument();
});
it('preserves album source details when matching an album', async () => { it('preserves album source details when matching an album', async () => {
renderImportRoute(); renderImportRoute();

View file

@ -629,7 +629,7 @@
} }
.importPageSingleCheckboxWrap { .importPageSingleCheckboxWrap {
grid-area: checkbox; grid-column: 1;
position: relative; position: relative;
width: 18px; width: 18px;
height: 18px; height: 18px;
@ -685,6 +685,7 @@
} }
.importPageSingleInfo { .importPageSingleInfo {
grid-column: 2;
min-width: 0; min-width: 0;
} }
@ -729,6 +730,8 @@
} }
.importPageSingleActions { .importPageSingleActions {
grid-column: 3;
justify-self: end;
display: flex; display: flex;
gap: 6px; gap: 6px;
align-items: center; align-items: center;
@ -1084,6 +1087,10 @@
gap: 8px; gap: 8px;
} }
.importPageSingleCheckboxWrap {
grid-area: checkbox;
}
.importPageSingleInfo { .importPageSingleInfo {
grid-area: info; grid-area: info;
} }

View file

@ -1,10 +1,12 @@
import { useEffect } from 'react';
import type { SingleSearchState } from '../-import.store'; import type { SingleSearchState } from '../-import.store';
import type { ImportTrackResult } from '../-import.types'; import type { ImportTrackResult } from '../-import.types';
import type { ImportStagingFile } from '../-import.types'; import type { ImportStagingFile } from '../-import.types';
import styles from './import-page.module.css'; import styles from './import-page.module.css';
import { searchImportTracks } from '../-import.api'; import { searchImportTracks } from '../-import.api';
import { formatDuration } from '../-import.helpers'; import { formatDuration, getStagingFileKey } from '../-import.helpers';
import { useSinglesImportWorkflow } from '../-import.store'; import { useSinglesImportWorkflow } from '../-import.store';
import { import {
fallbackImage, fallbackImage,
@ -26,32 +28,37 @@ export function SinglesImportTab() {
setSingleSearch, setSingleSearch,
singleSearches, singleSearches,
singlesManualMatches, singlesManualMatches,
syncSinglesWorkflow,
toggleAllSingles, toggleAllSingles,
toggleSingleInStore, toggleSingleInStore,
} = useSinglesImportWorkflow(); } = useSinglesImportWorkflow();
const openSingleSearchPanel = (index: number) => { useEffect(() => {
if (openSingleSearch === index) { syncSinglesWorkflow(stagingFiles);
}, [stagingFiles, syncSinglesWorkflow]);
const openSingleSearchPanel = (file: ImportStagingFile) => {
const fileKey = getStagingFileKey(file);
if (openSingleSearch === fileKey) {
setOpenSingleSearch(null); setOpenSingleSearch(null);
return; return;
} }
setOpenSingleSearch(index); setOpenSingleSearch(fileKey);
const file = stagingFiles[index];
const defaultQuery = const defaultQuery =
[file?.artist, file?.title].filter(Boolean).join(' ') || [file?.artist, file?.title].filter(Boolean).join(' ') ||
(file?.filename || '').replace(/\.[^.]+$/, ''); (file?.filename || '').replace(/\.[^.]+$/, '');
ensureSingleSearch(index, defaultQuery); ensureSingleSearch(fileKey, defaultQuery);
if (defaultQuery && !singleSearches[index]?.results.length) { if (defaultQuery && !singleSearches[fileKey]?.results.length) {
void runSingleSearch(index, defaultQuery); void runSingleSearch(fileKey, defaultQuery);
} }
}; };
const runSingleSearch = async (index: number, query: string) => { const runSingleSearch = async (fileKey: string, query: string) => {
const trimmed = query.trim(); const trimmed = query.trim();
if (!trimmed) return; if (!trimmed) return;
setSingleSearch(index, (current) => ({ setSingleSearch(fileKey, (current) => ({
query: trimmed, query: trimmed,
loading: true, loading: true,
error: null, error: null,
@ -60,14 +67,14 @@ export function SinglesImportTab() {
try { try {
const payload = await searchImportTracks(trimmed); const payload = await searchImportTracks(trimmed);
setSingleSearch(index, { setSingleSearch(fileKey, {
query: trimmed, query: trimmed,
loading: false, loading: false,
error: null, error: null,
results: payload.tracks ?? [], results: payload.tracks ?? [],
}); });
} catch (error) { } catch (error) {
setSingleSearch(index, { setSingleSearch(fileKey, {
query: trimmed, query: trimmed,
loading: false, loading: false,
error: getErrorMessage(error), error: getErrorMessage(error),
@ -76,16 +83,15 @@ export function SinglesImportTab() {
} }
}; };
const selectSingleMatch = (fileIndex: number, track: ImportTrackResult) => { const selectSingleMatch = (fileKey: string, track: ImportTrackResult) => {
selectSingleMatchInStore(fileIndex, track); selectSingleMatchInStore(fileKey, track);
}; };
const processSingles = () => { const processSingles = () => {
if (selectedSingles.size === 0) return; const filesToProcess = stagingFiles.flatMap((file) => {
const filesToProcess = Array.from(selectedSingles).flatMap((index) => { const fileKey = getStagingFileKey(file);
const file = stagingFiles[index]; if (!selectedSingles.has(fileKey)) return [];
if (!file) return []; const manualMatch = singlesManualMatches[fileKey];
const manualMatch = singlesManualMatches[index];
return manualMatch ? [{ ...file, manual_match: manualMatch }] : [file]; return manualMatch ? [{ ...file, manual_match: manualMatch }] : [file];
}); });
@ -111,21 +117,21 @@ export function SinglesImportTab() {
<SinglesImportPanel <SinglesImportPanel
files={stagingFiles} files={stagingFiles}
manualMatches={singlesManualMatches} manualMatches={singlesManualMatches}
openSearchIndex={openSingleSearch} openSearchKey={openSingleSearch}
searchStates={singleSearches} searchStates={singleSearches}
selected={selectedSingles} selected={selectedSingles}
onOpenSearch={openSingleSearchPanel} onOpenSearch={openSingleSearchPanel}
onProcessSingles={processSingles} onProcessSingles={processSingles}
onRunSearch={runSingleSearch} onRunSearch={runSingleSearch}
onSearchQueryChange={(index, query) => { onSearchQueryChange={(fileKey, query) => {
setSingleSearch(index, (current) => ({ setSingleSearch(fileKey, (current) => ({
query, query,
loading: current.loading, loading: current.loading,
error: current.error, error: current.error,
results: current.results, results: current.results,
})); }));
}} }}
onSelectAll={() => toggleAllSingles(stagingFiles.length)} onSelectAll={() => toggleAllSingles(stagingFiles)}
onSelectMatch={selectSingleMatch} onSelectMatch={selectSingleMatch}
onToggleSingle={toggleSingleInStore} onToggleSingle={toggleSingleInStore}
/> />
@ -135,7 +141,7 @@ export function SinglesImportTab() {
export function SinglesImportPanel({ export function SinglesImportPanel({
files, files,
manualMatches, manualMatches,
openSearchIndex, openSearchKey,
searchStates, searchStates,
selected, selected,
onOpenSearch, onOpenSearch,
@ -147,19 +153,20 @@ export function SinglesImportPanel({
onToggleSingle, onToggleSingle,
}: { }: {
files: ImportStagingFile[]; files: ImportStagingFile[];
manualMatches: Record<number, ImportTrackResult>; manualMatches: Record<string, ImportTrackResult>;
openSearchIndex: number | null; openSearchKey: string | null;
searchStates: Record<number, SingleSearchState>; searchStates: Record<string, SingleSearchState>;
selected: Set<number>; selected: Set<string>;
onOpenSearch: (index: number) => void; onOpenSearch: (file: ImportStagingFile) => void;
onProcessSingles: () => void; onProcessSingles: () => void;
onRunSearch: (index: number, query: string) => void; onRunSearch: (fileKey: string, query: string) => void;
onSearchQueryChange: (index: number, query: string) => void; onSearchQueryChange: (fileKey: string, query: string) => void;
onSelectAll: () => void; onSelectAll: () => void;
onSelectMatch: (fileIndex: number, track: ImportTrackResult) => void; onSelectMatch: (fileKey: string, track: ImportTrackResult) => void;
onToggleSingle: (index: number) => void; onToggleSingle: (fileKey: string) => void;
}) { }) {
const allSelected = files.length > 0 && selected.size === files.length; const selectedCount = files.filter((file) => selected.has(getStagingFileKey(file))).length;
const allSelected = files.length > 0 && selectedCount === files.length;
return ( return (
<> <>
@ -174,28 +181,29 @@ export function SinglesImportPanel({
type="button" type="button"
className={styles.importPageProcessBtn} className={styles.importPageProcessBtn}
id="import-page-singles-process-btn" id="import-page-singles-process-btn"
disabled={selected.size === 0} disabled={selectedCount === 0}
onClick={onProcessSingles} onClick={onProcessSingles}
> >
Process Selected ({selected.size}) Process Selected ({selectedCount})
</button> </button>
</div> </div>
</div> </div>
<div className={styles.importPageSinglesList} id="import-page-singles-list"> <div className={styles.importPageSinglesList} id="import-page-singles-list">
{files.length === 0 ? ( {files.length === 0 ? (
<div className={styles.importPageEmptyState}>No audio files found in import folder</div> <div className={styles.importPageEmptyState}>No audio files found in import folder</div>
) : ( ) : (
files.map((file, index) => { files.map((file) => {
const manualMatch = manualMatches[index]; const fileKey = getStagingFileKey(file);
const isSelected = selected.has(index); const manualMatch = manualMatches[fileKey];
const searchState = searchStates[index]; const isSelected = selected.has(fileKey);
const searchState = searchStates[fileKey];
return ( return (
<div <div
key={`${file.full_path}-${index}`} key={fileKey}
className={`${styles.importPageSingleItem} ${ className={`${styles.importPageSingleItem} ${
manualMatch ? styles.matched : '' manualMatch ? styles.matched : ''
}`} }`}
data-single-idx={index} data-single-key={fileKey}
> >
<label className={styles.importPageSingleCheckboxWrap}> <label className={styles.importPageSingleCheckboxWrap}>
<input <input
@ -203,7 +211,7 @@ export function SinglesImportPanel({
aria-label={`Select ${file.filename}`} aria-label={`Select ${file.filename}`}
className={styles.importPageSingleCheckboxInput} className={styles.importPageSingleCheckboxInput}
checked={isSelected} checked={isSelected}
onChange={() => onToggleSingle(index)} onChange={() => onToggleSingle(fileKey)}
/> />
<span className={styles.importPageSingleCheckbox} aria-hidden="true" /> <span className={styles.importPageSingleCheckbox} aria-hidden="true" />
</label> </label>
@ -220,7 +228,7 @@ export function SinglesImportPanel({
<button <button
type="button" type="button"
className={styles.importPageSingleMatchedChange} className={styles.importPageSingleMatchedChange}
onClick={() => onOpenSearch(index)} onClick={() => onOpenSearch(file)}
> >
change change
</button> </button>
@ -231,14 +239,14 @@ export function SinglesImportPanel({
<button <button
type="button" type="button"
className={styles.importPageIdentifyBtn} className={styles.importPageIdentifyBtn}
onClick={() => onOpenSearch(index)} onClick={() => onOpenSearch(file)}
> >
🔍 Identify 🔍 Identify
</button> </button>
</div> </div>
{openSearchIndex === index ? ( {openSearchKey === fileKey ? (
<SingleSearchPanel <SingleSearchPanel
fileIndex={index} fileKey={fileKey}
searchState={searchState} searchState={searchState}
onQueryChange={onSearchQueryChange} onQueryChange={onSearchQueryChange}
onRunSearch={onRunSearch} onRunSearch={onRunSearch}
@ -255,17 +263,17 @@ export function SinglesImportPanel({
} }
function SingleSearchPanel({ function SingleSearchPanel({
fileIndex, fileKey,
searchState, searchState,
onQueryChange, onQueryChange,
onRunSearch, onRunSearch,
onSelectMatch, onSelectMatch,
}: { }: {
fileIndex: number; fileKey: string;
searchState: SingleSearchState | undefined; searchState: SingleSearchState | undefined;
onQueryChange: (index: number, query: string) => void; onQueryChange: (fileKey: string, query: string) => void;
onRunSearch: (index: number, query: string) => void; onRunSearch: (fileKey: string, query: string) => void;
onSelectMatch: (fileIndex: number, track: ImportTrackResult) => void; onSelectMatch: (fileKey: string, track: ImportTrackResult) => void;
}) { }) {
const query = searchState?.query ?? ''; const query = searchState?.query ?? '';
@ -277,20 +285,20 @@ function SingleSearchPanel({
className={styles.importPageSingleSearchInput} className={styles.importPageSingleSearchInput}
value={query} value={query}
placeholder="Search artist - title..." placeholder="Search artist - title..."
onChange={(event) => onQueryChange(fileIndex, event.target.value)} onChange={(event) => onQueryChange(fileKey, event.target.value)}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter') onRunSearch(fileIndex, query); if (event.key === 'Enter') onRunSearch(fileKey, query);
}} }}
/> />
<button <button
type="button" type="button"
className={styles.importPageSingleSearchGo} className={styles.importPageSingleSearchGo}
onClick={() => onRunSearch(fileIndex, query)} onClick={() => onRunSearch(fileKey, query)}
> >
Search Search
</button> </button>
</div> </div>
<div className={styles.importPageSingleSearchResults} id={`import-single-results-${fileIndex}`}> <div className={styles.importPageSingleSearchResults}>
{searchState?.loading ? ( {searchState?.loading ? (
<div className={styles.importPageEmptyState}>Searching...</div> <div className={styles.importPageEmptyState}>Searching...</div>
) : searchState?.error ? ( ) : searchState?.error ? (
@ -303,7 +311,7 @@ function SingleSearchPanel({
key={`${track.source || 'source'}-${track.id}-${index}`} key={`${track.source || 'source'}-${track.id}-${index}`}
type="button" type="button"
className={styles.importPageSingleResultItem} className={styles.importPageSingleResultItem}
onClick={() => onSelectMatch(fileIndex, track)} onClick={() => onSelectMatch(fileKey, track)}
> >
{track.image_url ? ( {track.image_url ? (
<img <img