diff --git a/src/components/Search/FilterDropdowns/GroupByPopup.tsx b/src/components/Search/FilterDropdowns/GroupByPopup.tsx
new file mode 100644
index 000000000000..949152ff8dae
--- /dev/null
+++ b/src/components/Search/FilterDropdowns/GroupByPopup.tsx
@@ -0,0 +1,119 @@
+import React, {useCallback, useMemo, useState} from 'react';
+import {View} from 'react-native';
+import Button from '@components/Button';
+import type {SearchGroupBy} from '@components/Search/types';
+import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
+import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections';
+import type {ListItem} from '@components/SelectionList/types';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import type {GroupBySection} from '@libs/SearchUIUtils';
+import CONST from '@src/CONST';
+
+type GroupByPopupItem = {
+ text: string;
+ value: SearchGroupBy;
+};
+
+type GroupByPopupProps = {
+ /** The label to show when in an overlay on mobile */
+ label?: string;
+
+ /** The grouped options to show in the list */
+ sections: GroupBySection[];
+
+ /** The currently selected item */
+ value: GroupByPopupItem | null;
+
+ /** 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) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
+ const {isSmallScreenWidth} = useResponsiveLayout();
+ const {windowHeight} = useWindowDimensions();
+ const [selectedItem, setSelectedItem] = useState(value);
+
+ const allSelectableItems = useMemo(() => sections.flatMap((section) => section.options), [sections]);
+
+ const listSections = useMemo(
+ () =>
+ sections.map((section) => ({
+ sectionIndex: section.sectionIndex,
+ ...(section.sectionIndex > 0 ? {customHeader: } : {}),
+ data: section.options.map>((item) => ({
+ text: item.text,
+ keyForList: item.value,
+ isSelected: item.value === selectedItem?.value,
+ })),
+ })),
+ [sections, selectedItem?.value, styles.dividerLine],
+ );
+
+ const optionsCount = Math.max(
+ 1,
+ listSections.reduce((count, section) => count + section.data.length + (section.data.length > 0 && !!section.customHeader ? 1 : 0), 0),
+ );
+
+ const updateSelectedItem = useCallback(
+ (item: ListItem) => {
+ const newItem = allSelectableItems.find((option) => option.value === item.keyForList) ?? null;
+ setSelectedItem(newItem);
+ },
+ [allSelectableItems],
+ );
+
+ const applyChanges = useCallback(() => {
+ onChange(selectedItem);
+ closeOverlay();
+ }, [closeOverlay, onChange, selectedItem]);
+
+ const resetChanges = useCallback(() => {
+ onChange(null);
+ closeOverlay();
+ }, [closeOverlay, onChange]);
+
+ return (
+
+ {isSmallScreenWidth && !!label && {label}}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default GroupByPopup;
diff --git a/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx
index 73756b66bcad..8102aa915909 100644
--- a/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx
+++ b/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx
@@ -5,6 +5,7 @@ import type {OnyxCollection} from 'react-native-onyx';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
import type {SearchDateValues} from '@components/Search/FilterComponents/DatePresetFilterBase';
import type {PopoverComponentProps} from '@components/Search/FilterDropdowns/DropdownButton';
+import GroupByPopup from '@components/Search/FilterDropdowns/GroupByPopup';
import type {MultiSelectItem} from '@components/Search/FilterDropdowns/MultiSelectPopup';
import MultiSelectPopup from '@components/Search/FilterDropdowns/MultiSelectPopup';
import SingleSelectPopup from '@components/Search/FilterDropdowns/SingleSelectPopup';
@@ -33,6 +34,7 @@ import {
filterValidHasValues,
getFeedOptions,
getGroupByOptions,
+ getGroupBySections,
getGroupCurrencyOptions,
getHasOptions,
getStatusOptions,
@@ -165,6 +167,7 @@ function useSearchFiltersBar(queryJSON: SearchQueryJSON, isMobileSelectionModeEn
const type = typeOptions.find((option) => option.value === unsafeType) ?? null;
const groupByOptions = getGroupByOptions(translate);
+ const groupBySections = getGroupBySections(translate);
const groupBy = groupByOptions.find((option) => option.value === unsafeGroupBy) ?? null;
const viewOptions = getViewOptions(translate);
@@ -283,9 +286,9 @@ function useSearchFiltersBar(queryJSON: SearchQueryJSON, isMobileSelectionModeEn
);
const groupByComponent = ({closeOverlay}: PopoverComponentProps) => (
- {
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 31907d214b8e..40584fc5538b 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -163,6 +163,10 @@ import ViolationsUtils from './Violations/ViolationsUtils';
type ColumnSortMapping = Partial>;
type ColumnVisibility = Partial>;
+type GroupBySection = {
+ sectionIndex: number;
+ options: Array>;
+};
// List Item sorting
type TransactionSorting = ColumnSortMapping;
@@ -3989,6 +3993,30 @@ function getGroupByOptions(translate: LocalizedTranslate) {
return Object.values(CONST.SEARCH.GROUP_BY).map>((value) => ({text: translate(`search.filters.groupBy.${value}`), value}));
}
+function getGroupBySections(translate: LocalizedTranslate): GroupBySection[] {
+ const getOption = (groupBy: SearchGroupBy): SingleSelectItem => ({
+ text: translate(`search.filters.groupBy.${groupBy}`),
+ value: groupBy,
+ });
+ return [
+ {
+ options: [getOption(CONST.SEARCH.GROUP_BY.FROM), getOption(CONST.SEARCH.GROUP_BY.CARD)],
+ },
+ {
+ options: [getOption(CONST.SEARCH.GROUP_BY.CATEGORY), getOption(CONST.SEARCH.GROUP_BY.MERCHANT), getOption(CONST.SEARCH.GROUP_BY.TAG)],
+ },
+ {
+ options: [getOption(CONST.SEARCH.GROUP_BY.MONTH), getOption(CONST.SEARCH.GROUP_BY.WEEK), getOption(CONST.SEARCH.GROUP_BY.YEAR), getOption(CONST.SEARCH.GROUP_BY.QUARTER)],
+ },
+ {
+ options: [getOption(CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID)],
+ },
+ ].map((section, sectionIndex) => ({
+ ...section,
+ sectionIndex,
+ }));
+}
+
function getViewOptions(translate: LocalizedTranslate) {
return Object.values(CONST.SEARCH.VIEW).map>((value) => ({text: translate(`search.view.${value}`), value}));
}
@@ -4754,6 +4782,7 @@ export {
getStatusOptions,
getTypeOptions,
getGroupByOptions,
+ getGroupBySections,
getViewOptions,
getGroupCurrencyOptions,
getFeedOptions,
@@ -4784,4 +4813,4 @@ export {
isEligibleForApproveSuggestion,
applySelectionToItem,
};
-export type {SavedSearchMenuItem, SearchTypeMenuSection, SearchTypeMenuItem, SearchDateModifier, SearchDateModifierLower, SearchKey, ArchivedReportsIDSet};
+export type {SavedSearchMenuItem, SearchTypeMenuSection, SearchTypeMenuItem, SearchDateModifier, SearchDateModifierLower, SearchKey, ArchivedReportsIDSet, GroupBySection};
diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersGroupByPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersGroupByPage.tsx
index 041db1c1441f..e94425fdbf7c 100644
--- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersGroupByPage.tsx
+++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersGroupByPage.tsx
@@ -5,15 +5,15 @@ import FixedFooter from '@components/FixedFooter';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import type {SearchGroupBy} from '@components/Search/types';
-import SelectionList from '@components/SelectionList';
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
+import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections';
import type {ListItem} from '@components/SelectionList/types';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import {updateAdvancedFilters} from '@libs/actions/Search';
import Navigation from '@libs/Navigation/Navigation';
-import {getGroupByOptions} from '@libs/SearchUIUtils';
+import {getGroupBySections} from '@libs/SearchUIUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -23,14 +23,17 @@ function SearchFiltersGroupByPage() {
const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
const [selectedItem, setSelectedItem] = useState(searchAdvancedFiltersForm?.groupBy);
- const listData: Array> = useMemo(() => {
- return getGroupByOptions(translate).map((groupOption) => ({
- text: groupOption.text,
- keyForList: groupOption.value,
- isSelected: selectedItem === groupOption.value,
+ const sections = useMemo(() => {
+ return getGroupBySections(translate).map((section) => ({
+ sectionIndex: section.sectionIndex,
+ ...(section.sectionIndex > 0 ? {customHeader: } : {}),
+ data: section.options.map>((groupOption) => ({
+ text: groupOption.text,
+ keyForList: groupOption.value,
+ isSelected: selectedItem === groupOption.value,
+ })),
}));
- }, [translate, selectedItem]);
-
+ }, [selectedItem, styles.dividerLine, translate]);
const updateSelectedItem = useCallback((type: ListItem) => {
setSelectedItem(type?.keyForList ?? undefined);
}, []);
@@ -60,9 +63,9 @@ function SearchFiltersGroupByPage() {
}}
/>
-