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:
parent
526ed227c7
commit
0f7e15363b
4 changed files with 141 additions and 3 deletions
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue