diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 1aef4d27fbbb..beaf127a23cb 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1054,6 +1054,43 @@ function isSmartLimitEnabled(cardsList: CardList) { const CUSTOM_FEEDS = [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD, CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX, CONST.COMPANY_CARD.FEED_BANK_NAME.CSV]; +function collectUsedCSVFeedSlotNumbersFromCompanyCards(companyCards: CompanyFeeds | undefined, csvPrefix: string): number[] { + const numbers: number[] = []; + for (const key of Object.keys(companyCards ?? {})) { + if (!key.startsWith(csvPrefix)) { + continue; + } + const suffix = key.slice(csvPrefix.length); + if (!suffix) { + continue; + } + const n = Number.parseInt(suffix, 10); + if (!Number.isNaN(n) && n > 0) { + numbers.push(n); + } + } + return numbers; +} + +/** + * Next available CSV file-import feed (`ccuploadN`) from `settings.companyCards` keys only, + * including feeds omitted from {@link getFeedType}'s `CombinedCardFeeds` input. + */ +function getCSVFeedType(companyCards: CompanyFeeds | undefined): CompanyCardFeedWithNumber { + const csvPrefix = CONST.COMPANY_CARD.FEED_BANK_NAME.CSV; + const feedNumbers = [...new Set(collectUsedCSVFeedSlotNumbersFromCompanyCards(companyCards, csvPrefix))].sort((a, b) => a - b); + + let firstAvailableNumber = 1; + for (const num of feedNumbers) { + if (num && num !== firstAvailableNumber) { + return `${csvPrefix}${firstAvailableNumber}`; + } + firstAvailableNumber++; + } + + return `${csvPrefix}${firstAvailableNumber}`; +} + function getFeedType(feedKey: CompanyCardFeed, cardFeeds: OnyxEntry): CompanyCardFeedWithNumber { if (CUSTOM_FEEDS.some((feed) => feed === feedKey)) { const filteredFeeds = Object.keys(cardFeeds ?? {}) @@ -1670,6 +1707,7 @@ export { filterCardsByNonExpensify, getAllCardsForWorkspace, isCardHiddenFromSearch, + getCSVFeedType, getFeedType, flattenWorkspaceCardsList, isCardConnectionBroken, diff --git a/src/pages/workspace/companyCards/addNew/CompanyCardsImportedPage.tsx b/src/pages/workspace/companyCards/addNew/CompanyCardsImportedPage.tsx index ff8513cedcb7..166eaedac718 100644 --- a/src/pages/workspace/companyCards/addNew/CompanyCardsImportedPage.tsx +++ b/src/pages/workspace/companyCards/addNew/CompanyCardsImportedPage.tsx @@ -1,15 +1,14 @@ -import React, {useState} from 'react'; +import React, {useMemo, useState} from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import type {ColumnRole} from '@components/ImportColumn'; import ImportSpreadsheetColumns from '@components/ImportSpreadsheetColumns'; import ImportSpreadsheetConfirmModal from '@components/ImportSpreadsheetConfirmModal'; import ScreenWrapper from '@components/ScreenWrapper'; -import useCardFeeds from '@hooks/useCardFeeds'; import useCloseImportPage from '@hooks/useCloseImportPage'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; -import {getFeedType} from '@libs/CardUtils'; +import {getCSVFeedType} from '@libs/CardUtils'; import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -37,13 +36,15 @@ function CompanyCardsImportedPage({route}: CompanyCardsImportedPageProps) { const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID; const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`); const [workspaceCardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); - const [cardFeeds] = useCardFeeds(policyID); const [isImportingTransactions, setIsImportingTransactions] = useState(false); const {setIsClosing} = useCloseImportPage(); const shouldUseAdvancedFields = addNewCard?.data?.useAdvancedFields ?? false; const layoutName = addNewCard?.data?.companyCardLayoutName ?? ''; const prefilledLayoutType = addNewCard?.data?.layoutType; - const [generatedLayoutType] = useState(() => prefilledLayoutType ?? getFeedType(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV, cardFeeds)); + const generatedLayoutType = useMemo( + () => prefilledLayoutType ?? getCSVFeedType(workspaceCardFeeds?.settings?.companyCards), + [prefilledLayoutType, workspaceCardFeeds?.settings?.companyCards], + ); const layoutType = prefilledLayoutType ?? generatedLayoutType; const [existingCardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${layoutType}`); diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index cddf074db1ec..2367811356b2 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -29,6 +29,7 @@ import { getCompanyCardDescription, getCompanyCardFeed, getCompanyFeeds, + getCSVFeedType, getCustomFeedNameFromFeeds, getCustomOrFormattedFeedName, getDefaultExpensifyCardLimitType, @@ -78,7 +79,7 @@ import type { Policy, WorkspaceCardsList, } from '@src/types/onyx'; -import type {CardFeedWithDomainID, CardFeedWithNumber, CompanyCardFeedWithNumber} from '@src/types/onyx/CardFeeds'; +import type {CardFeedWithDomainID, CardFeedWithNumber, CompanyCardFeedWithNumber, CompanyFeeds} from '@src/types/onyx/CardFeeds'; import type IconAsset from '@src/types/utils/IconAsset'; import {localeCompare, translateLocal} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -1882,6 +1883,25 @@ describe('CardUtils', () => { }); }); + describe('getCSVFeedType', () => { + it('returns the first gap when higher-numbered CSV feeds exist in companyCards only', () => { + expect( + getCSVFeedType({ + ccupload3: {pending: false}, + ccupload7: {pending: false}, + } as CompanyFeeds), + ).toBe('ccupload1'); + }); + + it('returns ccupload2 when ccupload1 is already in companyCards', () => { + expect(getCSVFeedType({ccupload1: {pending: false}} as CompanyFeeds)).toBe('ccupload2'); + }); + + it('returns ccupload1 when no CSV feeds exist', () => { + expect(getCSVFeedType(undefined)).toBe('ccupload1'); + }); + }); + describe('flattenCompanyCards', () => { it('should return the flattened list of non-Expensify cards related to the provided workspaceAccountID', () => { const workspaceAccountID = 11111111;