diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 4789213e0fff0..5147c21896808 100644
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -8595,6 +8595,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',
@@ -8623,6 +8624,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',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index cc6a4747174a2..132a9f85a8a40 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -997,6 +997,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',
@@ -1162,6 +1164,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;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 952a920d1f6e0..18197125ecd0d 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -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,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 51fffec8051df..491c38eb51e74 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -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',
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
index 1239c32f21e84..68f35d0e5261b 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
@@ -670,7 +670,7 @@ function MoneyRequestReportTransactionList({
{shouldShowGroupedTransactions && (
diff --git a/src/components/Navigation/SearchSidebar.tsx b/src/components/Navigation/SearchSidebar.tsx
index 806b7c5148c88..37d789ef0c75a 100644
--- a/src/components/Navigation/SearchSidebar.tsx
+++ b/src/components/Navigation/SearchSidebar.tsx
@@ -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';
@@ -55,7 +55,7 @@ function SearchSidebar({state}: SearchSidebarProps) {
shouldDisplaySearch={false}
shouldDisplayHelpButton={false}
/>
-
+
diff --git a/src/components/Search/FilterDropdowns/DisplayPopup.tsx b/src/components/Search/FilterDropdowns/DisplayPopup.tsx
new file mode 100644
index 0000000000000..4cbc8bbaefca1
--- /dev/null
+++ b/src/components/Search/FilterDropdowns/DisplayPopup.tsx
@@ -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;
+ 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()] = 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 (
+
+ setSelectedDisplayFilter(CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY)}
+ />
+ {(isExpenseType || isTripType) && (
+ setSelectedDisplayFilter(CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY)}
+ />
+ )}
+ {isExpenseType && !!groupByValue && (
+ setSelectedDisplayFilter(CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW)}
+ />
+ )}
+ {isExpenseType && (
+ setSelectedDisplayFilter(CONST.SEARCH.SYNTAX_ROOT_KEYS.LIMIT)}
+ />
+ )}
+ {shouldShowColumnsButton && (
+
+ );
+ }
+
+ const updateFilterForm = (values: Partial) => {
+ const updatedFilterFormValues: Partial = {
+ ...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]: (
+ setSelectedDisplayFilter(null)}
+ />
+ ),
+ [CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY]: (
+ 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]: (
+ setSelectedDisplayFilter(null)}
+ onChange={(item) => updateFilterForm({view: item?.value ?? CONST.SEARCH.VIEW.TABLE})}
+ />
+ ),
+ [CONST.SEARCH.SYNTAX_ROOT_KEYS.LIMIT]: (
+ setSelectedDisplayFilter(null)}
+ onChange={(value) => updateFilterForm({limit: value})}
+ />
+ ),
+ };
+
+ return (
+
+ setSelectedDisplayFilter(null)}
+ />
+ {subPopup[selectedDisplayFilter]}
+
+ );
+}
+
+export default DisplayPopup;
diff --git a/src/components/Search/FilterDropdowns/DropdownButton.tsx b/src/components/Search/FilterDropdowns/DropdownButton.tsx
index f0012ea465e7d..b1c66354c7189 100644
--- a/src/components/Search/FilterDropdowns/DropdownButton.tsx
+++ b/src/components/Search/FilterDropdowns/DropdownButton.tsx
@@ -183,5 +183,5 @@ function DropdownButton({label, value, viewportOffsetTop, PopoverComponent, medi
);
}
-export type {PopoverComponentProps};
+export type {PopoverComponentProps, DropdownButtonProps};
export default withViewportOffsetTop(DropdownButton);
diff --git a/src/components/Search/FilterDropdowns/GroupByPopup.tsx b/src/components/Search/FilterDropdowns/GroupByPopup.tsx
index 949152ff8dae7..a12e714ad25b5 100644
--- a/src/components/Search/FilterDropdowns/GroupByPopup.tsx
+++ b/src/components/Search/FilterDropdowns/GroupByPopup.tsx
@@ -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';
@@ -28,6 +29,8 @@ type GroupByPopupProps = {
/** The currently selected item */
value: GroupByPopupItem | null;
+ style?: StyleProp;
+
/** Function to call to close the overlay when changes are applied */
closeOverlay: () => void;
@@ -35,7 +38,7 @@ type GroupByPopupProps = {
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
@@ -83,7 +86,7 @@ function GroupByPopup({label, value, sections, closeOverlay, onChange}: GroupByP
}, [closeOverlay, onChange]);
return (
-
+
{isSmallScreenWidth && !!label && {label}}
diff --git a/src/components/Search/FilterDropdowns/SingleSelectPopup.tsx b/src/components/Search/FilterDropdowns/SingleSelectPopup.tsx
index 40d836d2af625..7e98978530b44 100644
--- a/src/components/Search/FilterDropdowns/SingleSelectPopup.tsx
+++ b/src/components/Search/FilterDropdowns/SingleSelectPopup.tsx
@@ -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';
@@ -42,11 +43,24 @@ type SingleSelectPopupProps = {
/** The default value to set when reset is clicked */
defaultValue?: string;
+ style?: StyleProp;
+
/** Custom styles for the SelectionList */
selectionListStyle?: SelectionListStyle;
};
-function SingleSelectPopup({label, value, items, closeOverlay, onChange, isSearchable, searchPlaceholder, defaultValue, selectionListStyle}: SingleSelectPopupProps) {
+function SingleSelectPopup({
+ label,
+ value,
+ items,
+ closeOverlay,
+ onChange,
+ isSearchable,
+ searchPlaceholder,
+ defaultValue,
+ style,
+ selectionListStyle,
+}: SingleSelectPopupProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
@@ -117,7 +131,7 @@ function SingleSelectPopup({label, value, items, closeOverlay,
const shouldShowLabel = isSmallScreenWidth && !!label;
return (
-
+
{shouldShowLabel && {label}}
diff --git a/src/components/Search/FilterDropdowns/SortByPopup.tsx b/src/components/Search/FilterDropdowns/SortByPopup.tsx
new file mode 100644
index 0000000000000..3d1b3ecc796e4
--- /dev/null
+++ b/src/components/Search/FilterDropdowns/SortByPopup.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext';
+import type {SearchColumnType, SearchGroupBy, SearchQueryJSON} from '@components/Search/types';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+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 {buildSearchQueryString} from '@libs/SearchQueryUtils';
+import {getColumnsToShow, getSearchColumnTranslationKey, getSortByOptions} from '@libs/SearchUIUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import {columnsSelector} from '@src/selectors/AdvancedSearchFiltersForm';
+import type {SearchResults} from '@src/types/onyx';
+import SingleSelectPopup from './SingleSelectPopup';
+import type {SingleSelectItem} from './SingleSelectPopup';
+
+type SortByPopupProps = {
+ searchResults: OnyxEntry;
+ queryJSON: SearchQueryJSON;
+ groupBy: SingleSelectItem | null;
+ onSort: () => void;
+ closeOverlay: () => void;
+};
+
+function SortByPopup({searchResults, queryJSON, groupBy, onSort, closeOverlay}: SortByPopupProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const {accountID} = useCurrentUserPersonalDetails();
+ const {shouldUseLiveData} = useSearchStateContext();
+ const {clearSelectedTransactions} = useSearchActionsContext();
+ const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: columnsSelector});
+ const searchDataType = shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type;
+ const currentColumns = !searchResults?.data ? [] : getColumnsToShow(accountID, searchResults?.data, visibleColumns, false, searchDataType, groupBy?.value);
+ const sortableColumns = getSortByOptions(currentColumns, translate);
+ const sortBy = {text: translate(getSearchColumnTranslationKey(queryJSON.sortBy)), value: queryJSON.sortBy};
+
+ const onSortChange = (column: SearchColumnType) => {
+ clearSelectedTransactions();
+ const newQuery = buildSearchQueryString({
+ ...queryJSON,
+ sortBy: column,
+ sortOrder: CONST.SEARCH.SORT_ORDER.DESC,
+ });
+ onSort();
+ // We want to explicitly clear stale rawQuery since it's only used for manually typed-in queries.
+ close(() => {
+ Navigation.setParams({q: newQuery, rawQuery: undefined});
+ });
+ };
+
+ return (
+ {
+ if (!item) {
+ return;
+ }
+ onSortChange(item.value);
+ }}
+ />
+ );
+}
+
+export default SortByPopup;
diff --git a/src/components/Search/FilterDropdowns/TextInputPopup.tsx b/src/components/Search/FilterDropdowns/TextInputPopup.tsx
new file mode 100644
index 0000000000000..b3570ce3fbf03
--- /dev/null
+++ b/src/components/Search/FilterDropdowns/TextInputPopup.tsx
@@ -0,0 +1,67 @@
+import React, {useState} from 'react';
+import {View} from 'react-native';
+import type {StyleProp, ViewStyle} from 'react-native';
+import Button from '@components/Button';
+import TextInput from '@components/TextInput';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+
+type TextInputPopupProps = {
+ style?: StyleProp;
+ defaultValue: string;
+ placeholder?: string;
+ closeOverlay: () => void;
+ onChange: (value: string) => void;
+};
+
+function TextInputPopup({style, defaultValue, placeholder, closeOverlay, onChange}: TextInputPopupProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
+ const {isSmallScreenWidth} = useResponsiveLayout();
+ const [value, setValue] = useState(defaultValue);
+
+ const applyChanges = () => {
+ onChange(value);
+ closeOverlay();
+ };
+
+ const resetChanges = () => {
+ onChange('');
+ closeOverlay();
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export default TextInputPopup;
diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx
index ebcfa3e17ce7d..8087ce2d62bb4 100644
--- a/src/components/Search/SearchContext.tsx
+++ b/src/components/Search/SearchContext.tsx
@@ -60,7 +60,7 @@ const defaultSearchStateContext: SearchStateContextValue = {
lastSearchType: undefined,
areAllMatchingItemsSelected: false,
shouldShowSelectAllMatchingItems: false,
- shouldShowFiltersBarLoading: false,
+ shouldShowActionsBarLoading: false,
currentSearchResults: undefined,
shouldUseLiveData: false,
};
@@ -70,7 +70,7 @@ const defaultSearchActionsContext: SearchActionsContextValue = {
setSelectedTransactions: () => {},
removeTransaction: () => {},
clearSelectedTransactions: () => {},
- setShouldShowFiltersBarLoading: () => {},
+ setShouldShowActionsBarLoading: () => {},
setShouldShowSelectAllMatchingItems: () => {},
selectAllMatchingItems: () => {},
setShouldResetSearchQuery: () => {},
@@ -101,7 +101,7 @@ function SearchContextProvider({children}: SearchContextProps) {
const areTransactionsEmpty = useRef(true);
const [lastSearchType, setLastSearchType] = useState();
const [areAllMatchingItemsSelected, selectAllMatchingItems] = useState(false);
- const [shouldShowFiltersBarLoading, setShouldShowFiltersBarLoading] = useState(false);
+ const [shouldShowActionsBarLoading, setShouldShowActionsBarLoading] = useState(false);
const [shouldShowSelectAllMatchingItems, setShouldShowSelectAllMatchingItems] = useState(false);
const [searchContextData, setSearchContextData] = useState({...defaultSearchContextData});
@@ -282,7 +282,7 @@ function SearchContextProvider({children}: SearchContextProps) {
currentSimilarSearchHash,
currentSearchResults,
shouldUseLiveData,
- shouldShowFiltersBarLoading,
+ shouldShowActionsBarLoading,
lastSearchType,
shouldShowSelectAllMatchingItems,
areAllMatchingItemsSelected,
@@ -293,7 +293,7 @@ function SearchContextProvider({children}: SearchContextProps) {
removeTransaction,
setSelectedTransactions,
clearSelectedTransactions,
- setShouldShowFiltersBarLoading,
+ setShouldShowActionsBarLoading,
setLastSearchType,
setShouldShowSelectAllMatchingItems,
selectAllMatchingItems,
diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBarCreateButton.tsx b/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx
similarity index 98%
rename from src/components/Search/SearchPageHeader/SearchFiltersBarCreateButton.tsx
rename to src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx
index c8defc859f1a1..f6467e5d84294 100644
--- a/src/components/Search/SearchPageHeader/SearchFiltersBarCreateButton.tsx
+++ b/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx
@@ -34,7 +34,7 @@ import ROUTES from '@src/ROUTES';
import {groupPaidPoliciesWithExpenseChatEnabledSelector} from '@src/selectors/Policy';
import type * as OnyxTypes from '@src/types/onyx';
-function SearchFiltersBarCreateButton() {
+function SearchActionsBarCreateButton() {
const styles = useThemeStyles();
const {translate} = useLocalize();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Plus', 'Location', 'Document', 'Receipt', 'Coins', 'Cash', 'Transfer', 'MoneyCircle']);
@@ -225,7 +225,7 @@ function SearchFiltersBarCreateButton() {
);
return (
-
+
;
+ onSearchButtonPress: () => void;
+ onSort: () => void;
+};
+
+// NOTE: This is intentionally unused for now. It will be wired up in https://github.com/Expensify/App/issues/84876
+function SearchActionsBarNarrow({queryJSON, isMobileSelectionModeEnabled, isSearchInputVisible, searchResults, onSearchButtonPress, onSort}: SearchActionsBarNarrowProps) {
+ const {hasErrors, shouldShowActionsBarLoading, shouldShowSelectedDropdown, styles} = useSearchActionsBar(queryJSON, isMobileSelectionModeEnabled);
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['MagnifyingGlass']);
+
+ if (hasErrors) {
+ return null;
+ }
+
+ if (shouldShowActionsBarLoading) {
+ const skeletonReasonAttributes: SkeletonSpanReasonAttributes = {
+ context: 'SearchActionsBarNarrow',
+ shouldShowActionsBarLoading,
+ };
+ return (
+
+ );
+ }
+
+ return (
+
+ {shouldShowSelectedDropdown ? (
+
+ ) : (
+ <>
+
+ {!isSearchInputVisible && (
+
+ )}
+
+
+
+
+
+
+ >
+ )}
+
+ );
+}
+
+SearchActionsBarNarrow.displayName = 'SearchActionsBarNarrow';
+
+export default SearchActionsBarNarrow;
diff --git a/src/components/Search/SearchPageHeader/SearchActionsBarWide.tsx b/src/components/Search/SearchPageHeader/SearchActionsBarWide.tsx
new file mode 100644
index 0000000000000..19a074780a635
--- /dev/null
+++ b/src/components/Search/SearchPageHeader/SearchActionsBarWide.tsx
@@ -0,0 +1,137 @@
+import React from 'react';
+import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import DropdownButton from '@components/Search/FilterDropdowns/DropdownButton';
+import type {DropdownButtonProps} from '@components/Search/FilterDropdowns/DropdownButton';
+import useFilterFeedValue from '@components/Search/hooks/useFilterFeedValue';
+import useFilterFromValue from '@components/Search/hooks/useFilterFromValue';
+import useFilterWorkspaceValue from '@components/Search/hooks/useFilterWorkspaceValue';
+import SearchBulkActionsButton from '@components/Search/SearchBulkActionsButton';
+import type {SearchQueryJSON} from '@components/Search/types';
+import SearchActionsSkeleton from '@components/Skeletons/SearchActionsSkeleton';
+import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
+import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm';
+import type {SearchAdvancedFiltersKey} from '@src/types/form/SearchAdvancedFiltersForm';
+import type {SearchResults} from '@src/types/onyx';
+import SearchActionsBarCreateButton from './SearchActionsBarCreateButton';
+import SearchAdvancedFiltersButton from './SearchAdvancedFiltersButton';
+import SearchDisplayDropdownButton from './SearchDisplayDropdownButton';
+import SearchPageInputWide from './SearchPageInputWide';
+import SearchSaveButton from './SearchSaveButton';
+import useSearchActionsBar from './useSearchActionsBar';
+
+type SearchActionsBarWideProps = {
+ queryJSON: SearchQueryJSON;
+ searchResults: OnyxEntry;
+ handleSearch: (value: string) => void;
+ onSort: () => void;
+};
+
+type SearchDropdownProps = Omit;
+
+function FromDropdown({label, value, PopoverComponent, sentryLabel}: SearchDropdownProps) {
+ const fromValue = useFilterFromValue(value);
+ return (
+
+ );
+}
+
+function WorkspaceDropdown({label, value, PopoverComponent, sentryLabel}: SearchDropdownProps) {
+ const workspaceValue = useFilterWorkspaceValue(value);
+ return (
+
+ );
+}
+
+function FeedDropdown({label, value, PopoverComponent, sentryLabel}: SearchDropdownProps) {
+ const feedValue = useFilterFeedValue(value);
+ return (
+
+ );
+}
+
+const FILTER_KEY_TO_COMPONENT: Partial>> = {
+ [FILTER_KEYS.FROM]: FromDropdown,
+ [FILTER_KEYS.POLICY_ID]: WorkspaceDropdown,
+ [FILTER_KEYS.FEED]: FeedDropdown,
+};
+
+// NOTE: This is intentionally unused for now. It will be wired up in https://github.com/Expensify/App/issues/84876
+function SearchActionsBarWide({queryJSON, searchResults, handleSearch, onSort}: SearchActionsBarWideProps) {
+ const {filters, hasErrors, shouldShowActionsBarLoading, shouldShowSelectedDropdown, styles} = useSearchActionsBar(queryJSON, false);
+
+ if (hasErrors) {
+ return null;
+ }
+
+ if (shouldShowActionsBarLoading) {
+ const skeletonReasonAttributes: SkeletonSpanReasonAttributes = {
+ context: 'SearchActionsBarWide',
+ shouldShowActionsBarLoading,
+ };
+ return (
+
+ );
+ }
+
+ return (
+
+ {shouldShowSelectedDropdown ? (
+
+ ) : (
+ <>
+
+
+ {filters.map((item) => {
+ const Component = FILTER_KEY_TO_COMPONENT[item.key] ?? DropdownButton;
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+ >
+ )}
+
+ );
+}
+
+SearchActionsBarWide.displayName = 'SearchActionsBarWide';
+
+export default SearchActionsBarWide;
diff --git a/src/components/Search/SearchPageHeader/SearchAdvancedFiltersButton.tsx b/src/components/Search/SearchPageHeader/SearchAdvancedFiltersButton.tsx
new file mode 100644
index 0000000000000..899a099eef0b7
--- /dev/null
+++ b/src/components/Search/SearchPageHeader/SearchAdvancedFiltersButton.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import Button from '@components/Button';
+import type {SearchQueryJSON} from '@components/Search/types';
+import useFilterFormValues from '@hooks/useFilterFormValues';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import useSearchFilterSync from '@hooks/useSearchFilterSync';
+import {updateAdvancedFilters} from '@libs/actions/Search';
+import Navigation from '@libs/Navigation/Navigation';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+
+function SearchAdvancedFiltersButton({queryJSON}: {queryJSON: SearchQueryJSON}) {
+ const {translate} = useLocalize();
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Filter']);
+ const filterFormValues = useFilterFormValues(queryJSON);
+ useSearchFilterSync(filterFormValues);
+
+ const openAdvancedFilters = () => {
+ updateAdvancedFilters(filterFormValues);
+ Navigation.navigate(ROUTES.SEARCH_ADVANCED_FILTERS.getRoute());
+ };
+
+ return (
+
+ );
+}
+
+export default SearchAdvancedFiltersButton;
diff --git a/src/components/Search/SearchPageHeader/SearchDisplayDropdownButton.tsx b/src/components/Search/SearchPageHeader/SearchDisplayDropdownButton.tsx
new file mode 100644
index 0000000000000..5b08a5c745762
--- /dev/null
+++ b/src/components/Search/SearchPageHeader/SearchDisplayDropdownButton.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import DisplayPopup from '@components/Search/FilterDropdowns/DisplayPopup';
+import DropdownButton from '@components/Search/FilterDropdowns/DropdownButton';
+import type {PopoverComponentProps} from '@components/Search/FilterDropdowns/DropdownButton';
+import type {SearchQueryJSON} from '@components/Search/types';
+import useLocalize from '@hooks/useLocalize';
+import CONST from '@src/CONST';
+import type {SearchResults} from '@src/types/onyx';
+
+type SearchDisplayDropdownButtonProps = {
+ queryJSON: SearchQueryJSON;
+ searchResults: OnyxEntry;
+ onSort: () => void;
+};
+
+function SearchDisplayDropdownButton({queryJSON, searchResults, onSort}: SearchDisplayDropdownButtonProps) {
+ const {translate} = useLocalize();
+
+ if (queryJSON.type === CONST.SEARCH.DATA_TYPES.CHAT) {
+ return null;
+ }
+
+ const displayPopup = ({closeOverlay}: PopoverComponentProps) => (
+
+ );
+
+ return (
+
+ );
+}
+
+export default SearchDisplayDropdownButton;
diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBarNarrow.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBarNarrow.tsx
index c234d311185cd..d48c155314ae7 100644
--- a/src/components/Search/SearchPageHeader/SearchFiltersBarNarrow.tsx
+++ b/src/components/Search/SearchPageHeader/SearchFiltersBarNarrow.tsx
@@ -4,7 +4,7 @@ import Button from '@components/Button';
import DropdownButton from '@components/Search/FilterDropdowns/DropdownButton';
import SearchBulkActionsButton from '@components/Search/SearchBulkActionsButton';
import type {SearchQueryJSON} from '@components/Search/types';
-import SearchFiltersSkeleton from '@components/Skeletons/SearchFiltersSkeleton';
+import SearchActionsSkeleton from '@components/Skeletons/SearchActionsSkeleton';
import shouldAdjustScroll from '@libs/shouldAdjustScroll';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import CONST from '@src/CONST';
@@ -71,7 +71,7 @@ function SearchFiltersBarNarrow({queryJSON, isMobileSelectionModeEnabled}: Searc
shouldShowFiltersBarLoading,
};
return (
-
diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBarWide.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBarWide.tsx
index 0f24fd3774688..97f2d08a7a195 100644
--- a/src/components/Search/SearchPageHeader/SearchFiltersBarWide.tsx
+++ b/src/components/Search/SearchPageHeader/SearchFiltersBarWide.tsx
@@ -4,10 +4,10 @@ import Button from '@components/Button';
import DropdownButton from '@components/Search/FilterDropdowns/DropdownButton';
import SearchBulkActionsButton from '@components/Search/SearchBulkActionsButton';
import type {SearchQueryJSON} from '@components/Search/types';
-import SearchFiltersSkeleton from '@components/Skeletons/SearchFiltersSkeleton';
+import SearchActionsSkeleton from '@components/Skeletons/SearchActionsSkeleton';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import CONST from '@src/CONST';
-import SearchFiltersBarCreateButton from './SearchFiltersBarCreateButton';
+import SearchActionsBarCreateButton from './SearchActionsBarCreateButton';
import useSearchFiltersBar from './useSearchFiltersBar';
type SearchFiltersBarWideProps = {
@@ -41,7 +41,7 @@ function SearchFiltersBarWide({queryJSON, isMobileSelectionModeEnabled}: SearchF
shouldShowFiltersBarLoading,
};
return (
-
@@ -93,7 +93,7 @@ function SearchFiltersBarWide({queryJSON, isMobileSelectionModeEnabled}: SearchF
)}
-
+
>
)}
diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderNarrow.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderNarrow.tsx
new file mode 100644
index 0000000000000..0642821180091
--- /dev/null
+++ b/src/components/Search/SearchPageHeader/SearchPageHeaderNarrow.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import TopBar from '@components/Navigation/TopBar';
+import type {SearchQueryJSON} from '@components/Search/types';
+import useLocalize from '@hooks/useLocalize';
+import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow';
+
+type SearchPageHeaderNarrowProps = {
+ queryJSON: SearchQueryJSON;
+ shouldShowLoadingBar: boolean;
+ isMobileSelectionModeEnabled: boolean;
+ cancelSearch?: () => void;
+};
+// NOTE: This is intentionally unused for now. It will be wired up in https://github.com/Expensify/App/issues/84876
+function SearchPageHeaderNarrow({queryJSON, shouldShowLoadingBar = false, isMobileSelectionModeEnabled, cancelSearch}: SearchPageHeaderNarrowProps) {
+ const {translate} = useLocalize();
+
+ if (isMobileSelectionModeEnabled) {
+ return ;
+ }
+
+ return (
+
+ );
+}
+
+export default SearchPageHeaderNarrow;
diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderWide.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderWide.tsx
new file mode 100644
index 0000000000000..2647f64f3f075
--- /dev/null
+++ b/src/components/Search/SearchPageHeader/SearchPageHeaderWide.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import TopBar from '@components/Navigation/TopBar';
+import type {SearchQueryJSON} from '@components/Search/types';
+import useLocalize from '@hooks/useLocalize';
+import useSearchTypeMenuSections from '@hooks/useSearchTypeMenuSections';
+
+type SearchPageHeaderWideProps = {
+ queryJSON: SearchQueryJSON;
+};
+// NOTE: This is intentionally unused for now. It will be wired up in https://github.com/Expensify/App/issues/84876
+function SearchPageHeaderWide({queryJSON}: SearchPageHeaderWideProps) {
+ const {translate} = useLocalize();
+ const {typeMenuSections, activeItemIndex} = useSearchTypeMenuSections(queryJSON);
+ const selectedItem = typeMenuSections.flatMap((section) => section.menuItems).at(activeItemIndex);
+ const title = activeItemIndex >= 0 && selectedItem ? translate(selectedItem.translationPath) : translate('common.reports');
+
+ return (
+
+ );
+}
+
+export default SearchPageHeaderWide;
diff --git a/src/components/Search/SearchPageHeader/SearchPageInputNarrow.tsx b/src/components/Search/SearchPageHeader/SearchPageInputNarrow.tsx
new file mode 100644
index 0000000000000..31107f394daa3
--- /dev/null
+++ b/src/components/Search/SearchPageHeader/SearchPageInputNarrow.tsx
@@ -0,0 +1,95 @@
+import React, {useEffect} from 'react';
+import {View} from 'react-native';
+import Animated from 'react-native-reanimated';
+import SearchAutocompleteList from '@components/Search/SearchAutocompleteList';
+import SearchInputSelectionWrapper from '@components/Search/SearchInputSelectionWrapper';
+import type {SearchQueryJSON} from '@components/Search/types';
+import useThemeStyles from '@hooks/useThemeStyles';
+import KeyboardUtils from '@src/utils/keyboard';
+import useSearchPageInput from './useSearchPageInput';
+
+type SearchPageInputNarrowProps = {
+ queryJSON: SearchQueryJSON;
+ searchRouterListVisible: boolean;
+ hideSearchRouterList: () => void;
+ onSearchRouterFocus: () => void;
+ handleSearch: (value: string) => void;
+};
+// NOTE: This is intentionally unused for now. It will be wired up in https://github.com/Expensify/App/issues/84876
+function SearchPageInputNarrow({queryJSON, searchRouterListVisible, hideSearchRouterList, onSearchRouterFocus, handleSearch}: SearchPageInputNarrowProps) {
+ const styles = useThemeStyles();
+
+ const {
+ allFeeds,
+ autocompleteSubstitutions,
+ autocompleteQueryValue,
+ personalAndWorkspaceCards,
+ personalDetails,
+ reports,
+ searchQueryItem,
+ selection,
+ textInputRef,
+ textInputValue,
+ handleKeyPress,
+ handleSearchAction,
+ onListItemPress,
+ onSearchQueryChange,
+ submitSearch,
+ } = useSearchPageInput({
+ queryJSON,
+ onSearch: handleSearch,
+ onSubmit: hideSearchRouterList,
+ });
+
+ // useEffect for blurring TextInput when we cancel SearchRouter interaction on narrow layout
+ useEffect(() => {
+ if (!!searchRouterListVisible || !textInputRef.current || !textInputRef.current.isFocused()) {
+ return;
+ }
+ textInputRef.current.blur();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [searchRouterListVisible]);
+
+ return (
+
+
+
+ {
+ KeyboardUtils.dismiss().then(() => submitSearch(textInputValue));
+ }}
+ autoFocus={false}
+ onFocus={onSearchRouterFocus}
+ wrapperStyle={{...styles.newSearchAutocompleteInputResults, ...styles.br2}}
+ wrapperFocusedStyle={styles.searchAutocompleteInputResultsFocused}
+ ref={textInputRef}
+ onKeyPress={handleKeyPress}
+ />
+
+
+ {!!searchRouterListVisible && (
+
+ )}
+
+ );
+}
+
+export default SearchPageInputNarrow;
diff --git a/src/components/Search/SearchPageHeader/SearchPageInputWide.tsx b/src/components/Search/SearchPageHeader/SearchPageInputWide.tsx
new file mode 100644
index 0000000000000..6a910d1646c58
--- /dev/null
+++ b/src/components/Search/SearchPageHeader/SearchPageInputWide.tsx
@@ -0,0 +1,120 @@
+import {useIsFocused} from '@react-navigation/native';
+import {useEffect, useRef, useState} from 'react';
+import {View} from 'react-native';
+import SearchAutocompleteList from '@components/Search/SearchAutocompleteList';
+import SearchInputSelectionWrapper from '@components/Search/SearchInputSelectionWrapper';
+import {useSearchRouterActions} from '@components/Search/SearchRouter/SearchRouterContext';
+import type {SearchQueryJSON} from '@components/Search/types';
+import type {SelectionListWithSectionsHandle} from '@components/SelectionList/SelectionListWithSections/types';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useSearchPageInput from './useSearchPageInput';
+
+type SearchPageInputWideProps = {
+ queryJSON: SearchQueryJSON;
+ handleSearch: (value: string) => void;
+};
+// NOTE: This is intentionally unused for now. It will be wired up in https://github.com/Expensify/App/issues/84876
+function SearchPageInputWide({queryJSON, handleSearch}: SearchPageInputWideProps) {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const isFocused = useIsFocused();
+ const {registerSearchPageInput} = useSearchRouterActions();
+
+ const [isAutocompleteListVisible, setIsAutocompleteListVisible] = useState(false);
+
+ const listRef = useRef(null);
+
+ const {
+ allFeeds,
+ autocompleteSubstitutions,
+ autocompleteQueryValue,
+ personalAndWorkspaceCards,
+ personalDetails,
+ reports,
+ searchQueryItem,
+ selection,
+ textInputRef,
+ textInputValue,
+ handleKeyPress,
+ handleSearchAction,
+ onListItemPress,
+ onSearchQueryChange,
+ submitSearch,
+ } = useSearchPageInput({
+ queryJSON,
+ onSearch: handleSearch,
+ onSubmit: () => setIsAutocompleteListVisible(false),
+ });
+
+ useEffect(() => {
+ if (!isFocused || !textInputRef.current) {
+ return;
+ }
+
+ registerSearchPageInput(textInputRef.current);
+ }, [isFocused, registerSearchPageInput, textInputRef]);
+
+ const hideAutocompleteList = () => setIsAutocompleteListVisible(false);
+ const showAutocompleteList = () => setIsAutocompleteListVisible(true);
+
+ const autocompleteInputStyle = isAutocompleteListVisible
+ ? [styles.border, styles.borderRadiusComponentLarge, styles.pAbsolute, styles.pt2, styles.w100, styles.zIndex10, {top: 0, maxWidth: 675}, {boxShadow: theme.shadow}]
+ : [];
+ const inputWrapperActiveStyle = isAutocompleteListVisible ? styles.ph2 : null;
+
+ return (
+ <>
+ {/* An empty view as the input placeholder so that the applied filters won't move when the real input position becomes absolute */}
+ {isAutocompleteListVisible && }
+
+ {
+ const focusedOption = listRef.current?.getFocusedOption();
+ if (focusedOption) {
+ return;
+ }
+ submitSearch(textInputValue);
+ }}
+ autoFocus={false}
+ onFocus={showAutocompleteList}
+ onBlur={hideAutocompleteList}
+ wrapperStyle={{...styles.newSearchAutocompleteInputResults, ...styles.br2}}
+ wrapperFocusedStyle={styles.searchAutocompleteInputResultsFocused}
+ outerWrapperStyle={[inputWrapperActiveStyle, styles.flex1]}
+ ref={textInputRef}
+ selection={selection}
+ substitutionMap={autocompleteSubstitutions}
+ onKeyPress={handleKeyPress}
+ />
+ {isAutocompleteListVisible && (
+
+
+
+ )}
+
+ >
+ );
+}
+
+export default SearchPageInputWide;
diff --git a/src/components/Search/SearchPageHeader/SearchSaveButton.tsx b/src/components/Search/SearchPageHeader/SearchSaveButton.tsx
new file mode 100644
index 0000000000000..db3e60a9cc612
--- /dev/null
+++ b/src/components/Search/SearchPageHeader/SearchSaveButton.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import Button from '@components/Button';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import Navigation from '@libs/Navigation/Navigation';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+
+function SearchSaveButton() {
+ const {translate} = useLocalize();
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Bookmark']);
+
+ return (
+
))}
@@ -144,4 +142,4 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
);
}
-export default SearchTypeMenu;
+export default SearchTypeMenuWide;
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 002af4179f911..6396fa712e4cd 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -2539,6 +2539,11 @@ const staticStyles = (theme: ThemeColors) =>
paddingRight: 12,
},
+ newSearchResultsHeaderBar: {
+ display: 'flex',
+ position: 'relative',
+ },
+
headerBarHeight: {
height: variables.contentHeaderHeight,
},
@@ -3458,6 +3463,11 @@ const staticStyles = (theme: ThemeColors) =>
height: 54,
},
+ newSearchAutocompleteInputResults: {
+ borderWidth: 1,
+ borderColor: theme.border,
+ },
+
searchAutocompleteInputResultsFocused: {
borderWidth: 1,
borderColor: theme.success,
@@ -4854,17 +4864,39 @@ const staticStyles = (theme: ThemeColors) =>
minHeight: variables.componentSizeSmall,
},
+ filtersBar: {
+ flexDirection: 'row',
+ gap: 8,
+ marginTop: 3,
+ },
+
searchFiltersBarContainer: {
marginTop: 8,
flexDirection: 'row',
alignItems: 'center',
},
- searchFiltersBarCreateButton: {
+ searchActionsBarContainer: {
+ marginTop: 12,
+ marginBottom: 16,
+ paddingHorizontal: 20,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ zIndex: 10,
+ },
+
+ searchActionsBarCreateButton: {
marginLeft: 'auto',
alignSelf: 'flex-start',
},
+ searchPageInputTouchableWrapper: {height: 32, width: 200},
+ searchPageInputPlaceholder: {
+ // Extra 2 to account for the borders
+ height: 34,
+ width: 202,
+ },
+
walletStaticIllustration: {
width: 262,
height: 152,
diff --git a/src/types/form/SearchAdvancedFiltersForm.ts b/src/types/form/SearchAdvancedFiltersForm.ts
index 4e799a2b01652..c06ae0e58ec60 100644
--- a/src/types/form/SearchAdvancedFiltersForm.ts
+++ b/src/types/form/SearchAdvancedFiltersForm.ts
@@ -541,6 +541,11 @@ const ALLOWED_TYPE_FILTERS: Record> = {
type SearchAdvancedFiltersKey = ValueOf | ReportFieldKey;
+type HasFilterValue = ValueOf;
+type HasFilterValues = Array>;
+type IsFilterValue = ValueOf;
+type IsFilterValues = Array>;
+
type SearchAdvancedFiltersForm = Form<
SearchAdvancedFiltersKey,
{
@@ -669,10 +674,10 @@ type SearchAdvancedFiltersForm = Form<
[FILTER_KEYS.ACTION]: string;
[FILTER_KEYS.ACTION_NOT]: string;
- [FILTER_KEYS.HAS]: string[];
+ [FILTER_KEYS.HAS]: HasFilterValues;
[FILTER_KEYS.HAS_NOT]: string[];
- [FILTER_KEYS.IS]: string[];
+ [FILTER_KEYS.IS]: IsFilterValues;
[FILTER_KEYS.IS_NOT]: string[];
[FILTER_KEYS.PURCHASE_AMOUNT_EQUAL_TO]: string;
@@ -696,6 +701,6 @@ type SearchAdvancedFiltersForm = Form<
Record
>;
-export type {SearchAdvancedFiltersForm, SearchAdvancedFiltersKey};
+export type {SearchAdvancedFiltersForm, SearchAdvancedFiltersKey, HasFilterValue, HasFilterValues, IsFilterValue, IsFilterValues};
export default FILTER_KEYS;
export {DATE_FILTER_KEYS, ALLOWED_TYPE_FILTERS, FILTER_KEYS, AMOUNT_FILTER_KEYS};
diff --git a/src/types/form/SearchSaveForm.ts b/src/types/form/SearchSaveForm.ts
new file mode 100644
index 0000000000000..18bbc4710600d
--- /dev/null
+++ b/src/types/form/SearchSaveForm.ts
@@ -0,0 +1,18 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ NAME: 'searchSaveName',
+} as const;
+
+type InputID = ValueOf;
+
+type SearchSaveForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.NAME]: string;
+ }
+>;
+
+export type {SearchSaveForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index b1b8e10793132..15414420991df 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -92,6 +92,7 @@ export type {WorkspaceCategoryDescriptionHintForm} from './WorkspaceCategoryDesc
export type {WorkspaceCategoryFlagAmountsOverForm} from './WorkspaceCategoryFlagAmountsOverForm';
export type {WorkspaceCompanyCardFeedName} from './WorkspaceCompanyCardFeedName';
export type {SearchSavedSearchRenameForm} from './SearchSavedSearchRenameForm';
+export type {SearchSaveForm} from './SearchSaveForm';
export type {WorkspaceCompanyCardEditName} from './WorkspaceCompanyCardEditName';
export type {PersonalDetailsForm} from './PersonalDetailsForm';
export type {OnboardingWorkEmailForm} from './OnboardingWorkEmailForm';
diff --git a/tests/perf-test/useFilterFormValues.perf-test.tsx b/tests/perf-test/useFilterFormValues.perf-test.tsx
index b8db4246be390..14d777864d287 100644
--- a/tests/perf-test/useFilterFormValues.perf-test.tsx
+++ b/tests/perf-test/useFilterFormValues.perf-test.tsx
@@ -3,7 +3,7 @@ import {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import Onyx, {useOnyx} from 'react-native-onyx';
import {measureFunction, measureRenders} from 'reassure';
-import {typeOptionsPoliciesSelector} from '@components/Search/SearchPageHeader/useSearchFiltersBar';
+import {typeOptionsPoliciesSelector} from '@components/Search/SearchPageHeader/useSearchActionsBar';
import {advancedSearchPoliciesSelector} from '@hooks/useAdvancedSearchFilters';
import {exportedToPoliciesSelector} from '@hooks/useExportedToFilterOptions';
import {policiesSelector, policyCategoriesSelector, policyTagsSelector, reportsSelector} from '@hooks/useFilterFormValues';
diff --git a/tests/ui/CategoryListItemHeaderTest.tsx b/tests/ui/CategoryListItemHeaderTest.tsx
index ffeec8ba0992c..9dbfce72afd48 100644
--- a/tests/ui/CategoryListItemHeaderTest.tsx
+++ b/tests/ui/CategoryListItemHeaderTest.tsx
@@ -35,7 +35,7 @@ const mockSearchStateContext = {
lastSearchType: undefined,
areAllMatchingItemsSelected: false,
shouldShowSelectAllMatchingItems: false,
- shouldShowFiltersBarLoading: false,
+ shouldShowActionsBarLoading: false,
shouldUseLiveData: false,
currentSimilarSearchHash: -1,
suggestedSearches: {} as SearchStateContextValue['suggestedSearches'],
@@ -46,7 +46,7 @@ const mockSearchActionsContext = {
setSelectedTransactions: jest.fn(),
removeTransaction: jest.fn(),
clearSelectedTransactions: jest.fn(),
- setShouldShowFiltersBarLoading: jest.fn(),
+ setShouldShowActionsBarLoading: jest.fn(),
setShouldShowSelectAllMatchingItems: jest.fn(),
selectAllMatchingItems: jest.fn(),
setShouldResetSearchQuery: jest.fn(),
diff --git a/tests/ui/MerchantListItemHeaderTest.tsx b/tests/ui/MerchantListItemHeaderTest.tsx
index c2ba432af9204..826fc2b83414a 100644
--- a/tests/ui/MerchantListItemHeaderTest.tsx
+++ b/tests/ui/MerchantListItemHeaderTest.tsx
@@ -35,7 +35,7 @@ const mockSearchStateContext = {
lastSearchType: undefined,
areAllMatchingItemsSelected: false,
shouldShowSelectAllMatchingItems: false,
- shouldShowFiltersBarLoading: false,
+ shouldShowActionsBarLoading: false,
shouldUseLiveData: false,
currentSimilarSearchHash: -1,
suggestedSearches: {} as SearchStateContextValue['suggestedSearches'],
@@ -46,7 +46,7 @@ const mockSearchActionsContext = {
setSelectedTransactions: jest.fn(),
removeTransaction: jest.fn(),
clearSelectedTransactions: jest.fn(),
- setShouldShowFiltersBarLoading: jest.fn(),
+ setShouldShowActionsBarLoading: jest.fn(),
setShouldShowSelectAllMatchingItems: jest.fn(),
selectAllMatchingItems: jest.fn(),
setShouldResetSearchQuery: jest.fn(),
diff --git a/tests/ui/MonthListItemHeaderTest.tsx b/tests/ui/MonthListItemHeaderTest.tsx
index e426e429b87a9..ca5f73ff5eafd 100644
--- a/tests/ui/MonthListItemHeaderTest.tsx
+++ b/tests/ui/MonthListItemHeaderTest.tsx
@@ -35,7 +35,7 @@ const mockSearchStateContext = {
lastSearchType: undefined,
areAllMatchingItemsSelected: false,
shouldShowSelectAllMatchingItems: false,
- shouldShowFiltersBarLoading: false,
+ shouldShowActionsBarLoading: false,
shouldUseLiveData: false,
currentSimilarSearchHash: -1,
suggestedSearches: {} as SearchStateContextValue['suggestedSearches'],
@@ -46,7 +46,7 @@ const mockSearchActionsContext = {
setSelectedTransactions: jest.fn(),
removeTransaction: jest.fn(),
clearSelectedTransactions: jest.fn(),
- setShouldShowFiltersBarLoading: jest.fn(),
+ setShouldShowActionsBarLoading: jest.fn(),
setShouldShowSelectAllMatchingItems: jest.fn(),
selectAllMatchingItems: jest.fn(),
setShouldResetSearchQuery: jest.fn(),
diff --git a/tests/ui/ReportListItemHeaderTest.tsx b/tests/ui/ReportListItemHeaderTest.tsx
index 23af7b51296ba..3e70bef3f13cc 100644
--- a/tests/ui/ReportListItemHeaderTest.tsx
+++ b/tests/ui/ReportListItemHeaderTest.tsx
@@ -31,7 +31,7 @@ const mockSearchStateContext = {
currentSearchQueryJSON: undefined,
currentSearchResults: undefined,
shouldShowSelectAllMatchingItems: false,
- shouldShowFiltersBarLoading: false,
+ shouldShowActionsBarLoading: false,
shouldUseLiveData: false,
currentSimilarSearchHash: -1,
suggestedSearches: {} as SearchStateContextValue['suggestedSearches'],
@@ -41,7 +41,7 @@ const mockSearchActionsContext = {
clearSelectedTransactions: jest.fn(),
setLastSearchType: jest.fn(),
setSelectedTransactions: jest.fn(),
- setShouldShowFiltersBarLoading: jest.fn(),
+ setShouldShowActionsBarLoading: jest.fn(),
setShouldShowSelectAllMatchingItems: jest.fn(),
selectAllMatchingItems: jest.fn(),
setShouldResetSearchQuery: jest.fn(),
diff --git a/tests/ui/WeekListItemHeaderTest.tsx b/tests/ui/WeekListItemHeaderTest.tsx
index bb0c16831ac32..6e5c78890e287 100644
--- a/tests/ui/WeekListItemHeaderTest.tsx
+++ b/tests/ui/WeekListItemHeaderTest.tsx
@@ -34,7 +34,7 @@ const mockSearchStateContext = {
lastSearchType: undefined,
areAllMatchingItemsSelected: false,
shouldShowSelectAllMatchingItems: false,
- shouldShowFiltersBarLoading: false,
+ shouldShowActionsBarLoading: false,
shouldUseLiveData: false,
currentSimilarSearchHash: -1,
suggestedSearches: {} as SearchStateContextValue['suggestedSearches'],
@@ -45,7 +45,7 @@ const mockSearchActionsContext = {
setSelectedTransactions: jest.fn(),
removeTransaction: jest.fn(),
clearSelectedTransactions: jest.fn(),
- setShouldShowFiltersBarLoading: jest.fn(),
+ setShouldShowActionsBarLoading: jest.fn(),
setShouldShowSelectAllMatchingItems: jest.fn(),
selectAllMatchingItems: jest.fn(),
setShouldResetSearchQuery: jest.fn(),
diff --git a/tests/ui/YearListItemHeaderTest.tsx b/tests/ui/YearListItemHeaderTest.tsx
index 6cc6bd3f5399f..9feb4e2bb8404 100644
--- a/tests/ui/YearListItemHeaderTest.tsx
+++ b/tests/ui/YearListItemHeaderTest.tsx
@@ -35,7 +35,7 @@ const mockSearchStateContext = {
lastSearchType: undefined,
areAllMatchingItemsSelected: false,
shouldShowSelectAllMatchingItems: false,
- shouldShowFiltersBarLoading: false,
+ shouldShowActionsBarLoading: false,
shouldUseLiveData: false,
currentSimilarSearchHash: -1,
suggestedSearches: {} as SearchStateContextValue['suggestedSearches'],
@@ -46,7 +46,7 @@ const mockSearchActionsContext = {
setSelectedTransactions: jest.fn(),
removeTransaction: jest.fn(),
clearSelectedTransactions: jest.fn(),
- setShouldShowFiltersBarLoading: jest.fn(),
+ setShouldShowActionsBarLoading: jest.fn(),
setShouldShowSelectAllMatchingItems: jest.fn(),
selectAllMatchingItems: jest.fn(),
setShouldResetSearchQuery: jest.fn(),
diff --git a/tests/ui/components/SearchFiltersBarCreateButtonTest.tsx b/tests/ui/components/SearchActionsBarCreateButtonTest.tsx
similarity index 98%
rename from tests/ui/components/SearchFiltersBarCreateButtonTest.tsx
rename to tests/ui/components/SearchActionsBarCreateButtonTest.tsx
index 132408199a822..8750049f6e1d9 100644
--- a/tests/ui/components/SearchFiltersBarCreateButtonTest.tsx
+++ b/tests/ui/components/SearchActionsBarCreateButtonTest.tsx
@@ -5,7 +5,7 @@ import Onyx from 'react-native-onyx';
import ComposeProviders from '@components/ComposeProviders';
import {LocaleContextProvider} from '@components/LocaleContextProvider';
import OnyxListItemProvider from '@components/OnyxListItemProvider';
-import SearchFiltersBarCreateButton from '@components/Search/SearchPageHeader/SearchFiltersBarCreateButton';
+import SearchActionsBarCreateButton from '@components/Search/SearchPageHeader/SearchActionsBarCreateButton';
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
import {createNewReport} from '@libs/actions/Report';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
@@ -77,7 +77,7 @@ const MOCK_POLICY = {
function renderComponent() {
return render(
-
+
,
);
}
@@ -92,7 +92,7 @@ function createMockPressEvent(target: unknown) {
};
}
-describe('SearchFiltersBarCreateButton', () => {
+describe('SearchActionsBarCreateButton', () => {
beforeAll(() => {
Onyx.init({
keys: ONYXKEYS,
diff --git a/tests/unit/hooks/useFilterFormValues.test.ts b/tests/unit/hooks/useFilterFormValues.test.ts
index 3b4e3f6c5e07b..12c06bc2a178c 100644
--- a/tests/unit/hooks/useFilterFormValues.test.ts
+++ b/tests/unit/hooks/useFilterFormValues.test.ts
@@ -1,5 +1,5 @@
import type {OnyxCollection} from 'react-native-onyx';
-import {typeOptionsPoliciesSelector} from '@components/Search/SearchPageHeader/useSearchFiltersBar';
+import {typeOptionsPoliciesSelector} from '@components/Search/SearchPageHeader/useSearchActionsBar';
import {advancedSearchPoliciesSelector} from '@hooks/useAdvancedSearchFilters';
import {exportedToPoliciesSelector} from '@hooks/useExportedToFilterOptions';
import {policiesSelector, policyCategoriesSelector, policyTagsSelector, reportsSelector} from '@hooks/useFilterFormValues';