Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
97 changes: 76 additions & 21 deletions src/components/Search/FilterDropdowns/SingleSelectPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {View} from 'react-native';
import Button from '@components/Button';
import SelectionList from '@components/SelectionList';
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections';
import type {Section} from '@components/SelectionList/SelectionListWithSections/types';
import type {ListItem} from '@components/SelectionList/types';
import Text from '@components/Text';
import useDebouncedState from '@hooks/useDebouncedState';
Expand All @@ -12,18 +14,25 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import CONST from '@src/CONST';

type SingleSelectItem<T> = {
type SingleSelectItem<T extends string> = {
text: string;
value: T;
};

type SingleSelectPopupProps<T> = {
type SingleSelectSection<T extends string> = Omit<Section<ListItem<T>>, 'data'> & {
data: Array<SingleSelectItem<T>>;
};

type SingleSelectPopupProps<T extends string> = {
/** The label to show when in an overlay on mobile */
label?: string;

/** The list of all items to show up in the list */
items: Array<SingleSelectItem<T>>;

/** Optional sectioned list data used to show grouped items */
sections?: Array<SingleSelectSection<T>>;

/** The currently selected item */
value: SingleSelectItem<T> | null;

Expand All @@ -43,7 +52,7 @@ type SingleSelectPopupProps<T> = {
defaultValue?: string;
};

function SingleSelectPopup<T extends string>({label, value, items, closeOverlay, onChange, isSearchable, searchPlaceholder, defaultValue}: SingleSelectPopupProps<T>) {
function SingleSelectPopup<T extends string>({label, value, items, sections, closeOverlay, onChange, isSearchable, searchPlaceholder, defaultValue}: SingleSelectPopupProps<T>) {
const {translate} = useLocalize();
const styles = useThemeStyles();
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
Expand All @@ -52,7 +61,9 @@ function SingleSelectPopup<T extends string>({label, value, items, closeOverlay,
const [selectedItem, setSelectedItem] = useState(value);
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');

const {options, noResultsFound} = useMemo(() => {
const allSelectableItems = useMemo(() => sections?.flatMap((section) => section.data) ?? items, [sections, items]);

const {options, sectionedOptions, noResultsFound} = useMemo(() => {
// If the selection is searchable, we push the initially selected item into its own section and display it at the top
if (isSearchable) {
const initiallySelectedOption = value?.text.toLowerCase().includes(debouncedSearchTerm?.toLowerCase())
Expand All @@ -69,26 +80,48 @@ function SingleSelectPopup<T extends string>({label, value, items, closeOverlay,
const isEmpty = allOptions.length === 0;
return {
options: allOptions,
sectionedOptions: undefined,
noResultsFound: isEmpty,
};
}

if (sections) {
const mappedSections: Array<Section<ListItem<T>>> = sections.map((section) => ({
...section,
data: section.data.map((item) => ({
text: item.text,
keyForList: item.value,
isSelected: item.value === selectedItem?.value,
})),
}));

return {
options: [],
sectionedOptions: mappedSections,
noResultsFound: mappedSections.every((section) => section.data.length === 0),
};
}

return {
options: items.map((item) => ({
text: item.text,
keyForList: item.value,
isSelected: item.value === selectedItem?.value,
})),
sectionedOptions: undefined,
noResultsFound: false,
};
}, [isSearchable, items, value, selectedItem?.value, debouncedSearchTerm]);
}, [isSearchable, items, value, selectedItem?.value, debouncedSearchTerm, sections]);

const updateSelectedItem = useCallback(
(item: ListItem) => {
const newItem = items.find((i) => i.value === item.keyForList) ?? null;
const newItem = allSelectableItems.find((i) => i.value === item.keyForList);
if (!newItem) {
return;
}
setSelectedItem(newItem);
},
[items],
[allSelectableItems],
);

const applyChanges = useCallback(() => {
Expand All @@ -97,9 +130,9 @@ function SingleSelectPopup<T extends string>({label, value, items, closeOverlay,
}, [closeOverlay, onChange, selectedItem]);

const resetChanges = useCallback(() => {
onChange(defaultValue ? (items.find((item) => item.value === defaultValue) ?? null) : null);
onChange(defaultValue ? (allSelectableItems.find((item) => item.value === defaultValue) ?? null) : null);
closeOverlay();
}, [closeOverlay, onChange, defaultValue, items]);
}, [closeOverlay, onChange, defaultValue, allSelectableItems]);

const textInputOptions = useMemo(
() => ({
Expand All @@ -112,22 +145,44 @@ function SingleSelectPopup<T extends string>({label, value, items, closeOverlay,
);

const shouldShowLabel = isSmallScreenWidth && !!label;
const optionsCount = Math.max(
1,
sectionedOptions
? sectionedOptions.reduce((count, section) => {
const hasHeader = section.data.length > 0 && (section.title ?? section.customHeader);
return count + section.data.length + (hasHeader ? 1 : 0);
}, 0)
: options.length,
);

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

<View style={[styles.getSelectionListPopoverHeight(options.length || 1, windowHeight, isSearchable ?? false)]}>
<SelectionList
data={options}
shouldSingleExecuteRowSelect
ListItem={SingleSelectListItem}
onSelectRow={updateSelectedItem}
textInputOptions={textInputOptions}
shouldUpdateFocusedIndex={isSearchable}
initiallyFocusedItemKey={isSearchable ? value?.value : undefined}
showLoadingPlaceholder={!noResultsFound}
/>
<View style={[styles.getSelectionListPopoverHeight(optionsCount, windowHeight, isSearchable ?? false)]}>
{sectionedOptions ? (
<SelectionListWithSections
sections={sectionedOptions}
shouldSingleExecuteRowSelect
ListItem={SingleSelectListItem}
onSelectRow={updateSelectedItem}
textInputOptions={textInputOptions}
shouldUpdateFocusedIndex={isSearchable}
initiallyFocusedItemKey={isSearchable ? value?.value : undefined}
showLoadingPlaceholder={!noResultsFound}
/>
) : (
<SelectionList
data={options}
shouldSingleExecuteRowSelect
ListItem={SingleSelectListItem}
onSelectRow={updateSelectedItem}
textInputOptions={textInputOptions}
shouldUpdateFocusedIndex={isSearchable}
initiallyFocusedItemKey={isSearchable ? value?.value : undefined}
showLoadingPlaceholder={!noResultsFound}
/>
)}
</View>
<View style={[styles.flexRow, styles.gap2, styles.ph5]}>
<Button
Expand All @@ -150,5 +205,5 @@ function SingleSelectPopup<T extends string>({label, value, items, closeOverlay,
);
}

export type {SingleSelectPopupProps, SingleSelectItem};
export type {SingleSelectPopupProps, SingleSelectItem, SingleSelectSection};
export default SingleSelectPopup;
7 changes: 7 additions & 0 deletions src/components/Search/SearchPageHeader/SearchFiltersBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
getDatePresets,
getFeedOptions,
getGroupByOptions,
getGroupBySections,
getGroupCurrencyOptions,
getHasOptions,
getStatusOptions,
Expand Down Expand Up @@ -229,6 +230,11 @@ function SearchFiltersBar({
})();

const groupByOptions = getGroupByOptions(translate);
const groupBySections = getGroupBySections(translate).map((section, sectionIndex) => ({
title: section.title,
sectionIndex,
data: section.options,
}));
const groupBy = groupByOptions.find((option) => option.value === unsafeGroupBy) ?? null;

const viewOptions = getViewOptions(translate);
Expand Down Expand Up @@ -358,6 +364,7 @@ function SearchFiltersBar({
<SingleSelectPopup
label={translate('search.groupBy')}
items={groupByOptions}
sections={groupBySections}
value={groupBy}
closeOverlay={closeOverlay}
onChange={(item) => {
Expand Down
35 changes: 35 additions & 0 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3728,6 +3728,40 @@ function getGroupByOptions(translate: LocalizedTranslate) {
return Object.values(CONST.SEARCH.GROUP_BY).map<SingleSelectItem<SearchGroupBy>>((value) => ({text: translate(`search.filters.groupBy.${value}`), value}));
}

type GroupBySection = {
title: string;
options: Array<SingleSelectItem<SearchGroupBy>>;
};

function getGroupBySections(translate: LocalizedTranslate): GroupBySection[] {
const groupByOptions = getGroupByOptions(translate);
const optionsByValue = new Map(groupByOptions.map((option) => [option.value, option]));
const getOption = (groupBy: SearchGroupBy): SingleSelectItem<SearchGroupBy> =>
optionsByValue.get(groupBy) ?? {
text: translate(`search.filters.groupBy.${groupBy}`),
value: groupBy,
};

return [
{
title: translate('common.member'),
options: [getOption(CONST.SEARCH.GROUP_BY.FROM)],
},
{
title: `${translate('common.expense')} ${translate('common.details')}`,
options: [getOption(CONST.SEARCH.GROUP_BY.CARD), getOption(CONST.SEARCH.GROUP_BY.MERCHANT), getOption(CONST.SEARCH.GROUP_BY.CATEGORY), getOption(CONST.SEARCH.GROUP_BY.TAG)],
},
{
title: translate('common.date'),
options: [getOption(CONST.SEARCH.GROUP_BY.WEEK), getOption(CONST.SEARCH.GROUP_BY.MONTH), getOption(CONST.SEARCH.GROUP_BY.QUARTER), getOption(CONST.SEARCH.GROUP_BY.YEAR)],
},
{
title: translate('search.reconciliation'),
options: [getOption(CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID)],
},
];
}

function getViewOptions(translate: LocalizedTranslate) {
return Object.values(CONST.SEARCH.VIEW).map<SingleSelectItem<SearchView>>((value) => ({text: translate(`search.view.${value}`), value}));
}
Expand Down Expand Up @@ -4408,6 +4442,7 @@ export {
getStatusOptions,
getTypeOptions,
getGroupByOptions,
getGroupBySections,
getViewOptions,
getGroupCurrencyOptions,
getFeedOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -23,11 +23,15 @@ function SearchFiltersGroupByPage() {
const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true});
const [selectedItem, setSelectedItem] = useState(searchAdvancedFiltersForm?.groupBy);

const listData: Array<ListItem<SearchGroupBy>> = 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) => ({
title: section.title,
sectionIndex,
data: section.options.map((groupOption) => ({
text: groupOption.text,
keyForList: groupOption.value,
isSelected: selectedItem === groupOption.value,
})),
}));
}, [translate, selectedItem]);

Expand Down Expand Up @@ -60,9 +64,9 @@ function SearchFiltersGroupByPage() {
}}
/>
<View style={[styles.flex1]}>
<SelectionList
<SelectionListWithSections
shouldSingleExecuteRowSelect
data={listData}
sections={sections}
ListItem={SingleSelectListItem}
onSelectRow={updateSelectedItem}
/>
Expand Down
Loading