Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feed syntax key #57454

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
372 changes: 186 additions & 186 deletions ios/NewExpensify.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6245,6 +6245,7 @@ const CONST = {
PAID: 'paid',
EXPORTED: 'exported',
POSTED: 'posted',
FEED: 'feed',
},
EMPTY_VALUE: 'none',
SEARCH_ROUTER_ITEM_TYPE: {
Expand Down Expand Up @@ -6278,6 +6279,7 @@ const CONST = {
PAID: 'paid',
EXPORTED: 'exported',
POSTED: 'posted',
FEED: 'feed',
},
DATE_MODIFIERS: {
BEFORE: 'Before',
Expand Down
23 changes: 21 additions & 2 deletions src/components/Search/SearchAutocompleteList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import usePolicy from '@hooks/usePolicy';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {searchInServer} from '@libs/actions/Report';
import {getCardDescription, isCard, isCardHiddenFromSearch, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import {generateDomainFeedsData, getCardDescription, getCardFeedKey, isCard, isCardHiddenFromSearch, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import {combineOrderingOfReportsAndPersonalDetails, getSearchOptions, getValidOptions} from '@libs/OptionsListUtils';
import type {Options, SearchOption} from '@libs/OptionsListUtils';
import Performance from '@libs/Performance';
Expand All @@ -33,6 +33,7 @@ import {
parseForAutocomplete,
} from '@libs/SearchAutocompleteUtils';
import {buildSearchQueryJSON, buildUserReadableQueryString, sanitizeSearchValue} from '@libs/SearchQueryUtils';
import {getCardFeedNames} from '@pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage';
import Timing from '@userActions/Timing';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -152,6 +153,10 @@ function SearchAutocompleteList(
const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds, userCardList), [userCardList, workspaceCardFeeds]);
const cardAutocompleteList = Object.values(allCards);

const domainFeeds = useMemo(() => generateDomainFeedsData(userCardList), [userCardList]);
const cardFeedNames = useMemo(() => getCardFeedNames(workspaceCardFeeds, domainFeeds, translate), [domainFeeds, translate, workspaceCardFeeds]);
const feedAutoCompleteList = useMemo(() => Object.entries(cardFeedNames).map(([value, key]) => ({value, key})), [cardFeedNames]);

const participantsAutocompleteList = useMemo(() => {
if (!areOptionsInitialized) {
return [];
Expand Down Expand Up @@ -339,6 +344,19 @@ function SearchAutocompleteList(
text: expenseType,
}));
}
case CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED: {
const filteredFeeds = feedAutoCompleteList
.filter((feed) => feed.key.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(feed.key))
.sort()
.slice(0, 10);

return filteredFeeds.map((feed) => ({
filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.FEED,
text: feed.key,
autocompleteID: getCardFeedKey(feed.value),
mapKey: CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED,
}));
}
case CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID: {
const filteredCards = cardAutocompleteList
.filter((card) => isCard(card) && !isCardHiddenFromSearch(card))
Expand Down Expand Up @@ -371,6 +389,7 @@ function SearchAutocompleteList(
typeAutocompleteList,
statusAutocompleteList,
expenseTypes,
feedAutoCompleteList,
cardAutocompleteList,
allCards,
]);
Expand All @@ -382,7 +401,7 @@ function SearchAutocompleteList(
const recentSearchesData = sortedRecentSearches?.slice(0, 5).map(({query, timestamp}) => {
const searchQueryJSON = buildSearchQueryJSON(query);
return {
text: searchQueryJSON ? buildUserReadableQueryString(searchQueryJSON, personalDetails, reports, taxRates, allCards) : query,
text: searchQueryJSON ? buildUserReadableQueryString(searchQueryJSON, personalDetails, reports, taxRates, allCards, cardFeedNames) : query,
singleIcon: Expensicons.History,
searchQuery: query,
keyForList: timestamp,
Expand Down
11 changes: 7 additions & 4 deletions src/components/Search/SearchPageHeaderInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import {navigateToAndOpenReport} from '@libs/actions/Report';
import {clearAllFilters} from '@libs/actions/Search';
import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import {generateDomainFeedsData, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getAllTaxRates} from '@libs/PolicyUtils';
import type {OptionData} from '@libs/ReportUtils';
import {getAutocompleteQueryWithComma, getQueryWithoutAutocompletedPart} from '@libs/SearchAutocompleteUtils';
import {buildUserReadableQueryString, getQueryWithUpdatedValues, isCannedSearchQuery, sanitizeSearchValue} from '@libs/SearchQueryUtils';
import {getCardFeedNames} from '@pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
Expand Down Expand Up @@ -77,9 +78,11 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
const [workspaceCardFeeds = {}] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST);
const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds, userCardList), [userCardList, workspaceCardFeeds]);

const domainFeeds = useMemo(() => generateDomainFeedsData(userCardList), [userCardList]);
const cardFeedNames = useMemo(() => getCardFeedNames(workspaceCardFeeds, domainFeeds, translate), [domainFeeds, translate, workspaceCardFeeds]);
const {type, inputQuery: originalInputQuery} = queryJSON;
const isCannedQuery = isCannedSearchQuery(queryJSON);
const queryText = buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, allCards);
const queryText = buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNames);
const headerText = isCannedQuery ? translate(getHeaderContent(type).titleText) : '';

// The actual input text that the user sees
Expand Down Expand Up @@ -113,9 +116,9 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
}, [queryText]);

useEffect(() => {
const substitutionsMap = buildSubstitutionsMap(originalInputQuery, personalDetails, reports, taxRates, allCards);
const substitutionsMap = buildSubstitutionsMap(originalInputQuery, personalDetails, reports, taxRates, allCards, cardFeedNames);
setAutocompleteSubstitutions(substitutionsMap);
}, [allCards, originalInputQuery, personalDetails, reports, taxRates]);
}, [cardFeedNames, allCards, originalInputQuery, personalDetails, reports, taxRates]);

const onSearchQueryChange = useCallback(
(userQuery: string) => {
Expand Down
6 changes: 4 additions & 2 deletions src/components/Search/SearchRouter/buildSubstitutionsMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function buildSubstitutionsMap(
reports: OnyxCollection<Report>,
allTaxRates: Record<string, string[]>,
cardList: CardList,
cardFeedNames: Record<string, string>,
): SubstitutionMap {
const parsedQuery = parse(query) as {ranges: SearchAutocompleteQueryRange[]};

Expand Down Expand Up @@ -61,9 +62,10 @@ function buildSubstitutionsMap(
filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO ||
filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.IN ||
filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID ||
filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG
filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG ||
filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED
) {
const displayValue = getFilterDisplayValue(filterKey, filterValue, personalDetails, reports, cardList);
const displayValue = getFilterDisplayValue(filterKey, filterValue, personalDetails, reports, cardList, cardFeedNames);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add 1 test case to buildSubstitutionsMap tests that will cover cardFeedNames


// If displayValue === filterValue, then it means there is nothing to substitute, so we don't add any key to map
if (displayValue !== filterValue) {
Expand Down
54 changes: 54 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,29 @@ function isCorporateCard(cardID: number) {
return !!allCards[cardID];
}

/**
* @param cardID
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there is no extra description to @param then please remove it.

This code used to be JS only, so IMO all the empty @param XXX are leftovers from JS.

* @returns string with the 'cards_' part removed from the beginning
*/
function getCardFeedKey(cardID: string): string {
const splittedCardID = cardID.split('_');
if (splittedCardID.at(0) === 'cards') {
return splittedCardID.slice(1).join('_');
}
return cardID;
}

/**
* @param cardID
* @returns string with added 'cards_' substring at the beginning
*/
function getWorkspaceCardFeedKey(cardID: string) {
if (cardID.split('_').at(0) !== 'cards') {
return `cards_${cardID}`;
}
return cardID;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double check if these 2 functions can be improved

}

/**
* @param cardID
* @returns string in format %<bank> - <lastFourPAN || Not Activated>%.
Expand All @@ -86,6 +109,32 @@ function getCardDescription(cardID?: number, cards: CardList = allCards) {
return cardDescriptor ? `${humanReadableBankName} - ${cardDescriptor}` : `${humanReadableBankName}`;
}

type DomainFeedData = {bank: string; domainName: string; correspondingCardIDs: string[]; fundID?: string};

/**
* @param cardList - The list of cards to process. Can be undefined.
* @returns a record where keys are domain names and values contain domain feed data.
*/
function generateDomainFeedsData(cardList: CardList | undefined): Record<string, DomainFeedData> {
return Object.values(cardList ?? {}).reduce((accumulator, currentCard) => {
// Cards in cardList can also be domain cards, we use them to compute domain feed
if (!currentCard.domainName.match(CONST.REGEX.EXPENSIFY_POLICY_DOMAIN_NAME) && !isCardHiddenFromSearch(currentCard)) {
if (accumulator[currentCard.domainName]) {
accumulator[currentCard.domainName].correspondingCardIDs.push(currentCard.cardID.toString());
} else {
// if the cards belongs to the same domain, every card of it should have the same fundID
accumulator[currentCard.domainName] = {
fundID: currentCard.fundID,
domainName: currentCard.domainName,
bank: currentCard.bank,
correspondingCardIDs: [currentCard.cardID.toString()],
};
}
}
return accumulator;
}, {} as Record<string, DomainFeedData>);
}

function isCard(item: Card | Record<string, string>): item is Card {
return typeof item === 'object' && 'cardID' in item && !!item.cardID && 'bank' in item && !!item.bank;
}
Expand Down Expand Up @@ -549,4 +598,9 @@ export {
getFeedType,
flatAllCardsList,
checkIfFeedConnectionIsBroken,
getCardFeedKey,
getWorkspaceCardFeedKey,
generateDomainFeedsData,
};

export type {DomainFeedData};
Loading