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:
parent
89252cf6e4
commit
aa86bedc6e
5 changed files with 177 additions and 92 deletions
|
|
@ -11,6 +11,10 @@ import type {
|
|||
|
||||
export const IMPORT_PLACEHOLDER_IMAGE = '/static/placeholder.png';
|
||||
|
||||
export function getStagingFileKey(file: ImportStagingFile): string {
|
||||
return file.full_path;
|
||||
}
|
||||
|
||||
export function formatImportBytes(bytes: number): string {
|
||||
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`;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import type {
|
|||
ImportAlbumResult,
|
||||
ImportQueueEntry,
|
||||
ImportQueueJob,
|
||||
ImportStagingFile,
|
||||
ImportTrackResult,
|
||||
} from './-import.types';
|
||||
import { getStagingFileKey } from './-import.helpers';
|
||||
|
||||
export type SingleSearchState = {
|
||||
query: string;
|
||||
|
|
@ -37,10 +39,10 @@ function createInitialWorkflowState() {
|
|||
albumMatchError: null as string | null,
|
||||
albumMatchLoading: false,
|
||||
matchOverrides: {} as Record<number, number>,
|
||||
selectedSingles: new Set<number>(),
|
||||
singlesManualMatches: {} as Record<number, ImportTrackResult>,
|
||||
openSingleSearch: null as number | null,
|
||||
singleSearches: {} as Record<number, SingleSearchState>,
|
||||
selectedSingles: new Set<string>(),
|
||||
singlesManualMatches: {} as Record<string, ImportTrackResult>,
|
||||
openSingleSearch: null as string | null,
|
||||
singleSearches: {} as Record<string, SingleSearchState>,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -111,34 +113,53 @@ export const useImportWorkflowStore = create(
|
|||
setMatchOverrides: (updater: StateUpdater<Record<number, number>>) => {
|
||||
set((state) => ({ matchOverrides: resolveState(state.matchOverrides, updater) }));
|
||||
},
|
||||
toggleSingle: (index: number) => {
|
||||
toggleSingle: (fileKey: string) => {
|
||||
set((state) => {
|
||||
const selectedSingles = new Set(state.selectedSingles);
|
||||
if (selectedSingles.has(index)) selectedSingles.delete(index);
|
||||
else selectedSingles.add(index);
|
||||
if (selectedSingles.has(fileKey)) selectedSingles.delete(fileKey);
|
||||
else selectedSingles.add(fileKey);
|
||||
return { selectedSingles };
|
||||
});
|
||||
},
|
||||
toggleAllSingles: (fileCount: number) => {
|
||||
toggleAllSingles: (stagingFiles: ImportStagingFile[]) => {
|
||||
set((state) => ({
|
||||
selectedSingles:
|
||||
state.selectedSingles.size === fileCount
|
||||
? new Set<number>()
|
||||
: new Set(Array.from({ length: fileCount }, (_, index) => index)),
|
||||
selectedSingles: (() => {
|
||||
const fileKeys = stagingFiles.map(getStagingFileKey);
|
||||
return state.selectedSingles.size === fileKeys.length &&
|
||||
fileKeys.every((key) => state.selectedSingles.has(key))
|
||||
? new Set<string>()
|
||||
: new Set(fileKeys);
|
||||
})(),
|
||||
}));
|
||||
},
|
||||
clearSinglesSelection: () => {
|
||||
set({
|
||||
selectedSingles: new Set<number>(),
|
||||
selectedSingles: new Set<string>(),
|
||||
singlesManualMatches: {},
|
||||
});
|
||||
},
|
||||
setOpenSingleSearch: (openSingleSearch: number | null) => set({ openSingleSearch }),
|
||||
ensureSingleSearch: (index: number, query: string) => {
|
||||
syncSinglesWorkflow: (stagingFiles: ImportStagingFile[]) => {
|
||||
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) => ({
|
||||
singleSearches: {
|
||||
...state.singleSearches,
|
||||
[index]: state.singleSearches[index] ?? {
|
||||
[fileKey]: state.singleSearches[fileKey] ?? {
|
||||
query,
|
||||
loading: false,
|
||||
error: null,
|
||||
|
|
@ -147,9 +168,9 @@ export const useImportWorkflowStore = create(
|
|||
},
|
||||
}));
|
||||
},
|
||||
setSingleSearch: (index: number, updater: StateUpdater<SingleSearchState>) => {
|
||||
setSingleSearch: (fileKey: string, updater: StateUpdater<SingleSearchState>) => {
|
||||
set((state) => {
|
||||
const current = state.singleSearches[index] ?? {
|
||||
const current = state.singleSearches[fileKey] ?? {
|
||||
query: '',
|
||||
loading: false,
|
||||
error: null,
|
||||
|
|
@ -158,15 +179,15 @@ export const useImportWorkflowStore = create(
|
|||
return {
|
||||
singleSearches: {
|
||||
...state.singleSearches,
|
||||
[index]: resolveState(current, updater),
|
||||
[fileKey]: resolveState(current, updater),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
selectSingleMatch: (fileIndex: number, track: ImportTrackResult) => {
|
||||
selectSingleMatch: (fileKey: string, track: ImportTrackResult) => {
|
||||
set((state) => ({
|
||||
singlesManualMatches: { ...state.singlesManualMatches, [fileIndex]: track },
|
||||
selectedSingles: new Set(state.selectedSingles).add(fileIndex),
|
||||
singlesManualMatches: { ...state.singlesManualMatches, [fileKey]: track },
|
||||
selectedSingles: new Set(state.selectedSingles).add(fileKey),
|
||||
openSingleSearch: null,
|
||||
}));
|
||||
},
|
||||
|
|
@ -229,6 +250,7 @@ export function useSinglesImportWorkflow() {
|
|||
setSingleSearch: state.setSingleSearch,
|
||||
singleSearches: state.singleSearches,
|
||||
singlesManualMatches: state.singlesManualMatches,
|
||||
syncSinglesWorkflow: state.syncSinglesWorkflow,
|
||||
toggleAllSingles: state.toggleAllSingles,
|
||||
toggleSingleInStore: state.toggleSingle,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { ShellBridge, ShellPageId } from '@/platform/shell/bridge';
|
|||
import { createAppQueryClient } from '@/app/query-client';
|
||||
import { AppRouterProvider, createAppRouter } from '@/app/router';
|
||||
|
||||
import type { ImportStagingFile } from './-import.types';
|
||||
import { resetImportWorkflowStore } from './-import.store';
|
||||
|
||||
function createResponse(body: unknown, status = 200) {
|
||||
|
|
@ -54,9 +55,30 @@ function getFetchUrls() {
|
|||
|
||||
describe('import route', () => {
|
||||
let albumMatchBodies: Record<string, unknown>[];
|
||||
let stagingFilesPayload: ImportStagingFile[];
|
||||
|
||||
beforeEach(() => {
|
||||
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();
|
||||
window.SoulSyncWebShellBridge = createShellBridge();
|
||||
window.showToast = vi.fn();
|
||||
|
|
@ -71,17 +93,7 @@ describe('import route', () => {
|
|||
return createResponse({
|
||||
success: true,
|
||||
staging_path: '/music/Staging',
|
||||
files: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
files: stagingFilesPayload,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -243,6 +255,38 @@ describe('import route', () => {
|
|||
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 () => {
|
||||
renderImportRoute();
|
||||
|
||||
|
|
|
|||
|
|
@ -629,7 +629,7 @@
|
|||
}
|
||||
|
||||
.importPageSingleCheckboxWrap {
|
||||
grid-area: checkbox;
|
||||
grid-column: 1;
|
||||
position: relative;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
|
@ -685,6 +685,7 @@
|
|||
}
|
||||
|
||||
.importPageSingleInfo {
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
|
@ -729,6 +730,8 @@
|
|||
}
|
||||
|
||||
.importPageSingleActions {
|
||||
grid-column: 3;
|
||||
justify-self: end;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
|
|
@ -1084,6 +1087,10 @@
|
|||
gap: 8px;
|
||||
}
|
||||
|
||||
.importPageSingleCheckboxWrap {
|
||||
grid-area: checkbox;
|
||||
}
|
||||
|
||||
.importPageSingleInfo {
|
||||
grid-area: info;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
import type { SingleSearchState } from '../-import.store';
|
||||
import type { ImportTrackResult } from '../-import.types';
|
||||
import type { ImportStagingFile } from '../-import.types';
|
||||
|
||||
import styles from './import-page.module.css';
|
||||
import { searchImportTracks } from '../-import.api';
|
||||
import { formatDuration } from '../-import.helpers';
|
||||
import { formatDuration, getStagingFileKey } from '../-import.helpers';
|
||||
import { useSinglesImportWorkflow } from '../-import.store';
|
||||
import {
|
||||
fallbackImage,
|
||||
|
|
@ -26,32 +28,37 @@ export function SinglesImportTab() {
|
|||
setSingleSearch,
|
||||
singleSearches,
|
||||
singlesManualMatches,
|
||||
syncSinglesWorkflow,
|
||||
toggleAllSingles,
|
||||
toggleSingleInStore,
|
||||
} = useSinglesImportWorkflow();
|
||||
|
||||
const openSingleSearchPanel = (index: number) => {
|
||||
if (openSingleSearch === index) {
|
||||
useEffect(() => {
|
||||
syncSinglesWorkflow(stagingFiles);
|
||||
}, [stagingFiles, syncSinglesWorkflow]);
|
||||
|
||||
const openSingleSearchPanel = (file: ImportStagingFile) => {
|
||||
const fileKey = getStagingFileKey(file);
|
||||
if (openSingleSearch === fileKey) {
|
||||
setOpenSingleSearch(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setOpenSingleSearch(index);
|
||||
const file = stagingFiles[index];
|
||||
setOpenSingleSearch(fileKey);
|
||||
const defaultQuery =
|
||||
[file?.artist, file?.title].filter(Boolean).join(' ') ||
|
||||
(file?.filename || '').replace(/\.[^.]+$/, '');
|
||||
ensureSingleSearch(index, defaultQuery);
|
||||
if (defaultQuery && !singleSearches[index]?.results.length) {
|
||||
void runSingleSearch(index, defaultQuery);
|
||||
ensureSingleSearch(fileKey, defaultQuery);
|
||||
if (defaultQuery && !singleSearches[fileKey]?.results.length) {
|
||||
void runSingleSearch(fileKey, defaultQuery);
|
||||
}
|
||||
};
|
||||
|
||||
const runSingleSearch = async (index: number, query: string) => {
|
||||
const runSingleSearch = async (fileKey: string, query: string) => {
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
setSingleSearch(index, (current) => ({
|
||||
setSingleSearch(fileKey, (current) => ({
|
||||
query: trimmed,
|
||||
loading: true,
|
||||
error: null,
|
||||
|
|
@ -60,14 +67,14 @@ export function SinglesImportTab() {
|
|||
|
||||
try {
|
||||
const payload = await searchImportTracks(trimmed);
|
||||
setSingleSearch(index, {
|
||||
setSingleSearch(fileKey, {
|
||||
query: trimmed,
|
||||
loading: false,
|
||||
error: null,
|
||||
results: payload.tracks ?? [],
|
||||
});
|
||||
} catch (error) {
|
||||
setSingleSearch(index, {
|
||||
setSingleSearch(fileKey, {
|
||||
query: trimmed,
|
||||
loading: false,
|
||||
error: getErrorMessage(error),
|
||||
|
|
@ -76,16 +83,15 @@ export function SinglesImportTab() {
|
|||
}
|
||||
};
|
||||
|
||||
const selectSingleMatch = (fileIndex: number, track: ImportTrackResult) => {
|
||||
selectSingleMatchInStore(fileIndex, track);
|
||||
const selectSingleMatch = (fileKey: string, track: ImportTrackResult) => {
|
||||
selectSingleMatchInStore(fileKey, track);
|
||||
};
|
||||
|
||||
const processSingles = () => {
|
||||
if (selectedSingles.size === 0) return;
|
||||
const filesToProcess = Array.from(selectedSingles).flatMap((index) => {
|
||||
const file = stagingFiles[index];
|
||||
if (!file) return [];
|
||||
const manualMatch = singlesManualMatches[index];
|
||||
const filesToProcess = stagingFiles.flatMap((file) => {
|
||||
const fileKey = getStagingFileKey(file);
|
||||
if (!selectedSingles.has(fileKey)) return [];
|
||||
const manualMatch = singlesManualMatches[fileKey];
|
||||
return manualMatch ? [{ ...file, manual_match: manualMatch }] : [file];
|
||||
});
|
||||
|
||||
|
|
@ -111,21 +117,21 @@ export function SinglesImportTab() {
|
|||
<SinglesImportPanel
|
||||
files={stagingFiles}
|
||||
manualMatches={singlesManualMatches}
|
||||
openSearchIndex={openSingleSearch}
|
||||
openSearchKey={openSingleSearch}
|
||||
searchStates={singleSearches}
|
||||
selected={selectedSingles}
|
||||
onOpenSearch={openSingleSearchPanel}
|
||||
onProcessSingles={processSingles}
|
||||
onRunSearch={runSingleSearch}
|
||||
onSearchQueryChange={(index, query) => {
|
||||
setSingleSearch(index, (current) => ({
|
||||
onSearchQueryChange={(fileKey, query) => {
|
||||
setSingleSearch(fileKey, (current) => ({
|
||||
query,
|
||||
loading: current.loading,
|
||||
error: current.error,
|
||||
results: current.results,
|
||||
}));
|
||||
}}
|
||||
onSelectAll={() => toggleAllSingles(stagingFiles.length)}
|
||||
onSelectAll={() => toggleAllSingles(stagingFiles)}
|
||||
onSelectMatch={selectSingleMatch}
|
||||
onToggleSingle={toggleSingleInStore}
|
||||
/>
|
||||
|
|
@ -135,7 +141,7 @@ export function SinglesImportTab() {
|
|||
export function SinglesImportPanel({
|
||||
files,
|
||||
manualMatches,
|
||||
openSearchIndex,
|
||||
openSearchKey,
|
||||
searchStates,
|
||||
selected,
|
||||
onOpenSearch,
|
||||
|
|
@ -147,19 +153,20 @@ export function SinglesImportPanel({
|
|||
onToggleSingle,
|
||||
}: {
|
||||
files: ImportStagingFile[];
|
||||
manualMatches: Record<number, ImportTrackResult>;
|
||||
openSearchIndex: number | null;
|
||||
searchStates: Record<number, SingleSearchState>;
|
||||
selected: Set<number>;
|
||||
onOpenSearch: (index: number) => void;
|
||||
manualMatches: Record<string, ImportTrackResult>;
|
||||
openSearchKey: string | null;
|
||||
searchStates: Record<string, SingleSearchState>;
|
||||
selected: Set<string>;
|
||||
onOpenSearch: (file: ImportStagingFile) => void;
|
||||
onProcessSingles: () => void;
|
||||
onRunSearch: (index: number, query: string) => void;
|
||||
onSearchQueryChange: (index: number, query: string) => void;
|
||||
onRunSearch: (fileKey: string, query: string) => void;
|
||||
onSearchQueryChange: (fileKey: string, query: string) => void;
|
||||
onSelectAll: () => void;
|
||||
onSelectMatch: (fileIndex: number, track: ImportTrackResult) => void;
|
||||
onToggleSingle: (index: number) => void;
|
||||
onSelectMatch: (fileKey: string, track: ImportTrackResult) => 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 (
|
||||
<>
|
||||
|
|
@ -174,28 +181,29 @@ export function SinglesImportPanel({
|
|||
type="button"
|
||||
className={styles.importPageProcessBtn}
|
||||
id="import-page-singles-process-btn"
|
||||
disabled={selected.size === 0}
|
||||
disabled={selectedCount === 0}
|
||||
onClick={onProcessSingles}
|
||||
>
|
||||
Process Selected ({selected.size})
|
||||
Process Selected ({selectedCount})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.importPageSinglesList} id="import-page-singles-list">
|
||||
<div className={styles.importPageSinglesList} id="import-page-singles-list">
|
||||
{files.length === 0 ? (
|
||||
<div className={styles.importPageEmptyState}>No audio files found in import folder</div>
|
||||
) : (
|
||||
files.map((file, index) => {
|
||||
const manualMatch = manualMatches[index];
|
||||
const isSelected = selected.has(index);
|
||||
const searchState = searchStates[index];
|
||||
files.map((file) => {
|
||||
const fileKey = getStagingFileKey(file);
|
||||
const manualMatch = manualMatches[fileKey];
|
||||
const isSelected = selected.has(fileKey);
|
||||
const searchState = searchStates[fileKey];
|
||||
return (
|
||||
<div
|
||||
key={`${file.full_path}-${index}`}
|
||||
key={fileKey}
|
||||
className={`${styles.importPageSingleItem} ${
|
||||
manualMatch ? styles.matched : ''
|
||||
}`}
|
||||
data-single-idx={index}
|
||||
data-single-key={fileKey}
|
||||
>
|
||||
<label className={styles.importPageSingleCheckboxWrap}>
|
||||
<input
|
||||
|
|
@ -203,7 +211,7 @@ export function SinglesImportPanel({
|
|||
aria-label={`Select ${file.filename}`}
|
||||
className={styles.importPageSingleCheckboxInput}
|
||||
checked={isSelected}
|
||||
onChange={() => onToggleSingle(index)}
|
||||
onChange={() => onToggleSingle(fileKey)}
|
||||
/>
|
||||
<span className={styles.importPageSingleCheckbox} aria-hidden="true" />
|
||||
</label>
|
||||
|
|
@ -220,7 +228,7 @@ export function SinglesImportPanel({
|
|||
<button
|
||||
type="button"
|
||||
className={styles.importPageSingleMatchedChange}
|
||||
onClick={() => onOpenSearch(index)}
|
||||
onClick={() => onOpenSearch(file)}
|
||||
>
|
||||
change
|
||||
</button>
|
||||
|
|
@ -231,14 +239,14 @@ export function SinglesImportPanel({
|
|||
<button
|
||||
type="button"
|
||||
className={styles.importPageIdentifyBtn}
|
||||
onClick={() => onOpenSearch(index)}
|
||||
onClick={() => onOpenSearch(file)}
|
||||
>
|
||||
🔍 Identify
|
||||
</button>
|
||||
</div>
|
||||
{openSearchIndex === index ? (
|
||||
{openSearchKey === fileKey ? (
|
||||
<SingleSearchPanel
|
||||
fileIndex={index}
|
||||
fileKey={fileKey}
|
||||
searchState={searchState}
|
||||
onQueryChange={onSearchQueryChange}
|
||||
onRunSearch={onRunSearch}
|
||||
|
|
@ -255,17 +263,17 @@ export function SinglesImportPanel({
|
|||
}
|
||||
|
||||
function SingleSearchPanel({
|
||||
fileIndex,
|
||||
fileKey,
|
||||
searchState,
|
||||
onQueryChange,
|
||||
onRunSearch,
|
||||
onSelectMatch,
|
||||
}: {
|
||||
fileIndex: number;
|
||||
fileKey: string;
|
||||
searchState: SingleSearchState | undefined;
|
||||
onQueryChange: (index: number, query: string) => void;
|
||||
onRunSearch: (index: number, query: string) => void;
|
||||
onSelectMatch: (fileIndex: number, track: ImportTrackResult) => void;
|
||||
onQueryChange: (fileKey: string, query: string) => void;
|
||||
onRunSearch: (fileKey: string, query: string) => void;
|
||||
onSelectMatch: (fileKey: string, track: ImportTrackResult) => void;
|
||||
}) {
|
||||
const query = searchState?.query ?? '';
|
||||
|
||||
|
|
@ -277,20 +285,20 @@ function SingleSearchPanel({
|
|||
className={styles.importPageSingleSearchInput}
|
||||
value={query}
|
||||
placeholder="Search artist - title..."
|
||||
onChange={(event) => onQueryChange(fileIndex, event.target.value)}
|
||||
onChange={(event) => onQueryChange(fileKey, event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') onRunSearch(fileIndex, query);
|
||||
if (event.key === 'Enter') onRunSearch(fileKey, query);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.importPageSingleSearchGo}
|
||||
onClick={() => onRunSearch(fileIndex, query)}
|
||||
onClick={() => onRunSearch(fileKey, query)}
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.importPageSingleSearchResults} id={`import-single-results-${fileIndex}`}>
|
||||
<div className={styles.importPageSingleSearchResults}>
|
||||
{searchState?.loading ? (
|
||||
<div className={styles.importPageEmptyState}>Searching...</div>
|
||||
) : searchState?.error ? (
|
||||
|
|
@ -303,7 +311,7 @@ function SingleSearchPanel({
|
|||
key={`${track.source || 'source'}-${track.id}-${index}`}
|
||||
type="button"
|
||||
className={styles.importPageSingleResultItem}
|
||||
onClick={() => onSelectMatch(fileIndex, track)}
|
||||
onClick={() => onSelectMatch(fileKey, track)}
|
||||
>
|
||||
{track.image_url ? (
|
||||
<img
|
||||
|
|
|
|||
Loading…
Reference in a new issue