Simpl-Resultat/src/components/import/ImportEmptyState.tsx
le king fu 11eb911657 fix(import): translate the sample folder refusals, banner after a good rescan (#384)
create_sample_import_folders refuses with stable codes instead of English
sentences: sample_folder_exists:<name>, import_folder_missing (no longer
carrying the folder's path) and invalid_date. The wizard maps them to
import.empty.sampleErrors.* through the pure sampleFolderError, the folder
name interpolated from the new errorValues of the state; any other message
stays raw. The raw refusal also goes to the Journal as a warning: a code, or
a write error naming a sample folder and the OS error, never a path.

scanFolderInternal now tells whether the scan went through, and the success
banner only follows a rescan that did: a failed rescan leaves its error
alone on screen.

ImportEmptyState keeps folderName alone (null without a folder); the page
passes "" for a folder stored as "", as hasFolder did.

Rust tests: the three codes asserted exactly, the case of the second sample
folder already there (nothing written), and {{M1}} pinned as the most
recent month line by line against the templates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:30:35 -04:00

100 lines
3.3 KiB
TypeScript

import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { FolderOpen, Inbox } from "lucide-react";
import EmptyState from "../shared/EmptyState";
import ImportFolderStructure from "./ImportFolderStructure";
/** Where the user guide explains how to export a CSV from each bank (#355). */
export const CSV_EXPORT_GUIDE_PATH = "/settings/users#import-export";
export interface ImportEmptyStateProps {
/**
* Last segment of the import folder path, never the full path; null when no
* import folder is configured. With a folder, the empty state speaks of its
* content (#384: one prop, no separate `hasFolder` to disagree with it).
*/
folderName: string | null;
onBrowse: () => void;
onCreateSample: () => void;
onRefresh: () => void;
/** The sample folders are being written: the primary button is disabled. */
isCreatingSample: boolean;
/**
* Highlight the primary action for the getting-started card (#398): "Choose
* a folder" without a folder, "Create a sample folder" with one.
*/
onboardingTarget?: boolean;
}
/**
* Last segment of a filesystem path, whichever separator the OS used, with
* trailing separators ignored (`C:\Users\max\Relevés\` → `Relevés`). The full
* path is already shown by the folder card; the empty state only names it.
*/
export function folderNameFromPath(path: string): string {
const segments = path.split(/[\\/]+/).filter((s) => s.length > 0);
return segments.length > 0 ? segments[segments.length - 1] : path;
}
/**
* Empty state of the import page (#356), replacing the empty `SourceList` in
* both of its situations: no import folder yet (choose one), or a folder with
* no account sub-folder (create the sample folders, or refresh after dropping
* files). Both variants show the expected structure of the folder (#422) and
* link to the bank CSV export guide. Pure: no hook besides `useTranslation`,
* so it is tested by direct call.
*/
export default function ImportEmptyState({
folderName,
onBrowse,
onCreateSample,
onRefresh,
isCreatingSample,
onboardingTarget,
}: ImportEmptyStateProps) {
const { t } = useTranslation();
const details = (
<>
<ImportFolderStructure />
<p className="mt-4 text-center">
<Link
to={CSV_EXPORT_GUIDE_PATH}
className="text-sm text-[var(--primary)] hover:underline"
>
{t("import.empty.guideLink")}
</Link>
</p>
</>
);
if (folderName === null) {
return (
<EmptyState
icon={<FolderOpen size={40} />}
title={t("import.empty.noFolder.title")}
description={t("import.empty.noFolder.description")}
primaryAction={{ label: t("import.empty.noFolder.cta"), onClick: onBrowse, onboardingTarget }}
>
{details}
</EmptyState>
);
}
return (
<EmptyState
icon={<Inbox size={40} />}
title={t("import.empty.noSources.title")}
description={t("import.empty.noSources.description", { folderName })}
primaryAction={{
label: t("import.empty.createSample"),
onClick: onCreateSample,
disabled: isCreatingSample,
onboardingTarget,
}}
secondaryAction={{ label: t("import.empty.refresh"), onClick: onRefresh }}
>
{details}
</EmptyState>
);
}