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:
Antti Kettunen 2026-05-18 21:11:45 +03:00
parent 9ba54bd82d
commit 7a0548ac94
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7
5 changed files with 70 additions and 117 deletions

View file

@ -304,13 +304,6 @@ describe('import route', () => {
expect(await screen.findByRole('button', { name: /^Needs Review\s*1$/ })).toBeInTheDocument();
expect(screen.getAllByText('Album A').length).toBeGreaterThan(0);
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/suggestions'))).toBe(
false,

View file

@ -1,10 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import { type DragEvent, type KeyboardEvent, useState } from 'react';
import clsx from 'clsx';
import { Button, TextInput } from '@/components/form/form';
import type { ImportAlbumResult } from '../-import.types';
import styles from './import-page.module.css';
import {
importStagingGroupsQueryOptions,
@ -20,6 +20,7 @@ import {
IMPORT_PLACEHOLDER_IMAGE,
} from '../-import.helpers';
import { useAlbumImportWorkflow } from '../-import.store';
import styles from './import-page.module.css';
import {
fallbackImage,
getErrorMessage,
@ -235,10 +236,7 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
return (
<>
<div
id="import-page-album-search-section"
className={showingMatch ? styles.hidden : ''}
>
<div id="import-page-album-search-section" className={clsx({ [styles.hidden]: showingMatch })}>
{albumResults === null && (
<>
{groups.length > 0 && (
@ -246,10 +244,9 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
<div className={styles.importPageSectionLabel}>Auto-Detected Albums</div>
<div className={styles.importPageAlbumGrid}>
{groups.map((group, index) => (
<button
<button
key={`${group.artist}-${group.album}-${index}`}
type="button"
className={`${styles.importPageAlbumCard} ${styles.importPageAutoGroupCard}`}
className={clsx(styles.importPageAlbumCard, styles.importPageAutoGroupCard)}
onClick={() => onRunGroupSearch(group)}
>
<div className={styles.importPageAutoGroupCount}>{group.file_count}</div>
@ -299,17 +296,16 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
}}
/>
<Button
type="button"
variant="ghost"
size="icon"
className={albumResults === null ? styles.hidden : ''}
className={clsx({ [styles.hidden]: albumResults === null })}
id="import-page-album-clear-btn"
title="Clear search"
onClick={onBackToSearch}
>
x
</Button>
<Button type="button" variant="primary" onClick={onRunSearch}>
<Button variant="primary" onClick={onRunSearch}>
Search
</Button>
</div>
@ -333,10 +329,7 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
</div>
</div>
<div
id="import-page-album-match-section"
className={showingMatch ? '' : styles.hidden}
>
<div id="import-page-album-match-section" className={clsx({ [styles.hidden]: !showingMatch })}>
{albumMatchLoading ? (
<div className={styles.importPageEmptyState}>Matching files to tracklist...</div>
) : albumMatchError ? (
@ -344,7 +337,9 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
) : albumMatch?.album ? (
<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>
</>
@ -359,7 +354,7 @@ function AlbumCard({
onSelect: (album: ImportAlbumResult) => void;
}) {
return (
<button type="button" className={styles.importPageAlbumCard} onClick={() => onSelect(album)}>
<button className={styles.importPageAlbumCard} onClick={() => onSelect(album)}>
<img
src={album.image_url || IMPORT_PLACEHOLDER_IMAGE}
alt={album.name}
@ -435,20 +430,10 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
<div className={styles.importPageMatchHeader}>
<h3>Track Matching</h3>
<div className={styles.importPageMatchActions}>
<Button
type="button"
variant="secondary"
size="sm"
onClick={onAutoRematch}
>
<Button variant="secondary" onClick={onAutoRematch}>
Re-match Automatically
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={onBackToSearch}
>
<Button variant="ghost" onClick={onBackToSearch}>
Back to Search
</Button>
</div>
@ -467,9 +452,10 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
return (
<div
key={`${trackInfo.displayTrackNumber}-${trackInfo.name}-${index}`}
className={`${styles.importPageMatchRow} ${
file ? styles.matched : ''
} ${dragOverTrack === index ? styles.dragOver : ''}`}
className={clsx(styles.importPageMatchRow, {
[styles.matched]: file,
[styles.dragOver]: dragOverTrack === index,
})}
onClick={() => {
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.importPageMatchTrack}>{trackInfo.name}</span>
<span
className={`${styles.importPageMatchFile} ${
file ? styles.hasFile : ''
}`}
>
<span className={clsx(styles.importPageMatchFile, {
[styles.hasFile]: file,
})}>
{file ? (
<>
<span className={styles.importPageMatchFileName}>{file.filename}</span>
<span
className={`${styles.importPageMatchConfidence} ${
confidence >= 0.7 ? '' : styles.low
}`}
className={clsx(styles.importPageMatchConfidence, {
[styles.low]: confidence < 0.7,
})}
>
{confidencePercent}%
</span>
@ -511,7 +495,6 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
<span>
{file ? (
<Button
type="button"
variant="ghost"
size="icon"
onClick={(event) => {
@ -541,9 +524,9 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
key={`${file.full_path}-${index}`}
role="button"
tabIndex={0}
className={`${styles.importPageFileChip} ${
tapSelectedChip === index ? styles.selected : ''
}`}
className={clsx(styles.importPageFileChip, {
[styles.selected]: tapSelectedChip === index,
})}
draggable
onClick={(event) => {
event.stopPropagation();
@ -572,7 +555,6 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
{matchedCount} of {albumMatch.matches?.length ?? 0} tracks matched
</div>
<Button
type="button"
variant="primary"
id="import-page-album-process-btn"
disabled={matchedCount === 0}

View file

@ -1,4 +1,5 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import {
@ -179,9 +180,7 @@ export function AutoImportPanel({
<div className={styles.importPageFlexSpacer} />
{statusQuery.data?.running ? (
<Button
type="button"
variant="secondary"
size="sm"
id="auto-import-scan-now"
title="Scan import folder now"
disabled={scanMutation.isPending}
@ -222,7 +221,6 @@ export function AutoImportPanel({
</Select>
</div>
<Button
type="button"
variant="primary"
size="sm"
disabled={saveSettingsMutation.isPending}
@ -271,9 +269,7 @@ export function AutoImportPanel({
<div className={styles.importPageFlexSpacer} />
{counts.review > 0 ? (
<Button
type="button"
variant="secondary"
size="sm"
id="auto-import-approve-all"
disabled={approveAllMutation.isPending}
onClick={() => approveAllMutation.mutate()}
@ -283,11 +279,10 @@ export function AutoImportPanel({
) : null}
{counts.imported + counts.failed > 0 ? (
<Button
type="button"
variant="ghost"
size="sm"
id="auto-import-clear-completed"
disabled={clearMutation.isPending}
size="sm"
onClick={() => clearMutation.mutate()}
>
Clear History
@ -389,7 +384,7 @@ function AutoImportResultCard({
return (
<div
className={`${styles.autoImportCard} ${statusCardClass}`}
className={clsx(styles.autoImportCard, statusCardClass)}
role="button"
tabIndex={0}
onClick={onToggle}
@ -432,12 +427,12 @@ function AutoImportResultCard({
) : null}
</div>
<div className={styles.autoImportCardRight}>
<div className={`${styles.autoImportStatusBadge} ${statusBadgeClass}`}>
<div className={clsx(styles.autoImportStatusBadge, statusBadgeClass)}>
{statusMeta.icon} {statusMeta.label}
</div>
<div className={styles.autoImportConfidenceBar}>
<div
className={`${styles.autoImportConfidenceFill} ${confidenceFillClass}`}
className={clsx(styles.autoImportConfidenceFill, confidenceFillClass)}
style={{ width: `${confidencePercent}%` }}
/>
</div>
@ -445,9 +440,7 @@ function AutoImportResultCard({
{result.status === 'pending_review' ? (
<div className={styles.autoImportActions}>
<Button
type="button"
variant="primary"
size="sm"
disabled={approvePending}
onClick={(event) => {
event.stopPropagation();
@ -457,9 +450,7 @@ function AutoImportResultCard({
Approve & Import
</Button>
<Button
type="button"
variant="secondary"
size="sm"
disabled={rejectPending}
onClick={(event) => {
event.stopPropagation();
@ -475,7 +466,9 @@ function AutoImportResultCard({
<div className={styles.autoImportCardFolderPath}>{result.folder_name}</div>
{trackDetails.length > 0 ? (
<div
className={`${styles.autoImportTrackList} ${expanded ? styles.expanded : ''}`}
className={clsx(styles.autoImportTrackList, {
[styles.expanded]: expanded,
})}
id={`auto-import-tracks-${index}`}
>
<div className={styles.autoImportTrackListHeader}>
@ -484,23 +477,21 @@ function AutoImportResultCard({
<span>Conf</span>
</div>
{trackDetails.map((track, trackIndex) => {
const rowClassName = [
styles.autoImportTrackRow,
isLiveProcessing && liveTrackIndex > 0 && trackIndex + 1 === liveTrackIndex
? styles.autoImportTrackRowActive
: '',
isLiveProcessing && liveTrackIndex > 0 && trackIndex + 1 < liveTrackIndex
? styles.autoImportTrackRowDone
: '',
]
.filter(Boolean)
.join(' ');
const rowClassName = clsx(styles.autoImportTrackRow, {
[styles.autoImportTrackRowActive]:
isLiveProcessing && liveTrackIndex > 0 && trackIndex + 1 === liveTrackIndex,
[styles.autoImportTrackRowDone]:
isLiveProcessing && liveTrackIndex > 0 && trackIndex + 1 < liveTrackIndex,
});
return (
<div key={`${track.name}-${track.file}-${trackIndex}`} className={rowClassName}>
<span className={styles.autoImportTrackName}>{track.name}</span>
<span className={styles.autoImportTrackFile}>{track.file}</span>
<span
className={`${styles.autoImportTrackConf} ${getAutoImportConfidenceClass(getConfidenceClass(track.confidence))}`}
className={clsx(
styles.autoImportTrackConf,
getAutoImportConfidenceClass(getConfidenceClass(track.confidence)),
)}
>
{track.confidence}%
</span>

View file

@ -1,7 +1,8 @@
import { Link, Outlet } from '@tanstack/react-router';
import clsx from 'clsx';
import { Show } from '@/components/primitives';
import { Button } from '@/components/form/form';
import { Show } from '@/components/primitives';
import { useReactPageShell } from '@/platform/shell/route-controllers';
import type { ImportQueueEntry } from '../-import.types';
@ -37,7 +38,7 @@ export function ImportPage() {
/>
<ImportProcessingQueue />
<ImportTabNav />
<section className={`${styles.importPageTabContent} ${styles.active}`}>
<section className={clsx(styles.importPageTabContent, styles.active)}>
<Outlet />
</section>
</div>
@ -78,7 +79,6 @@ function ImportHeader({
<span>Import Music</span>
</h1>
<Button
type="button"
variant="secondary"
title="Re-scan import folder"
aria-busy={refreshing}
@ -112,15 +112,15 @@ function ImportProcessingQueue() {
return (
<section
className={`${styles.importPageQueue} ${queue.length === 0 ? styles.hidden : ''}`}
className={clsx(styles.importPageQueue, {
[styles.hidden]: queue.length === 0,
})}
id="import-page-queue"
>
<div className={styles.importPageQueueHeader}>
<span className={styles.importPageQueueTitle}>Processing</span>
<Button
type="button"
variant="ghost"
size="sm"
id="import-page-queue-clear"
style={{ display: hasFinished ? undefined : 'none' }}
onClick={clearFinishedJobs}
@ -139,12 +139,10 @@ function ImportProcessingQueue() {
function ImportQueueItem({ entry }: { entry: ImportQueueEntry }) {
const statusText = getQueueStatusText(entry);
const statusClass =
entry.status === 'error' || (entry.status === 'done' && entry.errors.length > 0)
? styles.error
: entry.status === 'done'
? styles.done
: '';
const statusClass = clsx({
[styles.error]: entry.status === 'error' || (entry.status === 'done' && entry.errors.length > 0),
[styles.done]: entry.status === 'done',
});
return (
<div className={styles.importPageQueueItem}>
@ -156,7 +154,7 @@ function ImportQueueItem({ entry }: { entry: ImportQueueEntry }) {
onError={fallbackImage}
/>
) : (
<div className={`${styles.importPageQueueArt} ${styles.importPageQueueArtEmpty}`}></div>
<div className={clsx(styles.importPageQueueArt, styles.importPageQueueArtEmpty)}></div>
)}
<div className={styles.importPageQueueInfo}>
<div className={styles.importPageQueueName}>{entry.label}</div>
@ -165,13 +163,13 @@ function ImportQueueItem({ entry }: { entry: ImportQueueEntry }) {
<div className={styles.importPageQueueProgress}>
<div className={styles.importPageQueueBar}>
<div
className={`${styles.importPageQueueFill} ${
entry.status === 'error' ? styles.error : ''
}`}
className={clsx(styles.importPageQueueFill, {
[styles.error]: entry.status === 'error',
})}
style={{ width: `${getQueueProgressPercent(entry)}%` }}
/>
</div>
<div className={`${styles.importPageQueueStatus} ${statusClass}`}>{statusText}</div>
<div className={clsx(styles.importPageQueueStatus, statusClass)}>{statusText}</div>
</div>
</div>
);
@ -183,7 +181,7 @@ function ImportTabNav() {
<Link
to="/import/auto"
className={styles.importPageTab}
activeProps={{ className: `${styles.importPageTab} ${styles.active}` }}
activeProps={{ className: clsx(styles.importPageTab, styles.active) }}
id="import-page-tab-auto"
>
Auto
@ -191,7 +189,7 @@ function ImportTabNav() {
<Link
to="/import/album"
className={styles.importPageTab}
activeProps={{ className: `${styles.importPageTab} ${styles.active}` }}
activeProps={{ className: clsx(styles.importPageTab, styles.active) }}
id="import-page-tab-album"
>
Albums
@ -199,7 +197,7 @@ function ImportTabNav() {
<Link
to="/import/singles"
className={styles.importPageTab}
activeProps={{ className: `${styles.importPageTab} ${styles.active}` }}
activeProps={{ className: clsx(styles.importPageTab, styles.active) }}
id="import-page-tab-singles"
>
Singles

View file

@ -1,3 +1,4 @@
import clsx from 'clsx';
import { useEffect } from 'react';
import { Badge, Button, Checkbox, TextInput } from '@/components/form/form';
@ -175,17 +176,15 @@ export function SinglesImportPanel({
<>
<div className={styles.importPageSinglesHeader}>
<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">
{allSelected ? 'Deselect All' : 'Select All'}
</span>
</Button>
<Button
type="button"
variant={processVariant}
id="import-page-singles-process-btn"
disabled={selectedCount === 0}
size="sm"
onClick={onProcessSingles}
>
<span>Process Selected</span>
@ -205,7 +204,9 @@ export function SinglesImportPanel({
return (
<div
key={fileKey}
className={`${styles.importPageSingleItem} ${manualMatch ? styles.matched : ''}`}
className={clsx(styles.importPageSingleItem, {
[styles.matched]: manualMatch,
})}
data-single-key={fileKey}
>
<label className={styles.importPageSingleCheckboxWrap}>
@ -226,7 +227,6 @@ export function SinglesImportPanel({
<div className={styles.importPageSingleMatchedInfo}>
{manualMatch.name} - {manualMatch.artist}
<button
type="button"
className={styles.importPageSingleMatchedChange}
onClick={() => onOpenSearch(file)}
>
@ -236,12 +236,7 @@ export function SinglesImportPanel({
) : null}
</div>
<div className={styles.importPageSingleActions}>
<Button
type="button"
variant="secondary"
size="sm"
onClick={() => onOpenSearch(file)}
>
<Button variant="secondary" size="sm" onClick={() => onOpenSearch(file)}>
🔍 Identify
</Button>
</div>
@ -291,12 +286,7 @@ function SingleSearchPanel({
if (event.key === 'Enter') onRunSearch(fileKey, query);
}}
/>
<Button
type="button"
variant="primary"
size="sm"
onClick={() => onRunSearch(fileKey, query)}
>
<Button variant="primary" onClick={() => onRunSearch(fileKey, query)}>
Search
</Button>
</div>
@ -311,7 +301,6 @@ function SingleSearchPanel({
searchState?.results.map((track, index) => (
<button
key={`${track.source || 'source'}-${track.id}-${index}`}
type="button"
className={styles.importPageSingleResultItem}
onClick={() => onSelectMatch(fileKey, track)}
>