Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
6afbc11
feat: add footerCount, footerTotal and footerCurrency query keys for …
truph01 Sep 9, 2026
29c3230
feat: add the count selector to the Spend footer
truph01 Sep 9, 2026
510bcb1
feat: add the count and spend total selectors to the Spend footer
truph01 Sep 9, 2026
e9a0dde
fix: conflicts
truph01 Sep 11, 2026
94dd759
fix: conflicts
truph01 Sep 11, 2026
493916d
fix: typecheck
truph01 Sep 11, 2026
c888775
fix: move the footer selections into the query filters the backend reads
truph01 Sep 11, 2026
45dbf72
fix: conflicts
truph01 Sep 11, 2026
4c98c49
fix: conflicts
truph01 Sep 11, 2026
15789f3
fix: conflicts
truph01 Sep 16, 2026
f53a2ac
fix: reduce scope of this PR
truph01 Sep 17, 2026
66a7f0e
fix: lint-changed
truph01 Sep 17, 2026
0b00ac0
Merge branch 'Expensify:main' into feat/99452
truph01 Sep 17, 2026
4d38602
fix: remove useCallbacks func
truph01 Sep 17, 2026
507d4ec
fix: update comment
truph01 Sep 17, 2026
e87151c
feat: highlight the Spend footer selections in the search input
truph01 Sep 17, 2026
5ec47fc
fix: keep the filters Reset button hidden for footer selections
truph01 Sep 17, 2026
91ca920
fix: skeleton the footer total only for the reload it asked for
truph01 Sep 18, 2026
75d7273
fix: hold the footer size while the total skeleton stands in
truph01 Sep 18, 2026
530ac97
fix: conflicts
truph01 Sep 21, 2026
6099e4d
fix: remove total from hash
truph01 Sep 22, 2026
3ca2199
fix: conflicts
truph01 Sep 22, 2026
2f1aa77
fix: conflicts
truph01 Sep 22, 2026
fc03f9e
fix: conflicts
truph01 Sep 22, 2026
81427f8
fix: test
truph01 Sep 22, 2026
5f6f484
Merge branch 'Expensify:main' into feat/99452
truph01 Sep 23, 2026
dac4417
fix: make whole footer clickable
truph01 Sep 23, 2026
13deba7
fix: lint
truph01 Sep 23, 2026
f093b81
fix: lint
truph01 Sep 23, 2026
7088fc5
Merge branch 'Expensify:main' into feat/99452
truph01 Sep 24, 2026
698ab7f
feat: key the search snapshot on the Spend footer total
truph01 Sep 24, 2026
7310577
fix: typecheck
truph01 Sep 24, 2026
0b53b60
fix: conflicts
truph01 Sep 24, 2026
1787b6b
fix: conflicts
truph01 Sep 24, 2026
d696036
fix: correct the Spend footer's report count, breakdown rule and pend…
truph01 Sep 24, 2026
23693d9
fix: lint
truph01 Sep 25, 2026
77d1e88
fix: conflicts
truph01 Sep 25, 2026
3d78ab8
fix: conflicts
truph01 Sep 25, 2026
6f74c66
fix: typecheck
truph01 Sep 25, 2026
3a31c09
fix: skeleton the Spend footer total while its search is fetched again
truph01 Sep 25, 2026
dbd1888
fix: test
truph01 Sep 25, 2026
b9be6ba
Merge branch 'Expensify:main' into feat/99452
truph01 Sep 26, 2026
41d5498
fix: crash issue and remove redundant changes
truph01 Sep 26, 2026
a95a916
fix: show footerCount in todo search
truph01 Sep 26, 2026
c36c150
fix: test
truph01 Sep 26, 2026
60a6040
fix: lint
truph01 Sep 26, 2026
0665516
fix: remove redundant change
truph01 Sep 26, 2026
7804698
fix: remove redundant change
truph01 Sep 26, 2026
6aff2cd
fix: correct total calculation in live onyx data
truph01 Sep 26, 2026
180d60d
fix: typecheck
truph01 Sep 26, 2026
23d1b0f
fix: remove redundant skeleton effect
truph01 Sep 26, 2026
a6cae54
fix: lint
truph01 Sep 26, 2026
f334541
resolve conflicts
truph01 Sep 29, 2026
16b5279
resolve conflicts
truph01 Sep 29, 2026
a0430da
fix: keep the Spend list and selection while a footer total loads
truph01 Sep 29, 2026
bdd82b0
fix: stop a Spend footer selection from making a search look filtered
truph01 Sep 29, 2026
1371ca6
fix: leave the default footer total out of the search query
truph01 Sep 29, 2026
e55e2a4
fix: test
truph01 Sep 29, 2026
4b6e603
Merge branch 'Expensify:main' into feat/99452
truph01 Sep 29, 2026
f51b7a3
feat: add beta flag
truph01 Sep 29, 2026
d640f6e
Merge branch 'Expensify:main' into feat/99452
truph01 Sep 30, 2026
797c34f
fix: conflicts
truph01 Oct 1, 2026
949c9d1
fix: conflicts
truph01 Oct 1, 2026
1241b3c
fix: keep the search selection across a Spend footer total switch
truph01 Oct 1, 2026
794570a
fix: stop the Spend footer total from skeletoning forever on a to-do …
truph01 Oct 1, 2026
9c56336
fix: remove redundant check
truph01 Oct 1, 2026
691aa90
fix: comment
truph01 Oct 1, 2026
b92eb53
fix: remove redundant changes in SearchPage
truph01 Oct 1, 2026
1763e75
fix: revert solution
truph01 Oct 1, 2026
b989951
fix: don't hold another search's results on a to-do search
truph01 Oct 2, 2026
cbf34dd
fix: selected rows disappear when togging footerTotal
truph01 Oct 2, 2026
0eb3075
fix: gate the Spend footer UI behind the beta, not just its data
truph01 Oct 2, 2026
1c26b5e
fix: ignore footer selections in the query when the beta is off
truph01 Oct 2, 2026
1aa84d2
fix: keep optimistic Search updates working when the query carries fo…
truph01 Oct 2, 2026
24c1853
fix: spell
truph01 Oct 2, 2026
47823c2
fix: conflicts
truph01 Oct 6, 2026
990d38f
fix: conflicts
truph01 Oct 6, 2026
e96e955
fix: conflicts
truph01 Oct 8, 2026
fccfb11
fix: conflicts
truph01 Oct 8, 2026
f4f777a
fix: take unticked reports off the footer's report count on a select-all
truph01 Oct 8, 2026
6915e4f
fix: name the figure the Spend footer shows when a group selection hi…
truph01 Oct 8, 2026
2afac19
fix: typecheck
truph01 Oct 8, 2026
2fbe291
refactor: keep the footer total out of the search hash
truph01 Oct 8, 2026
2619b03
fix: typecheck
truph01 Oct 8, 2026
8f0b0cf
Merge branch 'Expensify:main' into feat/99452
truph01 Oct 8, 2026
4b8e82a
fix: hide a footer total offline that belongs to another breakdown
truph01 Oct 8, 2026
3768ff0
fix: show a footer total offline from when that breakdown was last an…
truph01 Oct 9, 2026
2d7080f
fix: conflicts
truph01 Oct 9, 2026
2c9bfc8
fix: conflicts
truph01 Oct 9, 2026
dabc33a
fix: Count an unticked empty report off the select-all report count
truph01 Oct 9, 2026
dcc58da
fix: Re-ask for a footer breakdown when the snapshot total answers an…
truph01 Oct 9, 2026
01414aa
fix: Keep Home's spend totals off a footer breakdown answered on the …
truph01 Oct 9, 2026
9ec9bc0
fix: Skip the footer breakdown request offline and judge the offline …
truph01 Oct 9, 2026
66e91ab
fix: remove log
truph01 Oct 9, 2026
fa7ebf9
fix: conflicts
truph01 Oct 11, 2026
2d13737
fix: conflicts
truph01 Oct 11, 2026
3ebb0b8
fix: Subtract only an excluded report's matching expenses under a bre…
truph01 Oct 11, 2026
42256cc
fix: Hide the footer total when a collapsed group is excluded under a…
truph01 Oct 11, 2026
10ab486
fix: Leave the search type's default count out of the query
truph01 Oct 11, 2026
fb72cd8
fix: Take Home's spend totals only from a snapshot stamped as the pla…
truph01 Oct 11, 2026
6e6d4ab
fix: apply reviewer suggestion
truph01 Oct 12, 2026
fe2c813
fix: Keep the Total selector when an excluded collapsed group hides t…
truph01 Oct 12, 2026
55ad155
fix: Trust a settled unstamped snapshot as Home's plain total, but no…
truph01 Oct 12, 2026
a4ef5c1
fix: Ignore only the footer keys when deciding on an optimistic Searc…
truph01 Oct 12, 2026
b9c7060
fix: apply AI's suggestions
truph01 Oct 12, 2026
99fed98
fix: update comment
truph01 Oct 12, 2026
0a0245f
Merge branch 'Expensify:main' into feat/99452
truph01 Oct 13, 2026
93676bc
Merge branch 'Expensify:main' into feat/99452
truph01 Oct 13, 2026
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
26 changes: 26 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,7 @@ const CONST = {
INSIGHTS_PAGE: 'insightsPage',
INSIGHTS_COMPARE: 'insightsCompare',
PAYMENT_HISTORY: 'paymentHistory',
SPEND_FOOTER_SELECTORS: 'spendFooterSelectors',
SUPPORT_TICKET: 'supportTicket',
ANCHORED_FIELD_DROPDOWNS: 'anchoredFieldDropdowns',
RESIZABLE_TABLE_COLUMNS: 'resizableTableColumns',
Expand Down Expand Up @@ -8243,6 +8244,19 @@ const CONST = {
LINE: 'line',
PIE: 'pie',
},
/** Which of the counts every search returns the Spend footer displays. Applied client-side, so it never retriggers a search. */
FOOTER_COUNT: {
EXPENSES: 'expenses',
REPORTS: 'reports',
},
/** Which aggregate the Spend footer displays as its total. Changing it retriggers the search, since the backend computes the aggregate. */
FOOTER_TOTAL: {
TOTAL: 'total',
REIMBURSABLE: 'reimbursable',
NON_REIMBURSABLE: 'non-reimbursable',
BILLABLE: 'billable',
NON_BILLABLE: 'non-billable',
},
// Comparison modes for Insights queries, defined by the API.
COMPARE: {
PREVIOUS_PERIOD: 'previousPeriod',
Expand Down Expand Up @@ -8305,6 +8319,12 @@ const CONST = {
SUBMITTER_USER_ID: 'submitterUserID',
SUBMITTER_PAYROLL_ID: 'submitterPayrollID',
ORDER_DEAL_NUMBERS: 'orderDealNumbers',
// The Spend footer's display selections. They travel as filters because that is where the backend reads
// them from, but they never narrow the result set: only `footerTotal` changes the response, by swapping
// which aggregate comes back as the search total.
FOOTER_COUNT: 'footerCount',
FOOTER_TOTAL: 'footerTotal',
FOOTER_CURRENCY: 'footerCurrency',
},
REPORT_FIELD: {
// All report fields start with this, so use this to check if a search key is a report field
Expand Down Expand Up @@ -8390,6 +8410,9 @@ const CONST = {
ORDER_DEAL_NUMBERS: 'order-deal-numbers',
COLUMNS: 'columns',
LIMIT: 'limit',
FOOTER_COUNT: 'footer-count',
FOOTER_TOTAL: 'footer-total',
FOOTER_CURRENCY: 'footer-currency',
COMPARE: 'compare',
},
get SEARCH_USER_FRIENDLY_VALUES_MAP() {
Expand Down Expand Up @@ -9657,6 +9680,9 @@ const CONST = {
FILTER_GROUP_CURRENCY: 'Search-FilterGroupCurrency',
FILTER_VIEW: 'Search-FilterView',
FILTER_LIMIT: 'Search-FilterLimit',
FOOTER_COUNT: 'Search-FooterCount',
FOOTER_TOTAL: 'Search-FooterTotal',
FOOTER_CURRENCY: 'Search-FooterCurrency',
ADVANCED_FILTERS_BUTTON: 'Search-AdvancedFiltersButton',
COLUMNS_BUTTON: 'Search-ColumnsButton',
SELECT_ALL_CHECKBOX: 'Search-SelectAllCheckbox',
Expand Down
176 changes: 176 additions & 0 deletions src/components/Search/FilterDropdowns/SearchFooterPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScrollView from '@components/ScrollView';
import type {SingleSelectItem} from '@components/Search/FilterComponents/SingleSelect';
import type {SearchFooterCount, SearchFooterTotal} from '@components/Search/types';

import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';

import {getFooterTotalItems} from '@libs/SearchUIUtils';

import CONST from '@src/CONST';

import React, {useState} from 'react';

import CurrencyPopup from './CurrencyPopup';
import SingleSelectPopup from './SingleSelectPopup';

/** Which selector the menu has drilled into, or null while it is showing its list of rows */
type SelectedFooterFilter =
| typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.FOOTER_COUNT
| typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.FOOTER_TOTAL
| typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.FOOTER_CURRENCY
| null;

type SearchFooterPopupProps = {
/** The count the footer is currently displaying. Undefined hides the count row, e.g. while the footer describes a selection. */
countType: SearchFooterCount | undefined;

/** The count to fall back to when the count selector is reset */
defaultCountType: SearchFooterCount;

/** The total the footer is currently displaying. Undefined hides the total row, e.g. on an empty result set. */
totalType: SearchFooterTotal | undefined;

/** Whether the Total row is offered. A whole-group selection has no breakdown to pick, but still names its figure. */
shouldShowTotalSelector: boolean;

/** Whether the total and currency rows are waiting on a search, which recomputes both server-side */
isTotalLoading: boolean;

/** The currency the footer's total is currently displayed in */
currency: string | undefined;

/** The currency to fall back to when the currency selector is reset */
defaultCurrency: string | undefined;

/** Whether the popover is open. Keeps the currency list mounted only while it can be seen. */
isExpanded: boolean;

/** Function to call to close the popover */
closeOverlay: () => void;

/** Function to call when the displayed count changes */
onCountChange: (countType: SearchFooterCount) => void;

/** Function to call when the displayed total changes */
onTotalChange: (totalType: SearchFooterTotal) => void;

/** Function to call when the displayed currency changes */
onCurrencyChange: (item: SingleSelectItem<string> | undefined) => void;
};

/**
* The Spend footer's display menu: a row per selector, each opening its own sub-popup with Apply and Reset. Mirrors
* the Display menu in the search header, so the two behave the same way.
*/
function SearchFooterPopup({
countType,
defaultCountType,
totalType,
shouldShowTotalSelector,
isTotalLoading,
currency,
defaultCurrency,
isExpanded,
closeOverlay,
onCountChange,
onTotalChange,
onCurrencyChange,
}: SearchFooterPopupProps) {
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const styles = useThemeStyles();
const [selectedFooterFilter, setSelectedFooterFilter] = useState<SelectedFooterFilter>(null);

const countItems: Array<SingleSelectItem<SearchFooterCount>> = [
{text: translate('common.expenses'), value: CONST.SEARCH.FOOTER_COUNT.EXPENSES},
{text: translate('common.reports'), value: CONST.SEARCH.FOOTER_COUNT.REPORTS},
];
const totalItems = getFooterTotalItems(translate);

if (!selectedFooterFilter) {
return (
<ScrollView contentContainerStyle={[styles.pv4]}>
{!!countType && (
<MenuItemWithTopDescription
shouldShowRightIcon
description={translate('common.count')}
title={countItems.find((item) => item.value === countType)?.text}
onPress={() => setSelectedFooterFilter(CONST.SEARCH.SYNTAX_FILTER_KEYS.FOOTER_COUNT)}
sentryLabel={CONST.SENTRY_LABEL.SEARCH.FOOTER_COUNT}
/>
)}
{shouldShowTotalSelector && !!totalType && (
<MenuItemWithTopDescription
shouldShowRightIcon
// Both rows re-run the search, so they are inert until the one in flight lands. The count row
// stays interactive, since switching it needs no request.
interactive={!isTotalLoading}
description={translate('common.total')}
title={totalItems.find((item) => item.value === totalType)?.text}
onPress={() => setSelectedFooterFilter(CONST.SEARCH.SYNTAX_FILTER_KEYS.FOOTER_TOTAL)}
sentryLabel={CONST.SENTRY_LABEL.SEARCH.FOOTER_TOTAL}
/>
)}
<MenuItemWithTopDescription
shouldShowRightIcon
interactive={!isTotalLoading && !isOffline}
description={translate('common.currency')}
title={currency ?? defaultCurrency}
onPress={() => setSelectedFooterFilter(CONST.SEARCH.SYNTAX_FILTER_KEYS.FOOTER_CURRENCY)}
sentryLabel={CONST.SENTRY_LABEL.SEARCH.FOOTER_CURRENCY}
/>
</ScrollView>
);
}

const goBack = () => setSelectedFooterFilter(null);

if (selectedFooterFilter === CONST.SEARCH.SYNTAX_FILTER_KEYS.FOOTER_COUNT) {
return (
<SingleSelectPopup
items={countItems}
value={countItems.find((item) => item.value === countType)}
label={translate('common.count')}
onBackButtonPress={goBack}
closeOverlay={closeOverlay}
// Reset falls back to the count that matches the search type, which is also what an unset selection shows.
onChange={(item) => onCountChange(item?.value ?? defaultCountType)}
defaultValue={defaultCountType}
/>
);
}

if (selectedFooterFilter === CONST.SEARCH.SYNTAX_FILTER_KEYS.FOOTER_TOTAL) {
return (
<SingleSelectPopup
items={totalItems}
value={totalItems.find((item) => item.value === totalType)}
label={translate('common.total')}
onBackButtonPress={goBack}
closeOverlay={closeOverlay}
onChange={(item) => onTotalChange(item?.value ?? CONST.SEARCH.FOOTER_TOTAL.TOTAL)}
defaultValue={CONST.SEARCH.FOOTER_TOTAL.TOTAL}
/>
);
}

return (
<CurrencyPopup
key={currency ?? defaultCurrency}
value={currency}
label={translate('common.currency')}
onBackButtonPress={goBack}
closeOverlay={closeOverlay}
onChange={onCurrencyChange}
searchPlaceholder={translate('common.search')}
defaultValue={defaultCurrency}
shouldShowList={isExpanded}
shouldUseFixedPopoverHeight
/>
);
}

export default SearchFooterPopup;
Loading
Loading