refactor(import): normalize controls and classes
- let the singles action buttons use the default size again - remove redundant type="button" props from import controls - switch import page conditional classes to clsx object notation - drop route-test assertions that pinned compact auto-import sizes
This commit is contained in:
parent
9ba54bd82d
commit
7a0548ac94
5 changed files with 70 additions and 117 deletions
|
|
@ -304,13 +304,6 @@ describe('import route', () => {
|
||||||
expect(await screen.findByRole('button', { name: /^Needs Review\s*1$/ })).toBeInTheDocument();
|
expect(await screen.findByRole('button', { name: /^Needs Review\s*1$/ })).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('Album A').length).toBeGreaterThan(0);
|
expect(screen.getAllByText('Album A').length).toBeGreaterThan(0);
|
||||||
expect(screen.getByText('Watching')).toHaveAttribute('data-tone', 'success');
|
expect(screen.getByText('Watching')).toHaveAttribute('data-tone', 'success');
|
||||||
const compactFilterGroup = document.querySelector('#auto-import-results [data-size="sm"]');
|
|
||||||
expect(compactFilterGroup).toBeInTheDocument();
|
|
||||||
const intervalSelect = document.getElementById('auto-import-interval');
|
|
||||||
if (!(intervalSelect instanceof HTMLElement)) {
|
|
||||||
throw new Error('auto-import interval select missing');
|
|
||||||
}
|
|
||||||
expect(intervalSelect).toHaveAttribute('data-size', 'sm');
|
|
||||||
expect(getFetchUrls().some((url) => url.includes('/api/import/staging/groups'))).toBe(false);
|
expect(getFetchUrls().some((url) => url.includes('/api/import/staging/groups'))).toBe(false);
|
||||||
expect(getFetchUrls().some((url) => url.includes('/api/import/staging/suggestions'))).toBe(
|
expect(getFetchUrls().some((url) => url.includes('/api/import/staging/suggestions'))).toBe(
|
||||||
false,
|
false,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { type DragEvent, type KeyboardEvent, useState } from 'react';
|
import { type DragEvent, type KeyboardEvent, useState } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { Button, TextInput } from '@/components/form/form';
|
import { Button, TextInput } from '@/components/form/form';
|
||||||
|
|
||||||
import type { ImportAlbumResult } from '../-import.types';
|
import type { ImportAlbumResult } from '../-import.types';
|
||||||
import styles from './import-page.module.css';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
importStagingGroupsQueryOptions,
|
importStagingGroupsQueryOptions,
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
IMPORT_PLACEHOLDER_IMAGE,
|
IMPORT_PLACEHOLDER_IMAGE,
|
||||||
} from '../-import.helpers';
|
} from '../-import.helpers';
|
||||||
import { useAlbumImportWorkflow } from '../-import.store';
|
import { useAlbumImportWorkflow } from '../-import.store';
|
||||||
|
import styles from './import-page.module.css';
|
||||||
import {
|
import {
|
||||||
fallbackImage,
|
fallbackImage,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
|
|
@ -235,10 +236,7 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div id="import-page-album-search-section" className={clsx({ [styles.hidden]: showingMatch })}>
|
||||||
id="import-page-album-search-section"
|
|
||||||
className={showingMatch ? styles.hidden : ''}
|
|
||||||
>
|
|
||||||
{albumResults === null && (
|
{albumResults === null && (
|
||||||
<>
|
<>
|
||||||
{groups.length > 0 && (
|
{groups.length > 0 && (
|
||||||
|
|
@ -246,10 +244,9 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
|
||||||
<div className={styles.importPageSectionLabel}>Auto-Detected Albums</div>
|
<div className={styles.importPageSectionLabel}>Auto-Detected Albums</div>
|
||||||
<div className={styles.importPageAlbumGrid}>
|
<div className={styles.importPageAlbumGrid}>
|
||||||
{groups.map((group, index) => (
|
{groups.map((group, index) => (
|
||||||
<button
|
<button
|
||||||
key={`${group.artist}-${group.album}-${index}`}
|
key={`${group.artist}-${group.album}-${index}`}
|
||||||
type="button"
|
className={clsx(styles.importPageAlbumCard, styles.importPageAutoGroupCard)}
|
||||||
className={`${styles.importPageAlbumCard} ${styles.importPageAutoGroupCard}`}
|
|
||||||
onClick={() => onRunGroupSearch(group)}
|
onClick={() => onRunGroupSearch(group)}
|
||||||
>
|
>
|
||||||
<div className={styles.importPageAutoGroupCount}>{group.file_count}</div>
|
<div className={styles.importPageAutoGroupCount}>{group.file_count}</div>
|
||||||
|
|
@ -299,17 +296,16 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={albumResults === null ? styles.hidden : ''}
|
className={clsx({ [styles.hidden]: albumResults === null })}
|
||||||
id="import-page-album-clear-btn"
|
id="import-page-album-clear-btn"
|
||||||
title="Clear search"
|
title="Clear search"
|
||||||
onClick={onBackToSearch}
|
onClick={onBackToSearch}
|
||||||
>
|
>
|
||||||
x
|
x
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="primary" onClick={onRunSearch}>
|
<Button variant="primary" onClick={onRunSearch}>
|
||||||
Search
|
Search
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -333,10 +329,7 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div id="import-page-album-match-section" className={clsx({ [styles.hidden]: !showingMatch })}>
|
||||||
id="import-page-album-match-section"
|
|
||||||
className={showingMatch ? '' : styles.hidden}
|
|
||||||
>
|
|
||||||
{albumMatchLoading ? (
|
{albumMatchLoading ? (
|
||||||
<div className={styles.importPageEmptyState}>Matching files to tracklist...</div>
|
<div className={styles.importPageEmptyState}>Matching files to tracklist...</div>
|
||||||
) : albumMatchError ? (
|
) : albumMatchError ? (
|
||||||
|
|
@ -344,7 +337,9 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
|
||||||
) : albumMatch?.album ? (
|
) : albumMatch?.album ? (
|
||||||
<AlbumMatchPanel viewModel={viewModel} />
|
<AlbumMatchPanel viewModel={viewModel} />
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.importPageEmptyState}>Select an album to start matching files.</div>
|
<div className={styles.importPageEmptyState}>
|
||||||
|
Select an album to start matching files.
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
@ -359,7 +354,7 @@ function AlbumCard({
|
||||||
onSelect: (album: ImportAlbumResult) => void;
|
onSelect: (album: ImportAlbumResult) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button type="button" className={styles.importPageAlbumCard} onClick={() => onSelect(album)}>
|
<button className={styles.importPageAlbumCard} onClick={() => onSelect(album)}>
|
||||||
<img
|
<img
|
||||||
src={album.image_url || IMPORT_PLACEHOLDER_IMAGE}
|
src={album.image_url || IMPORT_PLACEHOLDER_IMAGE}
|
||||||
alt={album.name}
|
alt={album.name}
|
||||||
|
|
@ -435,20 +430,10 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
|
||||||
<div className={styles.importPageMatchHeader}>
|
<div className={styles.importPageMatchHeader}>
|
||||||
<h3>Track Matching</h3>
|
<h3>Track Matching</h3>
|
||||||
<div className={styles.importPageMatchActions}>
|
<div className={styles.importPageMatchActions}>
|
||||||
<Button
|
<Button variant="secondary" onClick={onAutoRematch}>
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={onAutoRematch}
|
|
||||||
>
|
|
||||||
Re-match Automatically
|
Re-match Automatically
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="ghost" onClick={onBackToSearch}>
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={onBackToSearch}
|
|
||||||
>
|
|
||||||
Back to Search
|
Back to Search
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -467,9 +452,10 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${trackInfo.displayTrackNumber}-${trackInfo.name}-${index}`}
|
key={`${trackInfo.displayTrackNumber}-${trackInfo.name}-${index}`}
|
||||||
className={`${styles.importPageMatchRow} ${
|
className={clsx(styles.importPageMatchRow, {
|
||||||
file ? styles.matched : ''
|
[styles.matched]: file,
|
||||||
} ${dragOverTrack === index ? styles.dragOver : ''}`}
|
[styles.dragOver]: dragOverTrack === index,
|
||||||
|
})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tapSelectedChip !== null) onTapAssign(index, tapSelectedChip);
|
if (tapSelectedChip !== null) onTapAssign(index, tapSelectedChip);
|
||||||
}}
|
}}
|
||||||
|
|
@ -488,18 +474,16 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
|
||||||
>
|
>
|
||||||
<span className={styles.importPageMatchNum}>{trackInfo.displayTrackNumber}</span>
|
<span className={styles.importPageMatchNum}>{trackInfo.displayTrackNumber}</span>
|
||||||
<span className={styles.importPageMatchTrack}>{trackInfo.name}</span>
|
<span className={styles.importPageMatchTrack}>{trackInfo.name}</span>
|
||||||
<span
|
<span className={clsx(styles.importPageMatchFile, {
|
||||||
className={`${styles.importPageMatchFile} ${
|
[styles.hasFile]: file,
|
||||||
file ? styles.hasFile : ''
|
})}>
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{file ? (
|
{file ? (
|
||||||
<>
|
<>
|
||||||
<span className={styles.importPageMatchFileName}>{file.filename}</span>
|
<span className={styles.importPageMatchFileName}>{file.filename}</span>
|
||||||
<span
|
<span
|
||||||
className={`${styles.importPageMatchConfidence} ${
|
className={clsx(styles.importPageMatchConfidence, {
|
||||||
confidence >= 0.7 ? '' : styles.low
|
[styles.low]: confidence < 0.7,
|
||||||
}`}
|
})}
|
||||||
>
|
>
|
||||||
{confidencePercent}%
|
{confidencePercent}%
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -511,7 +495,6 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
|
||||||
<span>
|
<span>
|
||||||
{file ? (
|
{file ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
|
|
@ -541,9 +524,9 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
|
||||||
key={`${file.full_path}-${index}`}
|
key={`${file.full_path}-${index}`}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={`${styles.importPageFileChip} ${
|
className={clsx(styles.importPageFileChip, {
|
||||||
tapSelectedChip === index ? styles.selected : ''
|
[styles.selected]: tapSelectedChip === index,
|
||||||
}`}
|
})}
|
||||||
draggable
|
draggable
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
@ -572,7 +555,6 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
|
||||||
{matchedCount} of {albumMatch.matches?.length ?? 0} tracks matched
|
{matchedCount} of {albumMatch.matches?.length ?? 0} tracks matched
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="primary"
|
variant="primary"
|
||||||
id="import-page-album-process-btn"
|
id="import-page-album-process-btn"
|
||||||
disabled={matchedCount === 0}
|
disabled={matchedCount === 0}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -179,9 +180,7 @@ export function AutoImportPanel({
|
||||||
<div className={styles.importPageFlexSpacer} />
|
<div className={styles.importPageFlexSpacer} />
|
||||||
{statusQuery.data?.running ? (
|
{statusQuery.data?.running ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
|
||||||
id="auto-import-scan-now"
|
id="auto-import-scan-now"
|
||||||
title="Scan import folder now"
|
title="Scan import folder now"
|
||||||
disabled={scanMutation.isPending}
|
disabled={scanMutation.isPending}
|
||||||
|
|
@ -222,7 +221,6 @@ export function AutoImportPanel({
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={saveSettingsMutation.isPending}
|
disabled={saveSettingsMutation.isPending}
|
||||||
|
|
@ -271,9 +269,7 @@ export function AutoImportPanel({
|
||||||
<div className={styles.importPageFlexSpacer} />
|
<div className={styles.importPageFlexSpacer} />
|
||||||
{counts.review > 0 ? (
|
{counts.review > 0 ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
|
||||||
id="auto-import-approve-all"
|
id="auto-import-approve-all"
|
||||||
disabled={approveAllMutation.isPending}
|
disabled={approveAllMutation.isPending}
|
||||||
onClick={() => approveAllMutation.mutate()}
|
onClick={() => approveAllMutation.mutate()}
|
||||||
|
|
@ -283,11 +279,10 @@ export function AutoImportPanel({
|
||||||
) : null}
|
) : null}
|
||||||
{counts.imported + counts.failed > 0 ? (
|
{counts.imported + counts.failed > 0 ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
|
||||||
id="auto-import-clear-completed"
|
id="auto-import-clear-completed"
|
||||||
disabled={clearMutation.isPending}
|
disabled={clearMutation.isPending}
|
||||||
|
size="sm"
|
||||||
onClick={() => clearMutation.mutate()}
|
onClick={() => clearMutation.mutate()}
|
||||||
>
|
>
|
||||||
Clear History
|
Clear History
|
||||||
|
|
@ -389,7 +384,7 @@ function AutoImportResultCard({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${styles.autoImportCard} ${statusCardClass}`}
|
className={clsx(styles.autoImportCard, statusCardClass)}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
|
|
@ -432,12 +427,12 @@ function AutoImportResultCard({
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.autoImportCardRight}>
|
<div className={styles.autoImportCardRight}>
|
||||||
<div className={`${styles.autoImportStatusBadge} ${statusBadgeClass}`}>
|
<div className={clsx(styles.autoImportStatusBadge, statusBadgeClass)}>
|
||||||
{statusMeta.icon} {statusMeta.label}
|
{statusMeta.icon} {statusMeta.label}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.autoImportConfidenceBar}>
|
<div className={styles.autoImportConfidenceBar}>
|
||||||
<div
|
<div
|
||||||
className={`${styles.autoImportConfidenceFill} ${confidenceFillClass}`}
|
className={clsx(styles.autoImportConfidenceFill, confidenceFillClass)}
|
||||||
style={{ width: `${confidencePercent}%` }}
|
style={{ width: `${confidencePercent}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -445,9 +440,7 @@ function AutoImportResultCard({
|
||||||
{result.status === 'pending_review' ? (
|
{result.status === 'pending_review' ? (
|
||||||
<div className={styles.autoImportActions}>
|
<div className={styles.autoImportActions}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
|
||||||
disabled={approvePending}
|
disabled={approvePending}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
@ -457,9 +450,7 @@ function AutoImportResultCard({
|
||||||
Approve & Import
|
Approve & Import
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
|
||||||
disabled={rejectPending}
|
disabled={rejectPending}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
@ -475,7 +466,9 @@ function AutoImportResultCard({
|
||||||
<div className={styles.autoImportCardFolderPath}>{result.folder_name}</div>
|
<div className={styles.autoImportCardFolderPath}>{result.folder_name}</div>
|
||||||
{trackDetails.length > 0 ? (
|
{trackDetails.length > 0 ? (
|
||||||
<div
|
<div
|
||||||
className={`${styles.autoImportTrackList} ${expanded ? styles.expanded : ''}`}
|
className={clsx(styles.autoImportTrackList, {
|
||||||
|
[styles.expanded]: expanded,
|
||||||
|
})}
|
||||||
id={`auto-import-tracks-${index}`}
|
id={`auto-import-tracks-${index}`}
|
||||||
>
|
>
|
||||||
<div className={styles.autoImportTrackListHeader}>
|
<div className={styles.autoImportTrackListHeader}>
|
||||||
|
|
@ -484,23 +477,21 @@ function AutoImportResultCard({
|
||||||
<span>Conf</span>
|
<span>Conf</span>
|
||||||
</div>
|
</div>
|
||||||
{trackDetails.map((track, trackIndex) => {
|
{trackDetails.map((track, trackIndex) => {
|
||||||
const rowClassName = [
|
const rowClassName = clsx(styles.autoImportTrackRow, {
|
||||||
styles.autoImportTrackRow,
|
[styles.autoImportTrackRowActive]:
|
||||||
isLiveProcessing && liveTrackIndex > 0 && trackIndex + 1 === liveTrackIndex
|
isLiveProcessing && liveTrackIndex > 0 && trackIndex + 1 === liveTrackIndex,
|
||||||
? styles.autoImportTrackRowActive
|
[styles.autoImportTrackRowDone]:
|
||||||
: '',
|
isLiveProcessing && liveTrackIndex > 0 && trackIndex + 1 < liveTrackIndex,
|
||||||
isLiveProcessing && liveTrackIndex > 0 && trackIndex + 1 < liveTrackIndex
|
});
|
||||||
? styles.autoImportTrackRowDone
|
|
||||||
: '',
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ');
|
|
||||||
return (
|
return (
|
||||||
<div key={`${track.name}-${track.file}-${trackIndex}`} className={rowClassName}>
|
<div key={`${track.name}-${track.file}-${trackIndex}`} className={rowClassName}>
|
||||||
<span className={styles.autoImportTrackName}>{track.name}</span>
|
<span className={styles.autoImportTrackName}>{track.name}</span>
|
||||||
<span className={styles.autoImportTrackFile}>{track.file}</span>
|
<span className={styles.autoImportTrackFile}>{track.file}</span>
|
||||||
<span
|
<span
|
||||||
className={`${styles.autoImportTrackConf} ${getAutoImportConfidenceClass(getConfidenceClass(track.confidence))}`}
|
className={clsx(
|
||||||
|
styles.autoImportTrackConf,
|
||||||
|
getAutoImportConfidenceClass(getConfidenceClass(track.confidence)),
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{track.confidence}%
|
{track.confidence}%
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { Link, Outlet } from '@tanstack/react-router';
|
import { Link, Outlet } from '@tanstack/react-router';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { Show } from '@/components/primitives';
|
|
||||||
import { Button } from '@/components/form/form';
|
import { Button } from '@/components/form/form';
|
||||||
|
import { Show } from '@/components/primitives';
|
||||||
import { useReactPageShell } from '@/platform/shell/route-controllers';
|
import { useReactPageShell } from '@/platform/shell/route-controllers';
|
||||||
|
|
||||||
import type { ImportQueueEntry } from '../-import.types';
|
import type { ImportQueueEntry } from '../-import.types';
|
||||||
|
|
@ -37,7 +38,7 @@ export function ImportPage() {
|
||||||
/>
|
/>
|
||||||
<ImportProcessingQueue />
|
<ImportProcessingQueue />
|
||||||
<ImportTabNav />
|
<ImportTabNav />
|
||||||
<section className={`${styles.importPageTabContent} ${styles.active}`}>
|
<section className={clsx(styles.importPageTabContent, styles.active)}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -78,7 +79,6 @@ function ImportHeader({
|
||||||
<span>Import Music</span>
|
<span>Import Music</span>
|
||||||
</h1>
|
</h1>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
title="Re-scan import folder"
|
title="Re-scan import folder"
|
||||||
aria-busy={refreshing}
|
aria-busy={refreshing}
|
||||||
|
|
@ -112,15 +112,15 @@ function ImportProcessingQueue() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={`${styles.importPageQueue} ${queue.length === 0 ? styles.hidden : ''}`}
|
className={clsx(styles.importPageQueue, {
|
||||||
|
[styles.hidden]: queue.length === 0,
|
||||||
|
})}
|
||||||
id="import-page-queue"
|
id="import-page-queue"
|
||||||
>
|
>
|
||||||
<div className={styles.importPageQueueHeader}>
|
<div className={styles.importPageQueueHeader}>
|
||||||
<span className={styles.importPageQueueTitle}>Processing</span>
|
<span className={styles.importPageQueueTitle}>Processing</span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
|
||||||
id="import-page-queue-clear"
|
id="import-page-queue-clear"
|
||||||
style={{ display: hasFinished ? undefined : 'none' }}
|
style={{ display: hasFinished ? undefined : 'none' }}
|
||||||
onClick={clearFinishedJobs}
|
onClick={clearFinishedJobs}
|
||||||
|
|
@ -139,12 +139,10 @@ function ImportProcessingQueue() {
|
||||||
|
|
||||||
function ImportQueueItem({ entry }: { entry: ImportQueueEntry }) {
|
function ImportQueueItem({ entry }: { entry: ImportQueueEntry }) {
|
||||||
const statusText = getQueueStatusText(entry);
|
const statusText = getQueueStatusText(entry);
|
||||||
const statusClass =
|
const statusClass = clsx({
|
||||||
entry.status === 'error' || (entry.status === 'done' && entry.errors.length > 0)
|
[styles.error]: entry.status === 'error' || (entry.status === 'done' && entry.errors.length > 0),
|
||||||
? styles.error
|
[styles.done]: entry.status === 'done',
|
||||||
: entry.status === 'done'
|
});
|
||||||
? styles.done
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.importPageQueueItem}>
|
<div className={styles.importPageQueueItem}>
|
||||||
|
|
@ -156,7 +154,7 @@ function ImportQueueItem({ entry }: { entry: ImportQueueEntry }) {
|
||||||
onError={fallbackImage}
|
onError={fallbackImage}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className={`${styles.importPageQueueArt} ${styles.importPageQueueArtEmpty}`}>♪</div>
|
<div className={clsx(styles.importPageQueueArt, styles.importPageQueueArtEmpty)}>♪</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.importPageQueueInfo}>
|
<div className={styles.importPageQueueInfo}>
|
||||||
<div className={styles.importPageQueueName}>{entry.label}</div>
|
<div className={styles.importPageQueueName}>{entry.label}</div>
|
||||||
|
|
@ -165,13 +163,13 @@ function ImportQueueItem({ entry }: { entry: ImportQueueEntry }) {
|
||||||
<div className={styles.importPageQueueProgress}>
|
<div className={styles.importPageQueueProgress}>
|
||||||
<div className={styles.importPageQueueBar}>
|
<div className={styles.importPageQueueBar}>
|
||||||
<div
|
<div
|
||||||
className={`${styles.importPageQueueFill} ${
|
className={clsx(styles.importPageQueueFill, {
|
||||||
entry.status === 'error' ? styles.error : ''
|
[styles.error]: entry.status === 'error',
|
||||||
}`}
|
})}
|
||||||
style={{ width: `${getQueueProgressPercent(entry)}%` }}
|
style={{ width: `${getQueueProgressPercent(entry)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${styles.importPageQueueStatus} ${statusClass}`}>{statusText}</div>
|
<div className={clsx(styles.importPageQueueStatus, statusClass)}>{statusText}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -183,7 +181,7 @@ function ImportTabNav() {
|
||||||
<Link
|
<Link
|
||||||
to="/import/auto"
|
to="/import/auto"
|
||||||
className={styles.importPageTab}
|
className={styles.importPageTab}
|
||||||
activeProps={{ className: `${styles.importPageTab} ${styles.active}` }}
|
activeProps={{ className: clsx(styles.importPageTab, styles.active) }}
|
||||||
id="import-page-tab-auto"
|
id="import-page-tab-auto"
|
||||||
>
|
>
|
||||||
Auto
|
Auto
|
||||||
|
|
@ -191,7 +189,7 @@ function ImportTabNav() {
|
||||||
<Link
|
<Link
|
||||||
to="/import/album"
|
to="/import/album"
|
||||||
className={styles.importPageTab}
|
className={styles.importPageTab}
|
||||||
activeProps={{ className: `${styles.importPageTab} ${styles.active}` }}
|
activeProps={{ className: clsx(styles.importPageTab, styles.active) }}
|
||||||
id="import-page-tab-album"
|
id="import-page-tab-album"
|
||||||
>
|
>
|
||||||
Albums
|
Albums
|
||||||
|
|
@ -199,7 +197,7 @@ function ImportTabNav() {
|
||||||
<Link
|
<Link
|
||||||
to="/import/singles"
|
to="/import/singles"
|
||||||
className={styles.importPageTab}
|
className={styles.importPageTab}
|
||||||
activeProps={{ className: `${styles.importPageTab} ${styles.active}` }}
|
activeProps={{ className: clsx(styles.importPageTab, styles.active) }}
|
||||||
id="import-page-tab-singles"
|
id="import-page-tab-singles"
|
||||||
>
|
>
|
||||||
Singles
|
Singles
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { Badge, Button, Checkbox, TextInput } from '@/components/form/form';
|
import { Badge, Button, Checkbox, TextInput } from '@/components/form/form';
|
||||||
|
|
@ -175,17 +176,15 @@ export function SinglesImportPanel({
|
||||||
<>
|
<>
|
||||||
<div className={styles.importPageSinglesHeader}>
|
<div className={styles.importPageSinglesHeader}>
|
||||||
<div className={styles.importPageSinglesActions}>
|
<div className={styles.importPageSinglesActions}>
|
||||||
<Button type="button" variant="secondary" size="sm" onClick={onSelectAll}>
|
<Button variant="secondary" onClick={onSelectAll}>
|
||||||
<span id="import-page-select-all-text">
|
<span id="import-page-select-all-text">
|
||||||
{allSelected ? 'Deselect All' : 'Select All'}
|
{allSelected ? 'Deselect All' : 'Select All'}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant={processVariant}
|
variant={processVariant}
|
||||||
id="import-page-singles-process-btn"
|
id="import-page-singles-process-btn"
|
||||||
disabled={selectedCount === 0}
|
disabled={selectedCount === 0}
|
||||||
size="sm"
|
|
||||||
onClick={onProcessSingles}
|
onClick={onProcessSingles}
|
||||||
>
|
>
|
||||||
<span>Process Selected</span>
|
<span>Process Selected</span>
|
||||||
|
|
@ -205,7 +204,9 @@ export function SinglesImportPanel({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={fileKey}
|
key={fileKey}
|
||||||
className={`${styles.importPageSingleItem} ${manualMatch ? styles.matched : ''}`}
|
className={clsx(styles.importPageSingleItem, {
|
||||||
|
[styles.matched]: manualMatch,
|
||||||
|
})}
|
||||||
data-single-key={fileKey}
|
data-single-key={fileKey}
|
||||||
>
|
>
|
||||||
<label className={styles.importPageSingleCheckboxWrap}>
|
<label className={styles.importPageSingleCheckboxWrap}>
|
||||||
|
|
@ -226,7 +227,6 @@ export function SinglesImportPanel({
|
||||||
<div className={styles.importPageSingleMatchedInfo}>
|
<div className={styles.importPageSingleMatchedInfo}>
|
||||||
✓ {manualMatch.name} - {manualMatch.artist}
|
✓ {manualMatch.name} - {manualMatch.artist}
|
||||||
<button
|
<button
|
||||||
type="button"
|
|
||||||
className={styles.importPageSingleMatchedChange}
|
className={styles.importPageSingleMatchedChange}
|
||||||
onClick={() => onOpenSearch(file)}
|
onClick={() => onOpenSearch(file)}
|
||||||
>
|
>
|
||||||
|
|
@ -236,12 +236,7 @@ export function SinglesImportPanel({
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.importPageSingleActions}>
|
<div className={styles.importPageSingleActions}>
|
||||||
<Button
|
<Button variant="secondary" size="sm" onClick={() => onOpenSearch(file)}>
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onOpenSearch(file)}
|
|
||||||
>
|
|
||||||
🔍 Identify
|
🔍 Identify
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -291,12 +286,7 @@ function SingleSearchPanel({
|
||||||
if (event.key === 'Enter') onRunSearch(fileKey, query);
|
if (event.key === 'Enter') onRunSearch(fileKey, query);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button variant="primary" onClick={() => onRunSearch(fileKey, query)}>
|
||||||
type="button"
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onRunSearch(fileKey, query)}
|
|
||||||
>
|
|
||||||
Search
|
Search
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -311,7 +301,6 @@ function SingleSearchPanel({
|
||||||
searchState?.results.map((track, index) => (
|
searchState?.results.map((track, index) => (
|
||||||
<button
|
<button
|
||||||
key={`${track.source || 'source'}-${track.id}-${index}`}
|
key={`${track.source || 'source'}-${track.id}-${index}`}
|
||||||
type="button"
|
|
||||||
className={styles.importPageSingleResultItem}
|
className={styles.importPageSingleResultItem}
|
||||||
onClick={() => onSelectMatch(fileKey, track)}
|
onClick={() => onSelectMatch(fileKey, track)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue