All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m57s
The Import empty state now shows an "Expected structure" box in both of its variants (no folder, folder without sources): the parent folder to choose, one subfolder per account, and files at its root ignored, as scan_import_folder reads them. New ImportFolderStructure component, a nested list with decorative icons. The getting-started cards ask for the parent folder and one subfolder per account instead of one per bank. The preview card says that an expense must count among the outflows and names "Flip the signs" next to "Check duplicates": WIZARD_ROWS rows now carry every label their sentence names. Every Import card sentence quotes the controls it names, in French and in English, pinned by a test that reads the locale files. Help tips, docs.import.steps, the user guide, the QA list and architecture.md follow; CHANGELOG entry under Changed. Resolves #422 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
3.2 KiB
TypeScript
99 lines
3.2 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 {
|
|
/** An import folder is configured; the empty state then speaks of its content. */
|
|
hasFolder: boolean;
|
|
/** Last segment of the import folder path, never the full path. */
|
|
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({
|
|
hasFolder,
|
|
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 (!hasFolder) {
|
|
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: folderName ?? "" })}
|
|
primaryAction={{
|
|
label: t("import.empty.createSample"),
|
|
onClick: onCreateSample,
|
|
disabled: isCreatingSample,
|
|
onboardingTarget,
|
|
}}
|
|
secondaryAction={{ label: t("import.empty.refresh"), onClick: onRefresh }}
|
|
>
|
|
{details}
|
|
</EmptyState>
|
|
);
|
|
}
|