Skip to content

Commit 9297c81

Browse files
authored
Merge pull request Expensify#67802 from s77rt/integrate-group-by-from
Search: Integrate group by from/card
2 parents 994ae9a + d66ee90 commit 9297c81

52 files changed

Lines changed: 1810 additions & 927 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/CONST/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3184,7 +3184,7 @@ const CONST = {
31843184
BREX: 'oauth.brex.com',
31853185
WELLS_FARGO: 'oauth.wellsfargo.com',
31863186
AMEX_DIRECT: 'oauth.americanexpressfdx.com',
3187-
CSV: '_ccupload',
3187+
CSV: 'ccupload',
31883188
},
31893189
STEP_NAMES: ['1', '2', '3', '4'],
31903190
STEP: {
@@ -6313,8 +6313,8 @@ const CONST = {
63136313
},
63146314
GROUP_BY: {
63156315
REPORTS: 'reports',
6316-
MEMBERS: 'members',
6317-
CARDS: 'cards',
6316+
FROM: 'from',
6317+
CARD: 'card',
63186318
},
63196319
BOOLEAN: {
63206320
YES: 'yes',
@@ -6374,6 +6374,7 @@ const CONST = {
63746374
TITLE: 'title',
63756375
ASSIGNEE: 'assignee',
63766376
IN: 'in',
6377+
CARD: 'card',
63776378
},
63786379
SYNTAX_OPERATORS: {
63796380
AND: 'and',
@@ -6399,6 +6400,7 @@ const CONST = {
63996400
AMOUNT: 'amount',
64006401
EXPENSE_TYPE: 'expenseType',
64016402
CURRENCY: 'currency',
6403+
GROUP_CURRENCY: 'groupCurrency',
64026404
MERCHANT: 'merchant',
64036405
DESCRIPTION: 'description',
64046406
FROM: 'from',
@@ -6445,6 +6447,7 @@ const CONST = {
64456447
AMOUNT: 'amount',
64466448
EXPENSE_TYPE: 'expense-type',
64476449
CURRENCY: 'currency',
6450+
GROUP_CURRENCY: 'group-currency',
64486451
MERCHANT: 'merchant',
64496452
DESCRIPTION: 'description',
64506453
FROM: 'from',
@@ -6503,6 +6506,7 @@ const CONST = {
65036506
UNAPPROVED_CASH: 'unapprovedCash',
65046507
UNAPPROVED_CARD: 'unapprovedCard',
65056508
},
6509+
GROUP_PREFIX: 'group_',
65066510
},
65076511

65086512
EXPENSE: {

src/ROUTES.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const ROUTES = {
6565
SEARCH_ADVANCED_FILTERS_STATUS: 'search/filters/status',
6666
SEARCH_ADVANCED_FILTERS_DATE: 'search/filters/date',
6767
SEARCH_ADVANCED_FILTERS_CURRENCY: 'search/filters/currency',
68+
SEARCH_ADVANCED_FILTERS_GROUP_CURRENCY: 'search/filters/group-currency',
6869
SEARCH_ADVANCED_FILTERS_MERCHANT: 'search/filters/merchant',
6970
SEARCH_ADVANCED_FILTERS_DESCRIPTION: 'search/filters/description',
7071
SEARCH_ADVANCED_FILTERS_REPORT_ID: 'search/filters/reportID',

src/SCREENS.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const SCREENS = {
5454
ADVANCED_FILTERS_POSTED_RHP: 'Search_Advanced_Filters_Posted_RHP',
5555
ADVANCED_FILTERS_WITHDRAWN_RHP: 'Search_Advanced_Filters_Withdrawn_RHP',
5656
ADVANCED_FILTERS_CURRENCY_RHP: 'Search_Advanced_Filters_Currency_RHP',
57+
ADVANCED_FILTERS_GROUP_CURRENCY_RHP: 'Search_Advanced_Filters_Group_Currency_RHP',
5758
ADVANCED_FILTERS_DESCRIPTION_RHP: 'Search_Advanced_Filters_Description_RHP',
5859
ADVANCED_FILTERS_MERCHANT_RHP: 'Search_Advanced_Filters_Merchant_RHP',
5960
ADVANCED_FILTERS_REPORT_ID_RHP: 'Search_Advanced_Filters_ReportID_RHP',

src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const shouldShowColumnConfig: Record<SortableColumnName, (isIOUReport: boolean)
3030
[CONST.SEARCH.TABLE_COLUMNS.ACTION]: () => false,
3131
[CONST.SEARCH.TABLE_COLUMNS.TITLE]: () => false,
3232
[CONST.SEARCH.TABLE_COLUMNS.ASSIGNEE]: () => false,
33+
[CONST.SEARCH.TABLE_COLUMNS.CARD]: () => false,
3334
};
3435

3536
const columnConfig: ColumnConfig[] = [

src/components/ReportActionAvatars/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ function ReportActionAvatars({
127127

128128
const [primaryAvatar, secondaryAvatar] = icons;
129129

130-
if (avatarType === CONST.REPORT_ACTION_AVATARS.TYPE.SUBSCRIPT && !!secondaryAvatar?.name) {
130+
if (avatarType === CONST.REPORT_ACTION_AVATARS.TYPE.SUBSCRIPT && (!!secondaryAvatar?.name || !!subscriptCardFeed)) {
131131
return (
132132
<ReportActionAvatar.Subscript
133133
primaryAvatar={primaryAvatar}

src/components/ReportActionAvatars/useReportActionAvatars.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ function useReportActionAvatars({
272272
secondaryAvatar = secondUserFallbackAvatar;
273273
}
274274

275-
const shouldUseMappedAccountIDs = avatarsForAccountIDs.length > 0 && (avatarType === CONST.REPORT_ACTION_AVATARS.TYPE.MULTIPLE || shouldUseActorAccountID);
275+
const shouldUseMappedAccountIDs = avatarsForAccountIDs.length > 0 && (avatarType === CONST.REPORT_ACTION_AVATARS.TYPE.MULTIPLE || shouldUseActorAccountID || shouldUseCardFeed);
276276
const shouldUsePrimaryAvatarID = isWorkspaceActor && !!primaryAvatar.id;
277277
const shouldUseInvoiceExpenseIcons = isWorkspaceExpense && isNestedInInvoiceReport && !!accountID;
278278

src/components/Search/FilterDropdowns/MultiSelectPopup.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Text from '@components/Text';
88
import useLocalize from '@hooks/useLocalize';
99
import useResponsiveLayout from '@hooks/useResponsiveLayout';
1010
import useThemeStyles from '@hooks/useThemeStyles';
11+
import useWindowDimensions from '@hooks/useWindowDimensions';
1112

1213
type MultiSelectItem<T> = {
1314
text: string;
@@ -36,6 +37,7 @@ function MultiSelectPopup<T extends string>({label, value, items, closeOverlay,
3637
const styles = useThemeStyles();
3738
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
3839
const {isSmallScreenWidth} = useResponsiveLayout();
40+
const {windowHeight} = useWindowDimensions();
3941
const [selectedItems, setSelectedItems] = useState(value);
4042

4143
const listData: ListItem[] = useMemo(() => {
@@ -76,7 +78,7 @@ function MultiSelectPopup<T extends string>({label, value, items, closeOverlay,
7678
<View style={[!isSmallScreenWidth && styles.pv4, styles.gap2]}>
7779
{isSmallScreenWidth && <Text style={[styles.textLabel, styles.textSupporting, styles.ph5, styles.pv1]}>{label}</Text>}
7880

79-
<View style={[styles.getSelectionListPopoverHeight(items.length)]}>
81+
<View style={[styles.getSelectionListPopoverHeight(items.length, windowHeight, false)]}>
8082
<SelectionList
8183
shouldSingleExecuteRowSelect
8284
sections={[{data: listData}]}

src/components/Search/FilterDropdowns/SingleSelectPopup.tsx

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import SelectionList from '@components/SelectionList';
55
import SingleSelectListItem from '@components/SelectionList/SingleSelectListItem';
66
import type {ListItem} from '@components/SelectionList/types';
77
import Text from '@components/Text';
8+
import useDebouncedState from '@hooks/useDebouncedState';
89
import useLocalize from '@hooks/useLocalize';
910
import useResponsiveLayout from '@hooks/useResponsiveLayout';
1011
import useThemeStyles from '@hooks/useThemeStyles';
12+
import useWindowDimensions from '@hooks/useWindowDimensions';
1113

1214
type SingleSelectItem<T> = {
1315
text: string;
@@ -29,22 +31,71 @@ type SingleSelectPopupProps<T> = {
2931

3032
/** Function to call when changes are applied */
3133
onChange: (item: SingleSelectItem<T> | null) => void;
34+
35+
/** Whether the search input should be displayed */
36+
isSearchable?: boolean;
37+
38+
/** Search input place holder */
39+
searchPlaceholder?: string;
3240
};
3341

34-
function SingleSelectPopup<T extends string>({label, value, items, closeOverlay, onChange}: SingleSelectPopupProps<T>) {
42+
function SingleSelectPopup<T extends string>({label, value, items, closeOverlay, onChange, isSearchable, searchPlaceholder}: SingleSelectPopupProps<T>) {
3543
const {translate} = useLocalize();
3644
const styles = useThemeStyles();
3745
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
3846
const {isSmallScreenWidth} = useResponsiveLayout();
47+
const {windowHeight} = useWindowDimensions();
3948
const [selectedItem, setSelectedItem] = useState(value);
49+
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
50+
51+
const {sections, noResultsFound} = useMemo(() => {
52+
// If the selection is searchable, we push the initially selected item into its own section and display it at the top
53+
if (isSearchable) {
54+
const initiallySelectedItemSection = value?.text.toLowerCase().includes(debouncedSearchTerm?.toLowerCase())
55+
? [{text: value.text, keyForList: value.value, isSelected: selectedItem?.value === value.value}]
56+
: [];
57+
const remainingItemsSection = items
58+
.filter((item) => item?.value !== value?.value && item?.text?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase()))
59+
.map((item) => ({
60+
text: item.text,
61+
keyForList: item.value,
62+
isSelected: selectedItem?.value === item.value,
63+
}));
64+
const isEmpty = !initiallySelectedItemSection.length && !remainingItemsSection.length;
65+
return {
66+
sections: isEmpty
67+
? []
68+
: [
69+
{
70+
data: initiallySelectedItemSection,
71+
shouldShow: initiallySelectedItemSection.length > 0,
72+
indexOffset: 0,
73+
},
74+
{
75+
data: remainingItemsSection,
76+
shouldShow: remainingItemsSection.length > 0,
77+
indexOffset: initiallySelectedItemSection.length,
78+
},
79+
],
80+
noResultsFound: isEmpty,
81+
};
82+
}
83+
84+
return {
85+
sections: [
86+
{
87+
data: items.map((item) => ({
88+
text: item.text,
89+
keyForList: item.value,
90+
isSelected: item.value === selectedItem?.value,
91+
})),
92+
},
93+
],
94+
noResultsFound: false,
95+
};
96+
}, [isSearchable, items, value, selectedItem, debouncedSearchTerm]);
4097

41-
const listData: ListItem[] = useMemo(() => {
42-
return items.map((item) => ({
43-
text: item.text,
44-
keyForList: item.value,
45-
isSelected: item.value === selectedItem?.value,
46-
}));
47-
}, [items, selectedItem]);
98+
const dataLength = useMemo(() => sections.flatMap((section) => section.data).length, [sections]);
4899

49100
const updateSelectedItem = useCallback(
50101
(item: ListItem) => {
@@ -68,12 +119,19 @@ function SingleSelectPopup<T extends string>({label, value, items, closeOverlay,
68119
<View style={[!isSmallScreenWidth && styles.pv4, styles.gap2]}>
69120
{isSmallScreenWidth && <Text style={[styles.textLabel, styles.textSupporting, styles.ph5, styles.pv1]}>{label}</Text>}
70121

71-
<View style={[styles.getSelectionListPopoverHeight(items.length)]}>
122+
<View style={[styles.getSelectionListPopoverHeight(dataLength || 1, windowHeight, isSearchable ?? false)]}>
72123
<SelectionList
73124
shouldSingleExecuteRowSelect
74-
sections={[{data: listData}]}
125+
sections={sections}
75126
ListItem={SingleSelectListItem}
76127
onSelectRow={updateSelectedItem}
128+
textInputValue={searchTerm}
129+
onChangeText={setSearchTerm}
130+
textInputLabel={isSearchable ? (searchPlaceholder ?? translate('common.search')) : undefined}
131+
shouldUpdateFocusedIndex={isSearchable}
132+
initiallyFocusedOptionKey={isSearchable ? value?.value : undefined}
133+
headerMessage={noResultsFound ? translate('common.noResultsFound') : undefined}
134+
showLoadingPlaceholder={!noResultsFound}
77135
/>
78136
</View>
79137
<View style={[styles.flexRow, styles.gap2, styles.ph5]}>

src/components/Search/SearchAutocompleteList.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
getQueryWithoutAutocompletedPart,
3232
parseForAutocomplete,
3333
} from '@libs/SearchAutocompleteUtils';
34-
import {buildSearchQueryJSON, buildUserReadableQueryString, getQueryWithoutFilters, sanitizeSearchValue, shouldHighlight} from '@libs/SearchQueryUtils';
34+
import {buildSearchQueryJSON, buildUserReadableQueryString, getQueryWithoutFilters, getUserFriendlyKey, sanitizeSearchValue, shouldHighlight} from '@libs/SearchQueryUtils';
3535
import {getDatePresets} from '@libs/SearchUIUtils';
3636
import StringUtils from '@libs/StringUtils';
3737
import Timing from '@userActions/Timing';
@@ -307,15 +307,16 @@ function SearchAutocompleteList(
307307
text: categoryName,
308308
}));
309309
}
310-
case CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY: {
310+
case CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY:
311+
case CONST.SEARCH.SYNTAX_FILTER_KEYS.GROUP_CURRENCY: {
311312
const autocompleteList = autocompleteValue ? currencyAutocompleteList : (recentCurrencyAutocompleteList ?? []);
312313
const filteredCurrencies = autocompleteList
313314
.filter((currency) => currency.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(currency.toLowerCase()))
314315
.sort()
315316
.slice(0, 10);
316317

317318
return filteredCurrencies.map((currencyName) => ({
318-
filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.CURRENCY,
319+
filterKey: getUserFriendlyKey(autocompleteKey),
319320
text: currencyName,
320321
}));
321322
}
@@ -460,7 +461,7 @@ function SearchAutocompleteList(
460461
case CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWN:
461462
case CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPORTED:
462463
case CONST.SEARCH.SYNTAX_FILTER_KEYS.POSTED: {
463-
const filteredDatePresets = (getDatePresets(autocompleteKey, true) ?? [])
464+
const filteredDatePresets = getDatePresets(autocompleteKey, true)
464465
.filter((datePreset) => datePreset.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(datePreset.toLowerCase()))
465466
.sort()
466467
.slice(0, 10);

0 commit comments

Comments
 (0)