Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/libs/actions/ImportSpreadsheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,38 @@ function applySavedColumnMappings(spreadsheetData: string[][], savedLayout: Save
}
}

/**
* Applies saved company card column mappings to the spreadsheet data.
*
* @param spreadsheetData - The spreadsheet data in column-major format
* @param savedColumnMappings - Saved mappings from uploadLayoutSettings, keyed by field role with a column index value
* @param availableColumnRoles - The field roles currently selectable in the mapping UI
*/
function applyCompanyCardSavedColumnMappings(spreadsheetData: string[][], savedColumnMappings: Record<string, string>, availableColumnRoles: string[]): void {
if (!savedColumnMappings) {
return;
}

const validRoles = new Set(availableColumnRoles);
const numColumns = spreadsheetData.length;
const columnUpdates: Record<number, string> = {};

for (const [role, indexValue] of Object.entries(savedColumnMappings)) {
if (role === CONST.CSV_IMPORT_COLUMNS.IGNORE || !validRoles.has(role)) {
continue;
}
const index = Number(indexValue);
if (!Number.isInteger(index) || index < 0 || index >= numColumns) {
continue;
}
columnUpdates[index] = role;
}

if (Object.keys(columnUpdates).length > 0) {
Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {columns: columnUpdates});
}
}

export {
setSpreadsheetData,
setColumnName,
Expand All @@ -210,6 +242,7 @@ export {
setImportTransactionCurrency,
setImportTransactionSettings,
applySavedColumnMappings,
applyCompanyCardSavedColumnMappings,
getImportFailedFinalModal,
getImportFinalModalID,
getImportFinalModalOnyxData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePolicy from '@hooks/usePolicy';

import {applyCompanyCardSavedColumnMappings} from '@libs/actions/ImportSpreadsheet';
import {getCSVFeedType} from '@libs/CardUtils';
import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils';
import Navigation from '@libs/Navigation/Navigation';
Expand All @@ -30,7 +31,7 @@ import type {Errors} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';

import React, {useMemo, useState} from 'react';
import React, {useEffect, useMemo, useRef, useState} from 'react';

type CompanyCardsImportedPageProps = PlatformStackScreenProps<WorkspaceSplitNavigatorParamList, typeof SCREENS.WORKSPACE.COMPANY_CARDS_IMPORTED>;

Expand Down Expand Up @@ -83,6 +84,35 @@ function CompanyCardsImportedPage({route}: CompanyCardsImportedPageProps) {
return [...baseRoles, ...advancedRoles];
})();

const savedColumnMappings = Object.entries(workspaceCardFeeds?.settings?.companyCards ?? {}).find(([feedKey]) => feedKey === layoutType)?.[1]?.uploadLayoutSettings?.columnMappings;
const hasAppliedSavedMappings = useRef(false);
const lastProcessedDataRef = useRef(spreadsheet?.data);
const lastAdvancedFieldsRef = useRef(shouldUseAdvancedFields);

useEffect(() => {
// Reset the flag when new spreadsheet data is loaded, or when the set of selectable roles changes.
if (spreadsheet?.data !== lastProcessedDataRef.current || shouldUseAdvancedFields !== lastAdvancedFieldsRef.current) {
hasAppliedSavedMappings.current = false;
lastProcessedDataRef.current = spreadsheet?.data;
lastAdvancedFieldsRef.current = shouldUseAdvancedFields;
}

if (hasAppliedSavedMappings.current) {
return;
}

if (!spreadsheet?.data || isEmptyObject(savedColumnMappings)) {
return;
}

hasAppliedSavedMappings.current = true;
applyCompanyCardSavedColumnMappings(
spreadsheet.data,
savedColumnMappings,
columnRoles.map((role) => role.value),
Comment thread
Gonals marked this conversation as resolved.
);
}, [spreadsheet?.data, savedColumnMappings, columnRoles, shouldUseAdvancedFields]);

const requiredColumns = columnRoles.filter((role) => role.isRequired);

const validate = () => {
Expand Down
94 changes: 93 additions & 1 deletion tests/unit/ImportTransactions.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {applySavedColumnMappings, getImportFinalModalOnyxData} from '@libs/actions/ImportSpreadsheet';
import {applyCompanyCardSavedColumnMappings, applySavedColumnMappings, getImportFinalModalOnyxData} from '@libs/actions/ImportSpreadsheet';
import importTransactionsFromCSV, {buildColumnLayout, buildTransactionListFromSpreadsheet, getColumnIndexes} from '@libs/actions/ImportTransactions';
import * as API from '@libs/API';

Expand Down Expand Up @@ -776,6 +776,98 @@ describe('ImportTransactions', () => {
});
});

describe('applyCompanyCardSavedColumnMappings', () => {
const availableRoles = [
CONST.CSV_IMPORT_COLUMNS.IGNORE,
CONST.CSV_IMPORT_COLUMNS.CARD_NUMBER,
CONST.CSV_IMPORT_COLUMNS.POSTED_DATE,
CONST.CSV_IMPORT_COLUMNS.MERCHANT,
CONST.CSV_IMPORT_COLUMNS.AMOUNT,
CONST.CSV_IMPORT_COLUMNS.CURRENCY,
];

// Column-major spreadsheet data (each entry is a column)
const spreadsheetData = [
['Card', '4111', '4111'],
['Posted', '2024-01-01', '2024-01-02'],
['Merchant', 'Store A', 'Store B'],
['Amount', '10.00', '20.00'],
['Currency', 'USD', 'USD'],
];

it('should apply saved company card column mappings by index', () => {
const savedColumnMappings = {
cardNumber: '0',
postedDate: '1',
merchant: '2',
amount: '3',
currency: '4',
};

applyCompanyCardSavedColumnMappings(spreadsheetData, savedColumnMappings, availableRoles);

expect(Onyx.merge).toHaveBeenCalledWith(ONYXKEYS.IMPORTED_SPREADSHEET, {
columns: {
0: CONST.CSV_IMPORT_COLUMNS.CARD_NUMBER,
1: CONST.CSV_IMPORT_COLUMNS.POSTED_DATE,
2: CONST.CSV_IMPORT_COLUMNS.MERCHANT,
3: CONST.CSV_IMPORT_COLUMNS.AMOUNT,
4: CONST.CSV_IMPORT_COLUMNS.CURRENCY,
},
});
});

it('should skip roles that are not selectable in the current context', () => {
const savedColumnMappings = {
cardNumber: '0',
merchant: '2',
amount: '3',
// Advanced field not available when advanced fields are disabled
originalAmount: '1',
// Auto-generated column that is never a selectable role
externalID: '4',
};

applyCompanyCardSavedColumnMappings(spreadsheetData, savedColumnMappings, availableRoles);

expect(Onyx.merge).toHaveBeenCalledWith(ONYXKEYS.IMPORTED_SPREADSHEET, {
columns: {
0: CONST.CSV_IMPORT_COLUMNS.CARD_NUMBER,
2: CONST.CSV_IMPORT_COLUMNS.MERCHANT,
3: CONST.CSV_IMPORT_COLUMNS.AMOUNT,
},
});
});

it('should skip the ignore role and out-of-range indexes', () => {
const savedColumnMappings = {
ignore: '0',
merchant: '2',
amount: '99',
currency: '-1',
};

applyCompanyCardSavedColumnMappings(spreadsheetData, savedColumnMappings, availableRoles);

expect(Onyx.merge).toHaveBeenCalledWith(ONYXKEYS.IMPORTED_SPREADSHEET, {
columns: {
2: CONST.CSV_IMPORT_COLUMNS.MERCHANT,
},
});
});

it('should not call Onyx.merge when no columns can be mapped', () => {
const savedColumnMappings = {
originalAmount: '1',
originalCurrency: '4',
};

applyCompanyCardSavedColumnMappings(spreadsheetData, savedColumnMappings, availableRoles);

expect(Onyx.merge).not.toHaveBeenCalled();
});
});

describe('importTransactionsFromCSV', () => {
const CURRENT_USER_ACCOUNT_ID = 12345;
const validSpreadsheet = {
Expand Down
Loading