Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const chatTypes = {
// Explicit type annotation is required
const cardActiveStates: number[] = [2, 3, 4, 7];

// Hide not issued or not activated cards (states 2 and 4) from card filter options in search, as no transactions can be made on cards in these states
// Hide not issued or not activated cards (states 2, 4) from card filter options in search, as no transactions can be made on cards in these states
const cardHiddenFromSearchStates: number[] = [2, 4];

const selectableOnboardingChoices = {
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4996,6 +4996,7 @@ const translations = {
card: {
expensify: 'Expensify',
individualCards: 'Individual cards',
closedCards: 'Closed cards',
cardFeeds: 'Card feeds',
cardFeedName: ({cardFeedBankName, cardFeedLabel}: {cardFeedBankName: string; cardFeedLabel?: string}) =>
`All ${cardFeedBankName}${cardFeedLabel ? ` - ${cardFeedLabel}` : ''}`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5045,6 +5045,7 @@ const translations = {
card: {
expensify: 'Expensify',
individualCards: 'Tarjetas individuales',
closedCards: 'Tarjetas cerradas',
cardFeeds: 'Flujos de tarjetas',
cardFeedName: ({cardFeedBankName, cardFeedLabel}: {cardFeedBankName: string; cardFeedLabel?: string}) =>
`Todo ${cardFeedBankName}${cardFeedLabel ? ` - ${cardFeedLabel}` : ''}`,
Expand Down
5 changes: 5 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ function isCardHiddenFromSearch(card: Card) {
return !card?.nameValuePairs?.isVirtual && CONST.EXPENSIFY_CARD.HIDDEN_FROM_SEARCH_STATES.includes(card.state ?? 0);
}

function isCardClosed(card: Card) {
return card?.state === CONST.EXPENSIFY_CARD.STATE.CLOSED;
}

function mergeCardListWithWorkspaceFeeds(workspaceFeeds: Record<string, WorkspaceCardsList | undefined>, cardList = allCards, shouldExcludeCardHiddenFromSearch = false) {
const feedCards: CardList = {};
Object.values(cardList).forEach((card) => {
Expand Down Expand Up @@ -537,6 +541,7 @@ export {
getSelectedFeed,
getCorrectStepForSelectedBank,
getCustomOrFormattedFeedName,
isCardClosed,
getFilteredCardList,
hasOnlyOneCardToAssign,
checkIfNewFeedConnected,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import {openSearchFiltersCardPage, updateAdvancedFilters} from '@libs/actions/Search';
import {getBankName, getCardFeedIcon, isCard, isCardHiddenFromSearch} from '@libs/CardUtils';
import {getBankName, getCardFeedIcon, isCard, isCardClosed, isCardHiddenFromSearch} from '@libs/CardUtils';
import {getDescriptionForPolicyDomainCard, getPolicy} from '@libs/PolicyUtils';
import type {OptionData} from '@libs/ReportUtils';
import Navigation from '@navigation/Navigation';
Expand Down Expand Up @@ -43,7 +43,7 @@ function getRepeatingBanks(workspaceCardFeedsKeys: string[], domainFeedsData: Re
return Object.keys(bankFrequency).filter((bank) => bankFrequency[bank] > 1);
}

function createIndividualCardFilterItem(card: Card, personalDetailsList: PersonalDetailsList, selectedCards: string[]): CardFilterItem {
function createCardFilterItem(card: Card, personalDetailsList: PersonalDetailsList, selectedCards: string[]): CardFilterItem {
const personalDetails = personalDetailsList[card?.accountID ?? CONST.DEFAULT_NUMBER_ID];
const isSelected = selectedCards.includes(card.cardID.toString());
const icon = getCardFeedIcon(card?.bank as CompanyCardFeed);
Expand All @@ -66,23 +66,26 @@ function createIndividualCardFilterItem(card: Card, personalDetailsList: Persona
};
}

function buildIndividualCardsData(
function buildCardsData(
workspaceCardFeeds: Record<string, WorkspaceCardsList | undefined>,
userCardList: CardList,
personalDetailsList: PersonalDetailsList,
selectedCards: string[],
isClosedCards = false,
): ItemsGroupedBySelection {
// Filter condition to build different cards data for closed cards and individual cards based on the isClosedCards flag, we don't want to show closed cards in the individual cards section
const filterCondition = (card: Card) => (isClosedCards ? isCardClosed(card) : !isCardHiddenFromSearch(card) && !isCardClosed(card));
const userAssignedCards: CardFilterItem[] = Object.values(userCardList ?? {})
.filter((card) => !isCardHiddenFromSearch(card))
.map((card) => createIndividualCardFilterItem(card, personalDetailsList, selectedCards));
.filter((card) => filterCondition(card))
.map((card) => createCardFilterItem(card, personalDetailsList, selectedCards));

// When user is admin of a workspace he sees all the cards of workspace under cards_ Onyx key
const allWorkspaceCards: CardFilterItem[] = Object.values(workspaceCardFeeds)
.filter((cardFeed) => !isEmptyObject(cardFeed))
.flatMap((cardFeed) => {
return Object.values(cardFeed as Record<string, Card>)
.filter((card) => card && isCard(card) && !userCardList?.[card.cardID] && !isCardHiddenFromSearch(card))
.map((card) => createIndividualCardFilterItem(card, personalDetailsList, selectedCards));
.filter((card) => card && isCard(card) && !userCardList?.[card.cardID] && filterCondition(card))
.map((card) => createCardFilterItem(card, personalDetailsList, selectedCards));
});

const allCardItems = [...userAssignedCards, ...allWorkspaceCards];
Expand Down Expand Up @@ -172,6 +175,7 @@ function buildCardFeedsData(
const isBankRepeating = repeatingBanks.includes(bank);
const policyID = domainName.match(CONST.REGEX.EXPENSIFY_POLICY_DOMAIN_NAME)?.[1] ?? '';
const correspondingPolicy = getPolicy(policyID?.toUpperCase());
// We need to assign correspondingCardIDs for closed cards as well, because we need to be able to select on "all" both closed and individual cards
const correspondingCardIDs = Object.entries(cardFeed ?? {})
.filter(([cardKey, card]) => cardKey !== 'cardList' && isCard(card) && !isCardHiddenFromSearch(card))
.map(([cardKey]) => cardKey);
Expand Down Expand Up @@ -211,7 +215,12 @@ function SearchFiltersCardPage() {
}, []);

const individualCardsSectionData = useMemo(
() => buildIndividualCardsData(workspaceCardFeeds ?? {}, userCardList ?? {}, personalDetails ?? {}, selectedCards),
() => buildCardsData(workspaceCardFeeds ?? {}, userCardList ?? {}, personalDetails ?? {}, selectedCards, false),
[workspaceCardFeeds, userCardList, personalDetails, selectedCards],
);

const closedCardsSectionData = useMemo(
() => buildCardsData(workspaceCardFeeds ?? {}, userCardList ?? {}, personalDetails ?? {}, selectedCards, true),
[workspaceCardFeeds, userCardList, personalDetails, selectedCards],
);

Expand Down Expand Up @@ -251,7 +260,7 @@ function SearchFiltersCardPage() {

const sections = useMemo(() => {
const newSections = [];
const selectedItems = [...cardFeedsSectionData.selected, ...individualCardsSectionData.selected];
const selectedItems = [...cardFeedsSectionData.selected, ...individualCardsSectionData.selected, ...closedCardsSectionData.selected];

newSections.push({
title: undefined,
Expand All @@ -268,8 +277,22 @@ function SearchFiltersCardPage() {
data: individualCardsSectionData.unselected.filter(searchFunction),
shouldShow: individualCardsSectionData.unselected.length > 0,
});
newSections.push({
title: translate('search.filters.card.closedCards'),
data: closedCardsSectionData.unselected.filter(searchFunction),
shouldShow: closedCardsSectionData.unselected.length > 0,
});
return newSections;
}, [cardFeedsSectionData.selected, cardFeedsSectionData.unselected, individualCardsSectionData.selected, individualCardsSectionData.unselected, searchFunction, translate]);
}, [
cardFeedsSectionData.selected,
cardFeedsSectionData.unselected,
individualCardsSectionData.selected,
individualCardsSectionData.unselected,
closedCardsSectionData.selected,
closedCardsSectionData.unselected,
searchFunction,
translate,
]);

const handleConfirmSelection = useCallback(() => {
updateAdvancedFilters({
Expand Down Expand Up @@ -354,4 +377,4 @@ function SearchFiltersCardPage() {
SearchFiltersCardPage.displayName = 'SearchFiltersCardPage';

export default SearchFiltersCardPage;
export {buildIndividualCardsData, buildCardFeedsData};
export {buildCardsData, buildCardFeedsData};
106 changes: 95 additions & 11 deletions tests/unit/Search/buildCardFilterDataTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import type {LocaleContextProps} from '@components/LocaleContextProvider';
// eslint-disable-next-line no-restricted-syntax
import * as PolicyUtils from '@libs/PolicyUtils';
import {buildCardFeedsData, buildIndividualCardsData} from '@pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage';
import {buildCardFeedsData, buildCardsData} from '@pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage';
import type {CardList, Policy, WorkspaceCardsList} from '@src/types/onyx';

// Use jest.spyOn to mock the implementation
Expand Down Expand Up @@ -204,13 +204,81 @@ const cardListHiddenOnSearch = {
},
};

const workspaceCardFeedsClosed = {
'cards_11111_Expensify Card': {
'21534278': {
accountID: 1,
bank: 'Expensify Card',
cardID: 21534278,
domainName: 'expensify-policy1.exfy',
nameValuePairs: {cardTitle: 'Not Issued card'},
isVirtual: false,
lastFourPAN: '1234',
state: 6, // CLOSED
},
'21539012': {
accountID: 1,
bank: 'Expensify Card',
cardID: 21539012,
domainName: 'expensify-policy1.exfy',
nameValuePairs: {cardTitle: 'Not activated card'},
isVirtual: false,
lastFourPAN: '3211',
state: 6, // CLOSED
},
'21539027': {
accountID: 1,
bank: 'Expensify Card',
cardID: 21539027,
domainName: 'expensify-policy1.exfy',
nameValuePairs: {cardTitle: 'Not activated card'},
isVirtual: false,
lastFourPAN: '',
state: 3, // OPEN
},
},
};

const cardListClosed = {
'21534538': {
accountID: 1,
bank: 'Expensify Card',
cardID: 21534538,
domainName: 'expensify-policy1.exfy',
nameValuePairs: {cardTitle: 'Not Issued card'},
isVirtual: false,
lastFourPAN: '',
state: 6, // CLOSED
},
'21534525': {
accountID: 1,
bank: 'Expensify Card',
cardID: 21534525,
domainName: 'expensify-policy1.exfy',
nameValuePairs: {cardTitle: 'Not activated card'},
isVirtual: false,
lastFourPAN: '',
state: 6, // CLOSED
},
'21534526': {
accountID: 1,
bank: 'Expensify Card',
cardID: 21534526,
domainName: 'expensify-policy1.exfy',
nameValuePairs: {cardTitle: 'Not activated card'},
isVirtual: false,
lastFourPAN: '',
state: 3, // OPEN
},
};

const domainFeedDataMock = {testDomain: {domainName: 'testDomain', bank: 'Expensify Card', correspondingCardIDs: ['11111111']}};

const translateMock = jest.fn();

describe('buildIndividualCardsData', () => {
describe('buildCardsData individual cards', () => {
it("Builds all individual cards and doesn't generate duplicates", () => {
const result = buildIndividualCardsData(workspaceCardFeeds as unknown as Record<string, WorkspaceCardsList | undefined>, cardList as unknown as CardList, {}, ['21588678']);
const result = buildCardsData(workspaceCardFeeds as unknown as Record<string, WorkspaceCardsList | undefined>, cardList as unknown as CardList, {}, ['21588678']);

expect(result.unselected.length + result.selected.length).toEqual(11);

Expand All @@ -229,19 +297,35 @@ describe('buildIndividualCardsData', () => {
});
});
it("Doesn't include physical cards that haven't been issued or haven't been activated", () => {
const result = buildIndividualCardsData(
workspaceCardFeedsHiddenOnSearch as unknown as Record<string, WorkspaceCardsList | undefined>,
cardListHiddenOnSearch as unknown as CardList,
{},
[],
);
const result = buildCardsData(workspaceCardFeedsHiddenOnSearch as unknown as Record<string, WorkspaceCardsList | undefined>, cardListHiddenOnSearch as unknown as CardList, {}, []);
expect(result.unselected.length + result.selected.length).toEqual(0);
});
});

describe('buildIndividualCardsData with empty argument objects', () => {
describe('buildCardsData closed cards', () => {
it("Builds all closed cards and doesn't generate duplicates", () => {
const result = buildCardsData(workspaceCardFeedsClosed as unknown as Record<string, WorkspaceCardsList | undefined>, cardListClosed as unknown as CardList, {}, ['21539012'], true);
expect(result.unselected.length + result.selected.length).toEqual(4);

// Check if Expensify card was built correctly
const expensifyCard = result.selected.find((card) => card.keyForList === '21539012');
expect(expensifyCard).toMatchObject({
lastFourPAN: '3211',
isSelected: true,
});

// Check if company card was built correctly
const companyCard = result.unselected.find((card) => card.keyForList === '21534525');
expect(companyCard).toMatchObject({
lastFourPAN: '',
isSelected: false,
});
});
});

describe('buildCardsData with empty argument objects', () => {
it('Returns empty array when cardList and workspaceCardFeeds are empty', () => {
const result = buildIndividualCardsData({}, {}, {}, []);
const result = buildCardsData({}, {}, {}, []);
expect(result).toEqual({selected: [], unselected: []});
});
});
Expand Down