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:
parent
4f55ced837
commit
fc419c6cae
|
|
@ -150,6 +150,7 @@ export function StepImportTransactions({
|
|||
categories={categories}
|
||||
banks={banks}
|
||||
automationRules={automationRules}
|
||||
autoSelectSingleAccount
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
Loading…
Reference in New Issue