Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
55b608d
revamp search page actions bar
bernhardoj Mar 17, 2026
8ffd9da
add other translations
bernhardoj Mar 17, 2026
58e2fed
lint
bernhardoj Mar 17, 2026
1154d89
lint
bernhardoj Mar 17, 2026
65d7690
fix wrong hooks being used
bernhardoj Mar 17, 2026
3e2eefd
fix extra spaces and label
bernhardoj Mar 17, 2026
3f42ae1
fix search name is saved as empty string
bernhardoj Mar 17, 2026
83f189d
don't include unfound card
bernhardoj Mar 17, 2026
a7aa479
make it optional
bernhardoj Mar 17, 2026
afca3f7
fix test
bernhardoj Mar 17, 2026
3b92af9
remove IIFE pattern
bernhardoj Mar 17, 2026
ef3265d
create a map lookup to optimize
bernhardoj Mar 17, 2026
44e8d17
prettier
bernhardoj Mar 17, 2026
2b80b34
drying
bernhardoj Mar 17, 2026
51784c7
remove unnecessary props
bernhardoj Mar 17, 2026
c729b9d
remove unused var
bernhardoj Mar 17, 2026
b379b42
simplify
bernhardoj Mar 17, 2026
1805dc0
Merge branch 'main' into feat/81558-revamp-search-actions-bar
bernhardoj Mar 18, 2026
b3f658a
replace for indexed with for..of
bernhardoj Mar 18, 2026
48ed825
return as string
bernhardoj Mar 18, 2026
e23fa58
prettier
bernhardoj Mar 18, 2026
0460060
move sortbypopup to a separate file
bernhardoj Mar 18, 2026
b57ce18
lint
bernhardoj Mar 18, 2026
fb26bfe
Merge branch 'main' into feat/81558-revamp-search-actions-bar
bernhardoj Mar 19, 2026
4ce7b25
fixed popover position
bernhardoj Mar 19, 2026
9ed7bab
Merge branch 'main' into feat/81558-revamp-search-actions-bar
bernhardoj Mar 19, 2026
8daac00
remove log
bernhardoj Mar 19, 2026
89125e0
remove unused var
bernhardoj Mar 19, 2026
359db31
only render the revamped search actions bar if it's on staging/dev
bernhardoj Mar 19, 2026
f332aa3
don't show the new component yet
bernhardoj Mar 20, 2026
d55abd9
remove unused import
bernhardoj Mar 20, 2026
0d3b6ae
fix test
bernhardoj Mar 20, 2026
0e07500
fix missing style
bernhardoj Mar 20, 2026
a3dc7db
update test
bernhardoj Mar 20, 2026
7aaf5eb
remove unused prop
bernhardoj Mar 20, 2026
5700da2
remove unused prop
bernhardoj Mar 20, 2026
060b191
fix wrong name
bernhardoj Mar 20, 2026
abaec93
make the name consistent
bernhardoj Mar 20, 2026
754b10b
add sentry label
bernhardoj Mar 20, 2026
a91a01a
refactor to use formprovider and make it autofocus
bernhardoj Mar 20, 2026
bedfb8a
show groupby for trip too
bernhardoj Mar 20, 2026
e85edf4
extract same login into hook
bernhardoj Mar 21, 2026
ee777a6
readd the extra padding
bernhardoj Mar 21, 2026
7d3d018
remove search save page for now
bernhardoj Mar 21, 2026
92b3baa
don't dismiss modal on narrow layout
bernhardoj Mar 21, 2026
3dba7ce
Merge branch 'main' into feat/81558-revamp-search-actions-bar
bernhardoj Mar 21, 2026
1efadbf
fix onyxkeys typing
bernhardoj Mar 21, 2026
3c07540
lint
bernhardoj Mar 21, 2026
dcbb745
fix error after merging main
bernhardoj Mar 21, 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
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8597,6 +8597,7 @@ const CONST = {
REPORT_EXPAND_COLLAPSE: 'Search-ReportExpandCollapse',
SELECT_ALL_BUTTON: 'Search-SelectAllButton',
TYPE_MENU_BUTTON: 'Search-TypeMenuButton',
FILTER_DISPLAY: 'Search-FilterDisplay',
FILTER_TYPE: 'Search-FilterType',
FILTER_STATUS: 'Search-FilterStatus',
FILTER_DATE: 'Search-FilterDate',
Expand Down Expand Up @@ -8625,6 +8626,8 @@ const CONST = {
FILTER_POPUP_APPLY_SINGLE_SELECT: 'Search-FilterPopupApplySingleSelect',
FILTER_POPUP_RESET_MULTI_SELECT: 'Search-FilterPopupResetMultiSelect',
FILTER_POPUP_APPLY_MULTI_SELECT: 'Search-FilterPopupApplyMultiSelect',
FILTER_POPUP_RESET_TEXT_INPUT: 'Search-FilterPopupResetTextInput',
FILTER_POPUP_APPLY_TEXT_INPUT: 'Search-FilterPopupApplyTextInput',
FILTER_POPUP_RESET_DATE: 'Search-FilterPopupResetDate',
FILTER_POPUP_APPLY_DATE: 'Search-FilterPopupApplyDate',
FILTER_POPUP_RESET_USER: 'Search-FilterPopupResetUser',
Expand Down
3 changes: 3 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,8 @@ const ONYXKEYS = {
SAGE_INTACCT_DIMENSION_TYPE_FORM_DRAFT: 'sageIntacctDimensionTypeFormDraft',
SEARCH_ADVANCED_FILTERS_FORM: 'searchAdvancedFiltersForm',
SEARCH_ADVANCED_FILTERS_FORM_DRAFT: 'searchAdvancedFiltersFormDraft',
SEARCH_SAVE_FORM: 'searchSaveForm',
SEARCH_SAVE_FORM_DRAFT: 'searchSaveFormDraft',
SEARCH_SAVED_SEARCH_RENAME_FORM: 'searchSavedSearchRenameForm',
SEARCH_SAVED_SEARCH_RENAME_FORM_DRAFT: 'searchSavedSearchRenameFormDraft',
TEXT_PICKER_MODAL_FORM: 'textPickerModalForm',
Expand Down Expand Up @@ -1163,6 +1165,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AGE_FORM]: FormTypes.RulesMaxExpenseAgeForm;
[ONYXKEYS.FORMS.RULES_CUSTOM_FORM]: FormTypes.RulesCustomForm;
[ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm;
[ONYXKEYS.FORMS.SEARCH_SAVE_FORM]: FormTypes.SearchSaveForm;
[ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm | FormTypes.DebugTransactionForm | FormTypes.DebugTransactionViolationForm;
[ONYXKEYS.FORMS.ONBOARDING_WORK_EMAIL_FORM]: FormTypes.OnboardingWorkEmailForm;
[ONYXKEYS.FORMS.MERGE_ACCOUNT_DETAILS_FORM]: FormTypes.MergeAccountDetailsForm;
Expand Down
1 change: 1 addition & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const ROUTES = {
},
},
SEARCH_ROOT_VERIFY_ACCOUNT: `search/${VERIFY_ACCOUNT}`,
SEARCH_SAVE: 'search/save',
SEARCH_SAVED_SEARCH_RENAME: {
route: 'search/saved-search/rename',
getRoute: ({name, jsonQuery}: {name: string; jsonQuery: SearchQueryString}) => `search/saved-search/rename?name=${name}&q=${jsonQuery}` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ const SCREENS = {

SEARCH_COLUMNS: 'SearchColumns',
SEARCH_ADVANCED_FILTERS: 'SearchAdvancedFilters',
SEARCH_SAVE: 'SearchSave',
SEARCH_SAVED_SEARCH: 'SearchSavedSearch',
SETTINGS_CATEGORIES: 'SettingsCategories',
SETTINGS_TAGS: 'SettingsTags',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ function MoneyRequestReportTransactionList({
<View style={[styles.flexRow, styles.gap2, styles.alignItemsCenter, styles.ph5, styles.pb2]}>
{shouldShowGroupedTransactions && (
<DropdownButton
label={translate('search.groupBy')}
label={translate('search.display.groupBy')}
value={selectedGroupByItem?.text ?? ''}
PopoverComponent={groupByPopoverComponent}
/>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Navigation/SearchSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import type {PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
import SearchTypeMenu from '@pages/Search/SearchTypeMenu';
import SearchTypeMenuWide from '@pages/Search/SearchTypeMenuWide';
import SCREENS from '@src/SCREENS';
import NavigationTabBar from './NavigationTabBar';
import NAVIGATION_TABS from './NavigationTabBar/NAVIGATION_TABS';
Expand Down Expand Up @@ -55,7 +55,7 @@ function SearchSidebar({state}: SearchSidebarProps) {
shouldDisplaySearch={false}
shouldDisplayHelpButton={false}
/>
<SearchTypeMenu queryJSON={currentSearchQueryJSON} />
<SearchTypeMenuWide queryJSON={currentSearchQueryJSON} />
</View>
<NavigationTabBar selectedTab={NAVIGATION_TABS.SEARCH} />
</View>
Expand Down
205 changes: 205 additions & 0 deletions src/components/Search/FilterDropdowns/DisplayPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import React, {useState} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import type {SearchQueryJSON} from '@components/Search/types';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {close} from '@libs/actions/Modal';
import Navigation from '@libs/Navigation/Navigation';
import {buildFilterQueryWithSortDefaults} from '@libs/SearchQueryUtils';
import {getGroupBySections, getSearchColumnTranslationKey, getViewOptions} from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
import type {SearchResults} from '@src/types/onyx';
import {getEmptyObject} from '@src/types/utils/EmptyObject';
import GroupByPopup from './GroupByPopup';
import SingleSelectPopup from './SingleSelectPopup';
import SortByPopup from './SortByPopup';
import TextInputPopup from './TextInputPopup';

type DisplayPopupProps = {
queryJSON: SearchQueryJSON;
searchResults: OnyxEntry<SearchResults>;
closeOverlay: () => void;
onSort: () => void;
};

function DisplayPopup({queryJSON, searchResults, closeOverlay, onSort}: DisplayPopupProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {shouldUseNarrowLayout, isLargeScreenWidth} = useResponsiveLayout();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Columns']);
const [searchAdvancedFilters = getEmptyObject<SearchAdvancedFiltersForm>()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
const [selectedDisplayFilter, setSelectedDisplayFilter] = useState<
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.LIMIT
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY
| null
>(null);

const groupBySections = getGroupBySections(translate);
const groupBy = groupBySections.flatMap((section) => section.options).find((option) => option.value === queryJSON.groupBy) ?? null;
const viewOptions = getViewOptions(translate);
const view = viewOptions.find((option) => option.value === queryJSON.view) ?? viewOptions.at(0) ?? null;
const shouldShowColumnsButton = isLargeScreenWidth && (queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE || queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT);

const sortByValue = queryJSON.sortBy;
const groupByValue = searchAdvancedFilters[CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY];
const viewValue = searchAdvancedFilters[CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW];
const limitValue = searchAdvancedFilters[CONST.SEARCH.SYNTAX_ROOT_KEYS.LIMIT];

if (!selectedDisplayFilter) {
const openSearchColumns = () => {
Navigation.navigate(ROUTES.SEARCH_COLUMNS);
};

const isExpenseType = queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE;
const isTripType = queryJSON.type === CONST.SEARCH.DATA_TYPES.TRIP;
return (
<View style={[!shouldUseNarrowLayout && styles.pv4]}>
<MenuItemWithTopDescription
shouldShowRightIcon
description={translate('search.display.sortBy')}
title={translate(getSearchColumnTranslationKey(sortByValue))}
onPress={() => setSelectedDisplayFilter(CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY)}
/>
{(isExpenseType || isTripType) && (
<MenuItemWithTopDescription
shouldShowRightIcon
description={translate('search.display.groupBy')}
title={groupByValue ? translate(`search.filters.groupBy.${groupByValue}`) : undefined}
onPress={() => setSelectedDisplayFilter(CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY)}
/>
)}
{isExpenseType && !!groupByValue && (
<MenuItemWithTopDescription
shouldShowRightIcon
description={translate('search.view.label')}
title={viewValue ? translate(`search.view.${viewValue}`) : undefined}
onPress={() => setSelectedDisplayFilter(CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW)}
/>
)}
{isExpenseType && (
<MenuItemWithTopDescription
shouldShowRightIcon
description={translate('search.display.limitResults')}
title={limitValue}
onPress={() => setSelectedDisplayFilter(CONST.SEARCH.SYNTAX_ROOT_KEYS.LIMIT)}
/>
)}
{shouldShowColumnsButton && (
<MenuItem
icon={expensifyIcons.Columns}
title={translate('search.editColumns')}
onPress={() => {
closeOverlay();
openSearchColumns();
}}
sentryLabel={CONST.SENTRY_LABEL.SEARCH.COLUMNS_BUTTON}
/>
)}
</View>
);
}

const updateFilterForm = (values: Partial<SearchAdvancedFiltersForm>) => {
const updatedFilterFormValues: Partial<SearchAdvancedFiltersForm> = {
...searchAdvancedFilters,
...values,
};

if (updatedFilterFormValues.groupBy !== searchAdvancedFilters.groupBy) {
updatedFilterFormValues.columns = [];
}

const queryString =
buildFilterQueryWithSortDefaults(
updatedFilterFormValues,
{view: searchAdvancedFilters.view, groupBy: searchAdvancedFilters.groupBy},
{sortBy: queryJSON.sortBy, sortOrder: queryJSON.sortOrder, limit: queryJSON.limit},
) ?? '';
if (!queryString) {
return;
}

close(() => Navigation.setParams({q: queryString, rawQuery: undefined}));
};

const subtitle = {
[CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY]: translate('search.display.sortBy'),
[CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY]: translate('search.display.groupBy'),
[CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW]: translate('search.view.label'),
[CONST.SEARCH.SYNTAX_ROOT_KEYS.LIMIT]: translate('search.display.limitResults'),
};

const subPopup = {
[CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY]: (
<SortByPopup
searchResults={searchResults}
queryJSON={queryJSON}
groupBy={groupBy}
onSort={onSort}
closeOverlay={() => setSelectedDisplayFilter(null)}
/>
),
[CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY]: (
<GroupByPopup
style={styles.p0}
label={shouldUseNarrowLayout ? undefined : translate('search.display.groupBy')}
sections={groupBySections}
value={groupBy}
closeOverlay={() => setSelectedDisplayFilter(null)}
onChange={(item) => {
const newValue = item?.value;
if (!newValue) {
updateFilterForm({groupBy: undefined, groupCurrency: undefined});
} else {
updateFilterForm({groupBy: newValue});
}
}}
/>
),
[CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW]: (
<SingleSelectPopup
style={styles.p0}
label={shouldUseNarrowLayout ? undefined : translate('search.view.label')}
items={viewOptions}
value={view}
closeOverlay={() => setSelectedDisplayFilter(null)}
onChange={(item) => updateFilterForm({view: item?.value ?? CONST.SEARCH.VIEW.TABLE})}
/>
),
[CONST.SEARCH.SYNTAX_ROOT_KEYS.LIMIT]: (
<TextInputPopup
style={styles.pv0}
placeholder={translate('search.filters.limit')}
defaultValue={limitValue}
closeOverlay={() => setSelectedDisplayFilter(null)}
onChange={(value) => updateFilterForm({limit: value})}
/>
),
};

return (
<View style={[!shouldUseNarrowLayout && styles.pv4]}>
<HeaderWithBackButton
shouldDisplayHelpButton={false}
style={[styles.h10]}
subtitle={subtitle[selectedDisplayFilter]}
onBackButtonPress={() => setSelectedDisplayFilter(null)}
/>
{subPopup[selectedDisplayFilter]}
</View>
);
}

export default DisplayPopup;
2 changes: 1 addition & 1 deletion src/components/Search/FilterDropdowns/DropdownButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,5 +183,5 @@ function DropdownButton({label, value, viewportOffsetTop, PopoverComponent, medi
);
}

export type {PopoverComponentProps};
export type {PopoverComponentProps, DropdownButtonProps};
export default withViewportOffsetTop(DropdownButton);
7 changes: 5 additions & 2 deletions src/components/Search/FilterDropdowns/GroupByPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
import type {StyleProp, ViewStyle} from 'react-native';
import Button from '@components/Button';
import type {SearchGroupBy} from '@components/Search/types';
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
Expand Down Expand Up @@ -28,14 +29,16 @@ type GroupByPopupProps = {
/** The currently selected item */
value: GroupByPopupItem | null;

style?: StyleProp<ViewStyle>;

/** Function to call to close the overlay when changes are applied */
closeOverlay: () => void;

/** Function to call when changes are applied */
onChange: (item: GroupByPopupItem | null) => void;
};

function GroupByPopup({label, value, sections, closeOverlay, onChange}: GroupByPopupProps) {
function GroupByPopup({label, value, sections, style, closeOverlay, onChange}: GroupByPopupProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
Expand Down Expand Up @@ -83,7 +86,7 @@ function GroupByPopup({label, value, sections, closeOverlay, onChange}: GroupByP
}, [closeOverlay, onChange]);

return (
<View style={[!isSmallScreenWidth && styles.pv4, styles.gap2]}>
<View style={[!isSmallScreenWidth && styles.pv4, styles.gap2, style]}>
{isSmallScreenWidth && !!label && <Text style={[styles.textLabel, styles.textSupporting, styles.ph5, styles.pv1]}>{label}</Text>}

<View style={[styles.getSelectionListPopoverHeight(optionsCount, windowHeight, false)]}>
Expand Down
18 changes: 16 additions & 2 deletions src/components/Search/FilterDropdowns/SingleSelectPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
import type {StyleProp, ViewStyle} from 'react-native';
import Button from '@components/Button';
import SelectionList from '@components/SelectionList';
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
Expand Down Expand Up @@ -42,11 +43,24 @@ type SingleSelectPopupProps<T> = {
/** The default value to set when reset is clicked */
defaultValue?: string;

style?: StyleProp<ViewStyle>;

/** Custom styles for the SelectionList */
selectionListStyle?: SelectionListStyle;
};

function SingleSelectPopup<T extends string>({label, value, items, closeOverlay, onChange, isSearchable, searchPlaceholder, defaultValue, selectionListStyle}: SingleSelectPopupProps<T>) {
function SingleSelectPopup<T extends string>({
label,
value,
items,
closeOverlay,
onChange,
isSearchable,
searchPlaceholder,
defaultValue,
style,
selectionListStyle,
}: SingleSelectPopupProps<T>) {
const {translate} = useLocalize();
const styles = useThemeStyles();
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
Expand Down Expand Up @@ -117,7 +131,7 @@ function SingleSelectPopup<T extends string>({label, value, items, closeOverlay,
const shouldShowLabel = isSmallScreenWidth && !!label;

return (
<View style={[!isSmallScreenWidth && styles.pv4, styles.gap2]}>
<View style={[!isSmallScreenWidth && styles.pv4, styles.gap2, style]}>
{shouldShowLabel && <Text style={[styles.textLabel, styles.textSupporting, styles.ph5, styles.pv1]}>{label}</Text>}

<View style={[styles.getSelectionListPopoverHeight(options.length || 1, windowHeight, isSearchable ?? false)]}>
Expand Down
Loading
Loading