Skip to content

Commit cd4db13

Browse files
authored
Merge pull request Expensify#83428 from marufsharifi/bugfix/group-by-filter-section-structure
Fix: Add sections within Group By filters
2 parents bee2195 + 7c1c389 commit cd4db13

4 files changed

Lines changed: 168 additions & 14 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import React, {useCallback, useMemo, useState} from 'react';
2+
import {View} from 'react-native';
3+
import Button from '@components/Button';
4+
import type {SearchGroupBy} from '@components/Search/types';
5+
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
6+
import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections';
7+
import type {ListItem} from '@components/SelectionList/types';
8+
import Text from '@components/Text';
9+
import useLocalize from '@hooks/useLocalize';
10+
import useResponsiveLayout from '@hooks/useResponsiveLayout';
11+
import useThemeStyles from '@hooks/useThemeStyles';
12+
import useWindowDimensions from '@hooks/useWindowDimensions';
13+
import type {GroupBySection} from '@libs/SearchUIUtils';
14+
import CONST from '@src/CONST';
15+
16+
type GroupByPopupItem = {
17+
text: string;
18+
value: SearchGroupBy;
19+
};
20+
21+
type GroupByPopupProps = {
22+
/** The label to show when in an overlay on mobile */
23+
label?: string;
24+
25+
/** The grouped options to show in the list */
26+
sections: GroupBySection[];
27+
28+
/** The currently selected item */
29+
value: GroupByPopupItem | null;
30+
31+
/** Function to call to close the overlay when changes are applied */
32+
closeOverlay: () => void;
33+
34+
/** Function to call when changes are applied */
35+
onChange: (item: GroupByPopupItem | null) => void;
36+
};
37+
38+
function GroupByPopup({label, value, sections, closeOverlay, onChange}: GroupByPopupProps) {
39+
const {translate} = useLocalize();
40+
const styles = useThemeStyles();
41+
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
42+
const {isSmallScreenWidth} = useResponsiveLayout();
43+
const {windowHeight} = useWindowDimensions();
44+
const [selectedItem, setSelectedItem] = useState(value);
45+
46+
const allSelectableItems = useMemo(() => sections.flatMap((section) => section.options), [sections]);
47+
48+
const listSections = useMemo(
49+
() =>
50+
sections.map((section) => ({
51+
sectionIndex: section.sectionIndex,
52+
...(section.sectionIndex > 0 ? {customHeader: <View style={styles.dividerLine} />} : {}),
53+
data: section.options.map<ListItem<SearchGroupBy>>((item) => ({
54+
text: item.text,
55+
keyForList: item.value,
56+
isSelected: item.value === selectedItem?.value,
57+
})),
58+
})),
59+
[sections, selectedItem?.value, styles.dividerLine],
60+
);
61+
62+
const optionsCount = Math.max(
63+
1,
64+
listSections.reduce((count, section) => count + section.data.length + (section.data.length > 0 && !!section.customHeader ? 1 : 0), 0),
65+
);
66+
67+
const updateSelectedItem = useCallback(
68+
(item: ListItem) => {
69+
const newItem = allSelectableItems.find((option) => option.value === item.keyForList) ?? null;
70+
setSelectedItem(newItem);
71+
},
72+
[allSelectableItems],
73+
);
74+
75+
const applyChanges = useCallback(() => {
76+
onChange(selectedItem);
77+
closeOverlay();
78+
}, [closeOverlay, onChange, selectedItem]);
79+
80+
const resetChanges = useCallback(() => {
81+
onChange(null);
82+
closeOverlay();
83+
}, [closeOverlay, onChange]);
84+
85+
return (
86+
<View style={[!isSmallScreenWidth && styles.pv4, styles.gap2]}>
87+
{isSmallScreenWidth && !!label && <Text style={[styles.textLabel, styles.textSupporting, styles.ph5, styles.pv1]}>{label}</Text>}
88+
89+
<View style={[styles.getSelectionListPopoverHeight(optionsCount, windowHeight, false)]}>
90+
<SelectionListWithSections
91+
sections={listSections}
92+
shouldSingleExecuteRowSelect
93+
ListItem={SingleSelectListItem}
94+
onSelectRow={updateSelectedItem}
95+
/>
96+
</View>
97+
98+
<View style={[styles.flexRow, styles.gap2, styles.ph5]}>
99+
<Button
100+
medium
101+
style={[styles.flex1]}
102+
text={translate('common.reset')}
103+
onPress={resetChanges}
104+
sentryLabel={CONST.SENTRY_LABEL.SEARCH.FILTER_POPUP_RESET_SINGLE_SELECT}
105+
/>
106+
<Button
107+
success
108+
medium
109+
style={[styles.flex1]}
110+
text={translate('common.apply')}
111+
onPress={applyChanges}
112+
sentryLabel={CONST.SENTRY_LABEL.SEARCH.FILTER_POPUP_APPLY_SINGLE_SELECT}
113+
/>
114+
</View>
115+
</View>
116+
);
117+
}
118+
119+
export default GroupByPopup;

src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {OnyxCollection} from 'react-native-onyx';
55
import {usePersonalDetails} from '@components/OnyxListItemProvider';
66
import type {SearchDateValues} from '@components/Search/FilterComponents/DatePresetFilterBase';
77
import type {PopoverComponentProps} from '@components/Search/FilterDropdowns/DropdownButton';
8+
import GroupByPopup from '@components/Search/FilterDropdowns/GroupByPopup';
89
import type {MultiSelectItem} from '@components/Search/FilterDropdowns/MultiSelectPopup';
910
import MultiSelectPopup from '@components/Search/FilterDropdowns/MultiSelectPopup';
1011
import SingleSelectPopup from '@components/Search/FilterDropdowns/SingleSelectPopup';
@@ -33,6 +34,7 @@ import {
3334
filterValidHasValues,
3435
getFeedOptions,
3536
getGroupByOptions,
37+
getGroupBySections,
3638
getGroupCurrencyOptions,
3739
getHasOptions,
3840
getStatusOptions,
@@ -165,6 +167,7 @@ function useSearchFiltersBar(queryJSON: SearchQueryJSON, isMobileSelectionModeEn
165167
const type = typeOptions.find((option) => option.value === unsafeType) ?? null;
166168

167169
const groupByOptions = getGroupByOptions(translate);
170+
const groupBySections = getGroupBySections(translate);
168171
const groupBy = groupByOptions.find((option) => option.value === unsafeGroupBy) ?? null;
169172

170173
const viewOptions = getViewOptions(translate);
@@ -283,9 +286,9 @@ function useSearchFiltersBar(queryJSON: SearchQueryJSON, isMobileSelectionModeEn
283286
);
284287

285288
const groupByComponent = ({closeOverlay}: PopoverComponentProps) => (
286-
<SingleSelectPopup
289+
<GroupByPopup
287290
label={translate('search.groupBy')}
288-
items={groupByOptions}
291+
sections={groupBySections}
289292
value={groupBy}
290293
closeOverlay={closeOverlay}
291294
onChange={(item) => {

src/libs/SearchUIUtils.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ import ViolationsUtils from './Violations/ViolationsUtils';
163163

164164
type ColumnSortMapping<T> = Partial<Record<SearchColumnType, keyof T | null>>;
165165
type ColumnVisibility = Partial<Record<SearchColumnType, boolean>>;
166+
type GroupBySection = {
167+
sectionIndex: number;
168+
options: Array<SingleSelectItem<SearchGroupBy>>;
169+
};
166170

167171
// List Item sorting
168172
type TransactionSorting = ColumnSortMapping<TransactionListItemType>;
@@ -3989,6 +3993,30 @@ function getGroupByOptions(translate: LocalizedTranslate) {
39893993
return Object.values(CONST.SEARCH.GROUP_BY).map<SingleSelectItem<SearchGroupBy>>((value) => ({text: translate(`search.filters.groupBy.${value}`), value}));
39903994
}
39913995

3996+
function getGroupBySections(translate: LocalizedTranslate): GroupBySection[] {
3997+
const getOption = (groupBy: SearchGroupBy): SingleSelectItem<SearchGroupBy> => ({
3998+
text: translate(`search.filters.groupBy.${groupBy}`),
3999+
value: groupBy,
4000+
});
4001+
return [
4002+
{
4003+
options: [getOption(CONST.SEARCH.GROUP_BY.FROM), getOption(CONST.SEARCH.GROUP_BY.CARD)],
4004+
},
4005+
{
4006+
options: [getOption(CONST.SEARCH.GROUP_BY.CATEGORY), getOption(CONST.SEARCH.GROUP_BY.MERCHANT), getOption(CONST.SEARCH.GROUP_BY.TAG)],
4007+
},
4008+
{
4009+
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)],
4010+
},
4011+
{
4012+
options: [getOption(CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID)],
4013+
},
4014+
].map((section, sectionIndex) => ({
4015+
...section,
4016+
sectionIndex,
4017+
}));
4018+
}
4019+
39924020
function getViewOptions(translate: LocalizedTranslate) {
39934021
return Object.values(CONST.SEARCH.VIEW).map<SingleSelectItem<SearchView>>((value) => ({text: translate(`search.view.${value}`), value}));
39944022
}
@@ -4754,6 +4782,7 @@ export {
47544782
getStatusOptions,
47554783
getTypeOptions,
47564784
getGroupByOptions,
4785+
getGroupBySections,
47574786
getViewOptions,
47584787
getGroupCurrencyOptions,
47594788
getFeedOptions,
@@ -4784,4 +4813,4 @@ export {
47844813
isEligibleForApproveSuggestion,
47854814
applySelectionToItem,
47864815
};
4787-
export type {SavedSearchMenuItem, SearchTypeMenuSection, SearchTypeMenuItem, SearchDateModifier, SearchDateModifierLower, SearchKey, ArchivedReportsIDSet};
4816+
export type {SavedSearchMenuItem, SearchTypeMenuSection, SearchTypeMenuItem, SearchDateModifier, SearchDateModifierLower, SearchKey, ArchivedReportsIDSet, GroupBySection};

src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersGroupByPage.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import FixedFooter from '@components/FixedFooter';
55
import HeaderWithBackButton from '@components/HeaderWithBackButton';
66
import ScreenWrapper from '@components/ScreenWrapper';
77
import type {SearchGroupBy} from '@components/Search/types';
8-
import SelectionList from '@components/SelectionList';
98
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
9+
import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections';
1010
import type {ListItem} from '@components/SelectionList/types';
1111
import useLocalize from '@hooks/useLocalize';
1212
import useOnyx from '@hooks/useOnyx';
1313
import useThemeStyles from '@hooks/useThemeStyles';
1414
import {updateAdvancedFilters} from '@libs/actions/Search';
1515
import Navigation from '@libs/Navigation/Navigation';
16-
import {getGroupByOptions} from '@libs/SearchUIUtils';
16+
import {getGroupBySections} from '@libs/SearchUIUtils';
1717
import ONYXKEYS from '@src/ONYXKEYS';
1818
import ROUTES from '@src/ROUTES';
1919

@@ -23,14 +23,17 @@ function SearchFiltersGroupByPage() {
2323
const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
2424
const [selectedItem, setSelectedItem] = useState(searchAdvancedFiltersForm?.groupBy);
2525

26-
const listData: Array<ListItem<SearchGroupBy>> = useMemo(() => {
27-
return getGroupByOptions(translate).map((groupOption) => ({
28-
text: groupOption.text,
29-
keyForList: groupOption.value,
30-
isSelected: selectedItem === groupOption.value,
26+
const sections = useMemo(() => {
27+
return getGroupBySections(translate).map((section) => ({
28+
sectionIndex: section.sectionIndex,
29+
...(section.sectionIndex > 0 ? {customHeader: <View style={styles.dividerLine} />} : {}),
30+
data: section.options.map<ListItem<SearchGroupBy>>((groupOption) => ({
31+
text: groupOption.text,
32+
keyForList: groupOption.value,
33+
isSelected: selectedItem === groupOption.value,
34+
})),
3135
}));
32-
}, [translate, selectedItem]);
33-
36+
}, [selectedItem, styles.dividerLine, translate]);
3437
const updateSelectedItem = useCallback((type: ListItem<SearchGroupBy>) => {
3538
setSelectedItem(type?.keyForList ?? undefined);
3639
}, []);
@@ -60,9 +63,9 @@ function SearchFiltersGroupByPage() {
6063
}}
6164
/>
6265
<View style={[styles.flex1]}>
63-
<SelectionList
66+
<SelectionListWithSections
6467
shouldSingleExecuteRowSelect
65-
data={listData}
68+
sections={sections}
6669
ListItem={SingleSelectListItem}
6770
onSelectRow={updateSelectedItem}
6871
/>

0 commit comments

Comments
 (0)