feat(import-ui): surface quality-filter + folder-artist toggles on Import page

The two import behaviour toggles previously lived only in Settings → Import.
Mirror them onto the React Import page (above the processing queue) so they're
visible and adjustable right where you import.

- New ImportOptions component: two Switches ("Quality check on import",
  "Use folder as artist") with optimistic update + immediate save.
- API: fetchImportOptions / saveImportOptions / importOptionsQueryOptions —
  read the whole settings blob, POST a partial {import: {...}} (the settings
  endpoint partial-merges, so the rest of config is untouched). Both default ON
  when absent, matching the backend defaults.
- Same import.quality_filter_enabled / import.folder_artist_override config
  keys as the Settings page, so the two stay in sync.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dev 2026-06-15 18:34:42 +02:00
parent 526ed227c7
commit 0f7e15363b
4 changed files with 141 additions and 3 deletions

View file

@ -10,6 +10,7 @@ import type {
ImportAutoImportResultsPayload,
ImportAutoImportSettingsPayload,
ImportAutoImportStatusPayload,
ImportOptionsPayload,
ImportProcessPayload,
ImportStagingFilesPayload,
ImportStagingGroupsPayload,
@ -25,7 +26,7 @@ export const IMPORT_QUERY_KEY = ['import'] as const;
// which left the progress bar stuck at 0 and showing "Failed" while files
// imported fine (#772). Give the import-process calls a generous bound so the
// responses actually arrive and the bar advances. Scoped to import only.
const IMPORT_REQUEST_TIMEOUT_MS = 300_000; // 5 min/track
const IMPORT_REQUEST_TIMEOUT_MS = 300_000; // 5 min/track
export async function fetchImportStagingFiles(): Promise<ImportStagingFilesPayload> {
return readJson<ImportStagingFilesPayload>(apiClient.get('import/staging/files'));
@ -225,6 +226,45 @@ export function autoImportResultsQueryOptions() {
});
}
// --- Import behaviour toggles (mirrors Settings → Import) ---
// Both live under the `import.*` config namespace. GET reads the whole settings
// blob; POST /api/settings partial-merges, so we only send the import keys.
type SettingsBlob = {
import?: { quality_filter_enabled?: boolean; folder_artist_override?: boolean };
};
export async function fetchImportOptions(): Promise<ImportOptionsPayload> {
const data = await readJson<SettingsBlob>(apiClient.get('settings'));
const imp = data.import ?? {};
return {
// Both default ON when the key is absent (matches the backend defaults).
qualityFilterEnabled: imp.quality_filter_enabled !== false,
folderArtistOverride: imp.folder_artist_override !== false,
};
}
export async function saveImportOptions(
options: ImportOptionsPayload,
): Promise<{ success: boolean; error?: string }> {
return readJson<{ success: boolean; error?: string }>(
apiClient.post('settings', {
json: {
import: {
quality_filter_enabled: options.qualityFilterEnabled,
folder_artist_override: options.folderArtistOverride,
},
},
}),
);
}
export function importOptionsQueryOptions() {
return queryOptions({
queryKey: [...IMPORT_QUERY_KEY, 'import-options'],
queryFn: fetchImportOptions,
});
}
export function invalidateImportQueries(queryClient: QueryClient) {
return queryClient.invalidateQueries({ queryKey: IMPORT_QUERY_KEY });
}

View file

@ -241,3 +241,10 @@ export interface ImportSinglesQueueJob {
}
export type ImportQueueJob = ImportAlbumQueueJob | ImportSinglesQueueJob;
// The two import behaviour toggles also shown in Settings → Import, mirrored
// onto the Import page for visibility. Both map to `import.*` config keys.
export interface ImportOptionsPayload {
qualityFilterEnabled: boolean;
folderArtistOverride: boolean;
}

View file

@ -56,6 +56,25 @@
color: rgba(255, 255, 255, 0.5);
}
.importOptionsRow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 22px;
padding: 10px 16px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
}
.importOption {
display: flex;
align-items: center;
gap: 8px;
}
.importStagingPath {
flex: 1;
overflow: hidden;

View file

@ -1,12 +1,14 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Link, Outlet } from '@tanstack/react-router';
import clsx from 'clsx';
import { Button } from '@/components/form/form';
import { Button, Switch } from '@/components/form/form';
import { Show } from '@/components/primitives';
import { useReactPageShell } from '@/platform/shell/route-controllers';
import type { ImportQueueEntry } from '../-import.types';
import type { ImportOptionsPayload, ImportQueueEntry } from '../-import.types';
import { importOptionsQueryOptions, IMPORT_QUERY_KEY, saveImportOptions } from '../-import.api';
import {
getQueueProgressPercent,
getQueueStatusText,
@ -36,6 +38,7 @@ export function ImportPage() {
lastRefreshedAt={lastRefreshedAt}
onRefresh={refreshStaging}
/>
<ImportOptions />
<ImportProcessingQueue />
<ImportTabNav />
<section className={clsx(styles.importPageTabContent, styles.active)}>
@ -54,6 +57,75 @@ function formatShortTime(timestamp: number) {
});
}
// The two import behaviour toggles, also in Settings → Import, surfaced here so
// they're visible right where you import. Each writes its `import.*` config key
// on change (POST /api/settings partial-merges, so the rest of config is safe).
function ImportOptions() {
const queryClient = useQueryClient();
const optionsQuery = useQuery(importOptionsQueryOptions());
const saveMutation = useMutation({
mutationFn: saveImportOptions,
onSuccess: () => {
window.showToast?.('Import options saved', 'success');
},
onError: (error: unknown) => {
window.showToast?.(
error instanceof Error ? error.message : 'Could not save import options',
'error',
);
// Re-sync the toggles to the server's actual state after a failed save.
void queryClient.invalidateQueries({
queryKey: [...IMPORT_QUERY_KEY, 'import-options'],
});
},
});
const opts = optionsQuery.data;
if (!opts) return null;
const update = (patch: Partial<ImportOptionsPayload>) => {
const next = { ...opts, ...patch };
// Optimistic: reflect the toggle immediately, the mutation persists it.
queryClient.setQueryData([...IMPORT_QUERY_KEY, 'import-options'], next);
saveMutation.mutate(next);
};
return (
<section className={styles.importOptionsRow} data-testid="import-options">
<div className={styles.importOption}>
<Switch
id="import-quality-filter"
checked={opts.qualityFilterEnabled}
disabled={saveMutation.isPending}
aria-labelledby="import-quality-filter-label"
onCheckedChange={(checked) => update({ qualityFilterEnabled: checked })}
/>
<span
id="import-quality-filter-label"
title="Only import tracks that meet your quality profile; otherwise import everything regardless of quality."
>
Quality check on import
</span>
</div>
<div className={styles.importOption}>
<Switch
id="import-folder-artist"
checked={opts.folderArtistOverride}
disabled={saveMutation.isPending}
aria-labelledby="import-folder-artist-label"
onCheckedChange={(checked) => update({ folderArtistOverride: checked })}
/>
<span
id="import-folder-artist-label"
title="Use the top Staging folder as the album artist (good for mixtapes; turn off for mixed piles of songs)."
>
Use folder as artist
</span>
</div>
</section>
);
}
function ImportHeader({
error,
fileCountText,