soulsync/webui/src/routes/import/-ui/import-page.tsx
Antti Kettunen 0d7fb91d98
refactor(webui): share import form primitives
- add shared Base UI-backed checkbox and slider primitives under the form component layer
- move the singles import checkbox and auto-import confidence slider to the shared controls
- keep the import route tests aligned with the new accessible component roles
2026-05-24 21:17:20 +03:00

210 lines
6.4 KiB
TypeScript

import { Link, Outlet } from '@tanstack/react-router';
import { Show } from '@/components/primitives';
import { Button } from '@/components/form/form';
import { useReactPageShell } from '@/platform/shell/route-controllers';
import type { ImportQueueEntry } from '../-import.types';
import {
getQueueProgressPercent,
getQueueStatusText,
getStagingStatsText,
} from '../-import.helpers';
import { useImportQueueWorkflow } from '../-import.store';
import styles from './import-page.module.css';
import { fallbackImage, RefreshIcon, useImportStaging } from './import-shared';
export function ImportPage() {
useReactPageShell('import');
const { refreshStaging, stagingFiles, stagingPath, stagingQuery } = useImportStaging();
const isRefreshing = stagingQuery.isRefetching;
const lastRefreshedAt =
stagingQuery.dataUpdatedAt > 0 ? formatShortTime(stagingQuery.dataUpdatedAt) : null;
return (
<div id="import-page" data-testid="import-page">
<div className={styles.importPageContainer}>
<ImportHeader
error={stagingQuery.error}
fileCountText={getStagingStatsText(stagingFiles)}
loading={stagingQuery.isLoading}
stagingPath={stagingPath}
refreshing={isRefreshing}
lastRefreshedAt={lastRefreshedAt}
onRefresh={refreshStaging}
/>
<ImportProcessingQueue />
<ImportTabNav />
<section className={`${styles.importPageTabContent} ${styles.active}`}>
<Outlet />
</section>
</div>
</div>
);
}
function formatShortTime(timestamp: number) {
return new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
}
function ImportHeader({
error,
fileCountText,
loading,
stagingPath,
refreshing,
lastRefreshedAt,
onRefresh,
}: {
error: unknown;
fileCountText: string;
loading: boolean;
stagingPath: string;
refreshing: boolean;
lastRefreshedAt: string | null;
onRefresh: () => void;
}) {
return (
<header className={styles.importPageHeader}>
<div className={styles.importPageTitleRow}>
<h1 className={styles.importPageTitle}>
<img src="/static/import.png" className="page-header-icon" alt="" />
<span>Import Music</span>
</h1>
<Button
type="button"
className={`${styles.importPageRefreshBtn} ${
refreshing ? styles.importPageRefreshBtnRefreshing : ''
}`}
title="Re-scan import folder"
aria-busy={refreshing}
disabled={refreshing}
onClick={onRefresh}
>
<RefreshIcon />
{refreshing ? 'Refreshing...' : 'Refresh'}
</Button>
</div>
<div className={styles.importPageStagingBar} id="import-staging-bar">
<span className={styles.importStagingPath} id="import-page-staging-path">
{error ? 'Import folder: error' : `Import: ${stagingPath}`}
</span>
<Show when={lastRefreshedAt != null}>
<span className={styles.importStagingRefreshAt}>
{lastRefreshedAt ? `Last refreshed: ${lastRefreshedAt}` : null}
</span>
</Show>
<span className={styles.importStagingStats} id="import-page-staging-stats">
{loading ? 'loading...' : fileCountText}
</span>
</div>
</header>
);
}
function ImportProcessingQueue() {
const { clearFinishedJobs, queue } = useImportQueueWorkflow();
const hasFinished = queue.some((entry) => entry.status !== 'running');
return (
<section
className={`${styles.importPageQueue} ${queue.length === 0 ? styles.hidden : ''}`}
id="import-page-queue"
>
<div className={styles.importPageQueueHeader}>
<span className={styles.importPageQueueTitle}>Processing</span>
<Button
type="button"
className={styles.importPageQueueClear}
id="import-page-queue-clear"
style={{ display: hasFinished ? undefined : 'none' }}
onClick={clearFinishedJobs}
>
Clear finished
</Button>
</div>
<div className={styles.importPageQueueList} id="import-page-queue-list">
{queue.map((entry) => (
<ImportQueueItem key={entry.id} entry={entry} />
))}
</div>
</section>
);
}
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
: '';
return (
<div className={styles.importPageQueueItem}>
{entry.imageUrl ? (
<img
className={styles.importPageQueueArt}
src={entry.imageUrl}
alt=""
onError={fallbackImage}
/>
) : (
<div className={`${styles.importPageQueueArt} ${styles.importPageQueueArtEmpty}`}></div>
)}
<div className={styles.importPageQueueInfo}>
<div className={styles.importPageQueueName}>{entry.label}</div>
<div className={styles.importPageQueueDetail}>{entry.sublabel}</div>
</div>
<div className={styles.importPageQueueProgress}>
<div className={styles.importPageQueueBar}>
<div
className={`${styles.importPageQueueFill} ${
entry.status === 'error' ? styles.error : ''
}`}
style={{ width: `${getQueueProgressPercent(entry)}%` }}
/>
</div>
<div className={`${styles.importPageQueueStatus} ${statusClass}`}>{statusText}</div>
</div>
</div>
);
}
function ImportTabNav() {
return (
<nav className={styles.importPageTabBar} aria-label="Import modes">
<Link
to="/import/auto"
className={styles.importPageTab}
activeProps={{ className: `${styles.importPageTab} ${styles.active}` }}
id="import-page-tab-auto"
>
Auto
</Link>
<Link
to="/import/album"
className={styles.importPageTab}
activeProps={{ className: `${styles.importPageTab} ${styles.active}` }}
id="import-page-tab-album"
>
Albums
</Link>
<Link
to="/import/singles"
className={styles.importPageTab}
activeProps={{ className: `${styles.importPageTab} ${styles.active}` }}
id="import-page-tab-singles"
>
Singles
</Link>
</nav>
);
}