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>
100 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|