Fix transaction importer account preselect (#396)

## Summary
- only auto-select the sole import account during onboarding
- hide the upload-step Back button after onboarding auto-select
- add component coverage for account selection and hidden back button

## Tests
- npm test --
resources/js/components/transactions/import-step-account.test.tsx
resources/js/components/transactions/import-step-upload.test.tsx
- npx eslint
resources/js/components/transactions/import-step-account.tsx
resources/js/components/transactions/import-step-upload.tsx
resources/js/components/transactions/import-transactions-drawer.tsx
resources/js/components/onboarding/step-import-transactions.tsx
resources/js/lib/import-config-storage.ts resources/js/types/import.ts
resources/js/components/transactions/import-step-account.test.tsx
resources/js/components/transactions/import-step-upload.test.tsx

## Notes
- npm run types still fails on existing unrelated project-wide
TypeScript errors.
This commit is contained in:
Víctor Falcón 2026-05-14 11:38:44 +01:00 committed by GitHub
parent 4f55ced837
commit fc419c6cae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 147 additions and 16 deletions

View File

@ -150,6 +150,7 @@ export function StepImportTransactions({
categories={categories}
banks={banks}
automationRules={automationRules}
autoSelectSingleAccount
/>
</div>
);

View File

@ -0,0 +1,64 @@
import type { Account } from '@/types/account';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { ImportStepAccount } from './import-step-account';
const account: Account = {
id: 'account-1',
name: 'Checking',
name_iv: null,
encrypted: false,
bank: null,
type: 'checking',
currency_code: 'USD',
banking_connection_id: null,
external_account_id: null,
linked_at: null,
};
describe('ImportStepAccount', () => {
it('shows a single account without auto-selecting by default', () => {
const onAccountSelect = vi.fn();
const onNext = vi.fn();
render(
<ImportStepAccount
accounts={[account]}
selectedAccountId={null}
onAccountSelect={onAccountSelect}
onNext={onNext}
/>,
);
expect(screen.getByText('Checking')).not.toBeNull();
expect(screen.getByRole('button', { name: 'Next' })).toHaveProperty(
'disabled',
true,
);
expect(onAccountSelect).not.toHaveBeenCalled();
expect(onNext).not.toHaveBeenCalled();
});
it('auto-selects a single account when enabled for onboarding', async () => {
const onAutoSelect = vi.fn();
const onAccountSelect = vi.fn();
const onNext = vi.fn();
render(
<ImportStepAccount
accounts={[account]}
selectedAccountId={null}
onAccountSelect={onAccountSelect}
onAutoSelect={onAutoSelect}
onNext={onNext}
autoSelectSingleAccount
/>,
);
await waitFor(() => {
expect(onAutoSelect).toHaveBeenCalledWith(account.id);
expect(onNext).toHaveBeenCalled();
});
expect(onAccountSelect).not.toHaveBeenCalled();
});
});

View File

@ -17,6 +17,8 @@ interface ImportStepAccountProps {
selectedAccountId: UUID | null;
onAccountSelect: (accountId: UUID) => void;
onNext: () => void;
autoSelectSingleAccount?: boolean;
onAutoSelect?: (accountId: UUID) => void;
}
export function ImportStepAccount({
@ -24,16 +26,35 @@ export function ImportStepAccount({
selectedAccountId,
onAccountSelect,
onNext,
autoSelectSingleAccount = false,
onAutoSelect,
}: ImportStepAccountProps) {
const accounts = filterTransactionalAccounts(rawAccounts);
// If there is only one account, auto-select it, and proceed to next step
useEffect(() => {
if (accounts.length === 1) {
onAccountSelect(accounts[0].id);
if (
autoSelectSingleAccount &&
accounts.length === 1 &&
!selectedAccountId
) {
const accountId = accounts[0].id;
if (onAutoSelect) {
onAutoSelect(accountId);
} else {
onAccountSelect(accountId);
}
onNext();
}
}, [accounts, onAccountSelect, onNext]);
}, [
accounts,
autoSelectSingleAccount,
onAccountSelect,
onAutoSelect,
onNext,
selectedAccountId,
]);
if (accounts.length === 0) {
return (

View File

@ -0,0 +1,20 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { ImportStepUpload } from './import-step-upload';
describe('ImportStepUpload', () => {
it('hides the back button when requested', () => {
render(
<ImportStepUpload
file={null}
onFileSelect={vi.fn()}
onNext={vi.fn()}
onBack={vi.fn()}
showBackButton={false}
/>,
);
expect(screen.queryByRole('button', { name: 'Back' })).toBeNull();
expect(screen.getByRole('button', { name: 'Next' })).not.toBeNull();
});
});

View File

@ -9,6 +9,7 @@ interface ImportStepUploadProps {
onFileSelect: (file: File) => void;
onNext: () => void;
onBack: () => void;
showBackButton?: boolean;
}
export function ImportStepUpload({
@ -16,6 +17,7 @@ export function ImportStepUpload({
onFileSelect,
onNext,
onBack,
showBackButton = true,
}: ImportStepUploadProps) {
const [isDragging, setIsDragging] = useState(false);
@ -135,10 +137,17 @@ export function ImportStepUpload({
)}
</div>
<div className="flex justify-between">
<Button variant="outline" onClick={onBack}>
{__('Back')}
</Button>
<div
className={cn(
'flex',
showBackButton ? 'justify-between' : 'justify-end',
)}
>
{showBackButton && (
<Button variant="outline" onClick={onBack}>
{__('Back')}
</Button>
)}
<Button onClick={onNext} disabled={!file}>
{__('Next')}
</Button>

View File

@ -31,6 +31,7 @@ import {
type ColumnMapping,
type ImportState,
} from '@/types/import';
import { type UUID } from '@/types/uuid';
import { __ } from '@/utils/i18n';
import { router, usePage } from '@inertiajs/react';
import { useEffect, useState } from 'react';
@ -48,6 +49,7 @@ interface ImportTransactionsDrawerProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onImportComplete?: () => void;
autoSelectSingleAccount?: boolean;
}
interface ImportError {
@ -68,6 +70,7 @@ export function ImportTransactionsDrawer({
open,
onOpenChange,
onImportComplete,
autoSelectSingleAccount = false,
}: ImportTransactionsDrawerProps) {
const { locale } = usePage<SharedData>().props;
const [isImporting, setIsImporting] = useState(false);
@ -75,6 +78,8 @@ export function ImportTransactionsDrawer({
const [importTotal, setImportTotal] = useState(0);
const [importErrors, setImportErrors] = useState<ImportError[]>([]);
const [error, setError] = useState<string | null>(null);
const [wasSingleAccountAutoSelected, setWasSingleAccountAutoSelected] =
useState(false);
const [selectedAccount, setSelectedAccount] = useState<Account | null>(
null,
);
@ -89,6 +94,7 @@ export function ImportTransactionsDrawer({
transaction_date: null,
description: null,
amount: null,
balance: null,
},
dateFormat: DateFormat.YearMonthDay,
dateFormatDetected: false,
@ -119,6 +125,7 @@ export function ImportTransactionsDrawer({
transaction_date: null,
description: null,
amount: null,
balance: null,
},
dateFormat: DateFormat.YearMonthDay,
dateFormatDetected: false,
@ -126,14 +133,20 @@ export function ImportTransactionsDrawer({
});
setIsImporting(false);
setError(null);
setWasSingleAccountAutoSelected(false);
setSelectedAccount(null);
}
}, [open]);
const handleAccountSelect = (accountId: number) => {
const handleAccountSelect = (accountId: UUID) => {
setState((prev) => ({ ...prev, selectedAccountId: accountId }));
};
const handleSingleAccountAutoSelect = (accountId: UUID) => {
setWasSingleAccountAutoSelected(true);
handleAccountSelect(accountId);
};
const handleFileSelect = async (file: File) => {
if (!file) {
setState((prev) => ({
@ -653,6 +666,8 @@ export function ImportTransactionsDrawer({
onNext={() => {
moveToStep(ImportStep.UploadFile);
}}
autoSelectSingleAccount={autoSelectSingleAccount}
onAutoSelect={handleSingleAccountAutoSelect}
/>
);
@ -665,6 +680,7 @@ export function ImportTransactionsDrawer({
moveToStep(ImportStep.MapColumns);
}}
onBack={() => moveToStep(ImportStep.SelectAccount)}
showBackButton={!wasSingleAccountAutoSelected}
/>
);

View File

@ -1,4 +1,5 @@
import { type ColumnMapping, DateFormat } from '@/types/import';
import { type UUID } from '@/types/uuid';
interface ImportConfig {
columnMapping: ColumnMapping;
@ -7,10 +8,7 @@ interface ImportConfig {
const STORAGE_KEY_PREFIX = 'import_config_account_';
export function saveImportConfig(
accountId: number,
config: ImportConfig,
): void {
export function saveImportConfig(accountId: UUID, config: ImportConfig): void {
if (typeof window === 'undefined') return;
try {
@ -21,7 +19,7 @@ export function saveImportConfig(
}
}
export function loadImportConfig(accountId: number): ImportConfig | null {
export function loadImportConfig(accountId: UUID): ImportConfig | null {
if (typeof window === 'undefined') return null;
try {
@ -45,7 +43,7 @@ export function loadImportConfig(accountId: number): ImportConfig | null {
}
}
export function clearImportConfig(accountId: number): void {
export function clearImportConfig(accountId: UUID): void {
if (typeof window === 'undefined') return;
try {

View File

@ -1,3 +1,5 @@
import { UUID } from './uuid';
export enum ImportStep {
SelectAccount = 'select-account',
UploadFile = 'upload-file',
@ -40,7 +42,7 @@ export interface ColumnOption {
export interface ImportState {
step: ImportStep;
selectedAccountId: number | null;
selectedAccountId: UUID | null;
file: File | null;
parsedData: ParsedRow[];
columnHeaders: string[];