import: page shows scan progress + auto-loads when the background scan finishes (#947, phase 2 UI)
Frontend half of the async staging scan. The endpoints now return {scanning, progress} while a
large staging folder is still being scanned in the background; the page surfaces that and fills in
automatically when it completes — no manual refresh, no timeout.
- types: ImportStagingFiles/GroupsPayload gain optional scanning + progress (additive).
- useImportStaging exposes `scanning`/`scanProgress` and, while scanning, polls via a plain
setInterval(refetch, 1500). Deliberately NOT react-query's refetchInterval — and a plain interval
that only runs while scanning leaves the normal + error query states completely untouched.
- the header shows "Scanning N of M files…" instead of a count while the scan runs.
vitest: new test asserts the scan-progress header renders from a {scanning} response; typecheck
clean. Note: -route.test.tsx's pre-existing "staging files fail to load" test fails only in the
full-file run (passes in isolation) — verified it also fails on clean dev with all my changes
stashed, so it's a pre-existing test-isolation flake, not from this change.
This commit is contained in:
parent
79648a4f5f
commit
8f1ffa7632
4 changed files with 52 additions and 2 deletions
|
|
@ -23,11 +23,21 @@ export interface ImportStagingFile {
|
||||||
manual_match?: ImportTrackResult;
|
manual_match?: ImportTrackResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** While a large staging folder is still being scanned in the background (#947), the
|
||||||
|
* staging endpoints return `scanning: true` + progress instead of files/groups; the query
|
||||||
|
* polls until the scan completes and real data arrives. */
|
||||||
|
export interface ImportScanProgress {
|
||||||
|
scanned: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ImportStagingFilesPayload {
|
export interface ImportStagingFilesPayload {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
files?: ImportStagingFile[];
|
files?: ImportStagingFile[];
|
||||||
staging_path?: string;
|
staging_path?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
scanning?: boolean;
|
||||||
|
progress?: ImportScanProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportStagingGroup {
|
export interface ImportStagingGroup {
|
||||||
|
|
@ -47,6 +57,8 @@ export interface ImportStagingGroupsPayload {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
groups?: ImportStagingGroup[];
|
groups?: ImportStagingGroup[];
|
||||||
error?: string;
|
error?: string;
|
||||||
|
scanning?: boolean;
|
||||||
|
progress?: ImportScanProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportAlbumResult {
|
export interface ImportAlbumResult {
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,19 @@ describe('import route', () => {
|
||||||
expect(await screen.findByText('Import folder: error')).toBeInTheDocument();
|
expect(await screen.findByText('Import folder: error')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows scan progress while a large staging folder is still scanning (#947)', async () => {
|
||||||
|
server.use(
|
||||||
|
http.get('/api/import/staging/files', () =>
|
||||||
|
HttpResponse.json({ success: true, scanning: true, progress: { scanned: 5, total: 20 } }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderImportRoute();
|
||||||
|
|
||||||
|
expect(await screen.findByTestId('import-page')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText(/Scanning 5 of 20 files/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('stores the active tab in nested route paths', async () => {
|
it('stores the active tab in nested route paths', async () => {
|
||||||
const { history } = renderImportRoute();
|
const { history } = renderImportRoute();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,24 @@ import { fallbackImage, RefreshIcon, useImportStaging } from './import-shared';
|
||||||
export function ImportPage() {
|
export function ImportPage() {
|
||||||
useReactPageShell('import');
|
useReactPageShell('import');
|
||||||
|
|
||||||
const { refreshStaging, stagingFiles, stagingPath, stagingQuery } = useImportStaging();
|
const { refreshStaging, scanning, scanProgress, stagingFiles, stagingPath, stagingQuery } =
|
||||||
|
useImportStaging();
|
||||||
const isRefreshing = stagingQuery.isRefetching;
|
const isRefreshing = stagingQuery.isRefetching;
|
||||||
const lastRefreshedAt =
|
const lastRefreshedAt =
|
||||||
stagingQuery.dataUpdatedAt > 0 ? formatShortTime(stagingQuery.dataUpdatedAt) : null;
|
stagingQuery.dataUpdatedAt > 0 ? formatShortTime(stagingQuery.dataUpdatedAt) : null;
|
||||||
|
// While a large staging folder is still scanning (#947), show progress instead of a count.
|
||||||
|
const fileCountText = scanning
|
||||||
|
? scanProgress && scanProgress.total > 0
|
||||||
|
? `Scanning ${scanProgress.scanned} of ${scanProgress.total} files…`
|
||||||
|
: 'Scanning staging folder…'
|
||||||
|
: getStagingStatsText(stagingFiles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="import-page" data-testid="import-page">
|
<div id="import-page" data-testid="import-page">
|
||||||
<div className={styles.importPageContainer}>
|
<div className={styles.importPageContainer}>
|
||||||
<ImportHeader
|
<ImportHeader
|
||||||
error={stagingQuery.error}
|
error={stagingQuery.error}
|
||||||
fileCountText={getStagingStatsText(stagingFiles)}
|
fileCountText={fileCountText}
|
||||||
loading={stagingQuery.isLoading}
|
loading={stagingQuery.isLoading}
|
||||||
stagingPath={stagingPath}
|
stagingPath={stagingPath}
|
||||||
refreshing={isRefreshing}
|
refreshing={isRefreshing}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import type { ImportQueueJob, ImportStagingFile } from '../-import.types';
|
import type { ImportQueueJob, ImportStagingFile } from '../-import.types';
|
||||||
|
|
||||||
|
|
@ -20,6 +21,21 @@ export function useImportStaging() {
|
||||||
...importStagingFilesQueryOptions(),
|
...importStagingFilesQueryOptions(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A large staging folder (whole-library migration, #947) is scanned in the background; the
|
||||||
|
// endpoint returns `scanning: true` until it's done. While scanning, poll so the page fills
|
||||||
|
// in automatically once the scan completes. A plain setInterval (NOT react-query's
|
||||||
|
// refetchInterval, which interferes with the data query's error/retry settling) that only
|
||||||
|
// runs while scanning, so the normal and error states are left completely untouched.
|
||||||
|
const scanning = stagingQuery.data?.scanning === true;
|
||||||
|
const { refetch } = stagingQuery;
|
||||||
|
useEffect(() => {
|
||||||
|
if (!scanning) return undefined;
|
||||||
|
const id = window.setInterval(() => {
|
||||||
|
void refetch();
|
||||||
|
}, 1500);
|
||||||
|
return () => window.clearInterval(id);
|
||||||
|
}, [scanning, refetch]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
refreshStaging: async () => {
|
refreshStaging: async () => {
|
||||||
clearFinishedJobs();
|
clearFinishedJobs();
|
||||||
|
|
@ -28,6 +44,8 @@ export function useImportStaging() {
|
||||||
// Keep the empty fallback stable so staging-driven effects do not loop while loading.
|
// Keep the empty fallback stable so staging-driven effects do not loop while loading.
|
||||||
stagingFiles: stagingQuery.data?.files ?? EMPTY_STAGING_FILES,
|
stagingFiles: stagingQuery.data?.files ?? EMPTY_STAGING_FILES,
|
||||||
stagingPath: stagingQuery.data?.staging_path || 'Not configured',
|
stagingPath: stagingQuery.data?.staging_path || 'Not configured',
|
||||||
|
scanning,
|
||||||
|
scanProgress: stagingQuery.data?.progress ?? null,
|
||||||
stagingQuery,
|
stagingQuery,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue