feat(onboarding): Import empty states, sample folder button and guide links (#356) #382
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#382
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-356-import-empty-state"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes #356
Stacked on PR #381 (
issue-355-docs-bank-guides), itself on #380 → #379 → #378 → #377. Base branch of this PR isissue-355-docs-bank-guides; merge the stack bottom-up.What
The import page without a source no longer shows the bare "no source found" card.
ImportEmptyState(built on the sharedEmptyStatefrom #352) replaces the emptySourceListin both situations:browseFolder), link « Comment exporter un CSV depuis ma banque ? » →/settings/users#import-export(anchor from #355).folderNameis the last path segment (/and\handled, trailing separators ignored), never the full path.useImportWizardgainscreateSampleFolders()→invoke("create_sample_import_folders", { importFolder, today })withtoday= the webview's local date asYYYY-MM-DD(from the local getters, nottoISOString()), then rescans the folder so both cards show up without another click, then dispatchesSET_SAMPLE_CREATED(names). A closable success banner (same shape as the error banner,var(--positive)border) reads the names from the newsampleCreatedstate;RESETandSET_IMPORT_FOLDER(whatbrowseFolderdispatches) clear it. A refusal from Rust (sample folder already exists, nothing written) lands in the existing error banner viaSET_ERROR.Also:
PageHelpgets an optionallinkToprop rendering a<Link>under the tips labelled${helpKey}.help.linkLabel(no explicit close: navigating unmounts the page); « Parcourir » drops to the secondary style when no folder is configured so a single primary button is on screen.SourceList.tsxis untouched. No CHANGELOG change (centralised in #357). No DB migration.Why
Onboarding wave
planned-2026-10-03-onboarding-phase1: a new user landing on an empty import page had no next action and no way to try the wizard before exporting a real statement.Privacy
logInfocarries the two folder names only ([import] Sample folders created: Exemple Desjardins, Exemple Tangerine), never the absolute path — the logs are attachable to a feedback report (review caveat made mandatory in planning). Theinvokepasses only the folder and the date.Tests
src/hooks/useImportWizard.test.ts(new) on the exportedwizardReducer/initialWizardState:SET_SAMPLE_CREATEDsets the names without touching the sources;RESETandSET_IMPORT_FOLDERclear it; an error leavesscannedSourcesuntouched (same reference) and raises no banner;formatLocalDateis TZ-independent.src/components/import/ImportEmptyState.test.tsx(new), direct call withEmptyStateexpanded: both variants, button wiring, disabled create button whileisCreatingSample, guide link target,folderNameshown without the path,folderNameFromPathfor POSIX/Windows/trailing separators.npm test: 79 files, 1440 passed.npm run build(tsc + vite): green.import.*verified by script.Generated autonomously by /autopilot run of 2026-10-03
🤖 Generated with Claude Code
Self-review (autopilot, inline on the full diff of #382)
Verdict: APPROVE
Summary
9 files, +~500 / −~15, all in scope. The empty
SourceListcard is replaced byImportEmptyStatein both situations of the issue;SourceList.tsxis byte-identical. The hook's new action invokescreate_sample_import_folderswith exactly two arguments (the persisted import folder and the webview's localYYYY-MM-DD), rescans, then raises the banner. Reducer and component are covered by direct tests; 1440 vitest andnpm run buildgreen on the branch.Security / privacy
logInfocarriesnames.join(", ")— the two folder names returned by Rust — neverstate.importFolder. Checked every+line of the diff touchingimportFolder,folder_path,logInfo,invoke: the only non-test occurrences are the reducer, theinvokeargument, and thefolderNameFromPath(...)call inImportPage(which yields the last segment only, so the empty state never shows the full path either).invokepayload:{ importFolder, today }only; the Rust side already refuses a non-directory and any pre-existing sample folder before writing.Acceptance criteria
useImportWizard.test.ts:SET_SAMPLE_CREATEDsets namesresetandSET_IMPORT_FOLDERclear itRESETkeeps folder + sources,SET_IMPORT_FOLDERdrops the bannerscannedSourcesuntouchedSET_ERRORtest, same reference +sampleCreatednull +isLoadingfalseImportEmptyState.test.tsx: no-folder variant (button + guide link)onBrowse, 1Linkto/settings/users#import-exportdisabledpropagated, refresh stays enabledfolderNamewithout the full pathRelevés, assertnot.toContain("Users")scanFolderInternal(folder)runs insidecreateSampleFoldersbeforeSET_SAMPLE_CREATED; banner has anX→dismissSampleCreatedErr("Folder already exists: …")→SET_ERROR, rendered through the existingt(state.error, { defaultValue })bannerImportEmptyStatenever renderedshowEmptyState = step === "source-list" && scannedSources.length === 0npm test,npm run buildgreeni18n
FR/EN key sets under
import.*are identical (script diff empty). French sentences are the issue's verbatim. Zero JSX text literal in the four touched components (grep).import.help.linkLabelreuses the guide-link sentence — the issue gives no distinct label for the popover.Concerns (non-blocking)
getImportFolder()resolves on mount,importFolderis null andscannedSourcesempty, so the « no folder » variant shows for one tick on every visit, then flips to the list. The previous code flashed the « Aucune source trouvée » card in the same window, and the folder card above already flashes « Aucun dossier configuré »; aloadedflag would remove all three at once — out of this issue's scope, worth a small follow-up.scanFolderInternalfails after a successful write, the error banner and the success banner show together. The folders were written, so the success message is true; the error names the scan. Acceptable.isCreatingSampleis fed by the sharedstate.isLoading, so the button is also disabled during an ordinary rescan — harmless, and avoids a second loading flag.Generated autonomously by /autopilot run of 2026-10-03
Revue adversariale — PR #382 (
/pr-review, passe indépendante)Verdict : APPROVE
Revue contre la base de la PR (
issue-355-docs-bank-guides), diff complet, un seul commit624eef7au format conventionnel. Tout ce qui est hors diff et dont la PR dépend est présent sur la tête :EmptyState(#352,src/components/shared/EmptyState.tsx), la commande Rustcreate_sample_import_folders(#354,fs_commands.rs:207, enregistrée danslib.rs:442, argumentsimport_folder/today→ camelCase côtéinvokeconforme), l'ancre#import-export(#355,DocsSubsections.tsx) et la clécommon.close.SourceList.tsxest intact,import.sources.emptyreste utilisée.Ce qui a été exécuté (copie hors dépôt,
git archivede624eef7)tsc --noEmit: exit 0.vitest run: 79 fichiers, 1440 tests verts (le chiffre de la PR est exact). Aucunskip/only.import.*: aucune clé orpheline. Aucune chaîne JSX en dur dans les 4 composants touchés.PR Check — Frontendsuccess (1m59s).check-rustnon déclenché, à raison (aucun fichiersrc-tauri/**dans ce maillon).pull_refclosesvenant de cette PR → l'issue se fermera au merge.Mutations du relecteur (chacune restaurée, copie vérifiée identique à l'octet)
SET_IMPORT_FOLDERgardesampleCreatedformatLocalDate→toISOString().slice(0,10)TZ=America/Toronto— 0 / 7 sousTZ=UTCfolderNameFromPathne coupe que sur/Vie privée
logInfone porte que les deux noms retournés par Rust (constantesExemple Desjardins/Exemple Tangerine), jamais le chemin. LecatchdecreateSampleFoldersne logge rien ; les messages Rust qui contiennent le chemin (Folder does not exist: …) vont à la bannière, pas au journal. Conforme à la contrainte de planification.Suggestions (non bloquantes)
formatLocalDateest aveugle en UTC. Il est bien TZ-indépendant pour l'implémentation correcte, mais la régression qu'il vise (toISOString) ne tombe que si le processus tourne hors UTC — c'est le fuseau probable du conteneur CI. Poserprocess.env.TZ = "America/Toronto"en tête du fichier de test (ouvi.stubEnv) pour que la CI attrape ce que ma machine en EDT a attrapé.hasFolderetfolderNamesont redondants dansImportEmptyStateProps:folderName !== nullsuffirait. Une seule source de vérité évite un étathasFolder: true, folderName: nullque le composant accepte (folderName ?? "").invokeréussit mais quescanFolderInternaléchoue, il dispatchSET_ERRORen interne puisSET_SAMPLE_CREATEDest quand même dispatché → les deux bannières s'affichent ensemble. Les deux énoncés sont vrais (dossiers écrits, rescan raté), donc acceptable, mais à connaître.Folder already exists: Exemple Desjardins) arrivent en anglais brut dans la bannière viadefaultValue. Motif préexistant pour toutes les erreurs du scan ; le cas « existe déjà » est le seul refus prévisible pour un utilisateur, une clé i18n dédiée serait plus propre. Chemin peu atteignable toutefois : après un succès, le rescan fait disparaître l'empty state et le bouton avec.isCreatingSample={state.isLoading}partage le drapeau avec le scan initial et « Actualiser » : pendant le scan de montage, l'empty state « aucune source » apparaît brièvement avec « Créer » désactivé. Même comportement transitoire qu'avant (la carte vide deSourceLists'affichait aussi), pas une régression.Actions faites
status:approved(posé par la self-review de l'autopilot) → inchangé.scratchpad/pr382etpr382-fullsupprimées.Revue indépendante sur la base de la PR ; valable pour ce maillon, le tip cumulé de la pile reste à valider en local avant le merge bottom-up.