Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Search] Add From & To advanced filters #46962

Merged
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add from and to filters and display their value in advanced filters
  • Loading branch information
Kicu committed Aug 7, 2024
commit c13df3684f467d7cb98e3ce8fc7cbcfeeb40bdc2
5 changes: 5 additions & 0 deletions src/libs/SearchUtils.ts
Original file line number Diff line number Diff line change
@@ -453,6 +453,11 @@ function buildQueryStringFromFilters(filterValues: Partial<SearchAdvancedFilters
return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM}:${accountIDs.join(',')}`;
}

if (filterKey === INPUT_IDS.TO && Array.isArray(filterValue) && filterValue.length > 0) {
const accountIDs = filterValues[filterKey] ?? [];
return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.TO}:${accountIDs.join(',')}`;
}

return undefined;
})
.filter(Boolean)
49 changes: 33 additions & 16 deletions src/pages/Search/AdvancedSearchFilters.tsx
Original file line number Diff line number Diff line change
@@ -5,20 +5,46 @@ import {useOnyx} from 'react-native-onyx';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import {usePersonalDetails} from '@components/OnyxProvider';
import ScrollView from '@components/ScrollView';
import type {AdvancedFiltersKeys} from '@components/Search/types';
import useLocalize from '@hooks/useLocalize';
import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import Navigation from '@libs/Navigation/Navigation';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as SearchUtils from '@libs/SearchUtils';
import * as SearchActions from '@userActions/Search';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
import type {CardList} from '@src/types/onyx';
import type {CardList, PersonalDetailsList} from '@src/types/onyx';

function getFilterCardDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, cards: CardList) {
const filterValue = filters[CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID];
return filterValue
? Object.values(cards)
.filter((card) => filterValue.includes(card.cardID.toString()))
.map((card) => card.bank)
.join(', ')
: undefined;
}

function getFilterParticipantDisplayTitle(accountIDs: string[], personalDetails: PersonalDetailsList) {
const selectedPersonalDetails = accountIDs.map((id) => personalDetails[id]);

return selectedPersonalDetails
.map((personalDetail) => {
if (!personalDetail) {
return '';
}

return PersonalDetailsUtils.createDisplayName(personalDetail.login ?? '', personalDetail);
})
.join(', ');
}

function getFilterDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, fieldName: AdvancedFiltersKeys, translate: LocaleContextProps['translate']) {
if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE) {
@@ -46,6 +72,7 @@ function getFilterDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, fiel
if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION) {
return filters[fieldName];
}

if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID && filters[fieldName]) {
const cards = filters[fieldName] ?? [];
return cards.join(', ');
@@ -58,24 +85,15 @@ function getFilterDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, fiel
return filterValue ? Str.recapitalize(filterValue) : undefined;
}

function getFilterCardDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, cards: CardList) {
const filterValue = filters[CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID];
return filterValue
? Object.values(cards)
.filter((card) => filterValue.includes(card.cardID.toString()))
.map((card) => card.bank)
.join(', ')
: undefined;
}

function AdvancedSearchFilters() {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {singleExecution} = useSingleExecution();
const waitForNavigate = useWaitForNavigation();

const [searchAdvancedFilters = {}] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
const [searchAdvancedFilters = {} as SearchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST);
const personalDetails = usePersonalDetails();

const advancedFilters = useMemo(
() => [
@@ -126,22 +144,21 @@ function AdvancedSearchFilters() {
shouldHide: Object.keys(cardList).length === 0,
},
{
title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM, translate),
title: getFilterParticipantDisplayTitle(searchAdvancedFilters.from ?? [], personalDetails),
description: 'common.from' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_FROM,
},
{
title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_FILTER_KEYS.TO, translate),
title: getFilterParticipantDisplayTitle(searchAdvancedFilters.to ?? [], personalDetails),
description: 'common.to' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_TO,
},
],
[searchAdvancedFilters, translate, cardList],
[searchAdvancedFilters, translate, cardList, personalDetails],
);

const onFormSubmit = () => {
const query = SearchUtils.buildQueryStringFromFilters(searchAdvancedFilters);
console.log({searchAdvancedFilters, query});
SearchActions.clearAdvancedFilters();
Navigation.navigate(
ROUTES.SEARCH_CENTRAL_PANE.getRoute({
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import {usePersonalDetails} from '@components/OnyxProvider';
import {useOptionsList} from '@components/OptionListContextProvider';
import SelectionList from '@components/SelectionList';
import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem';
import useLocalize from '@hooks/useLocalize';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import type {Option} from '@libs/OptionsListUtils';
import type {OptionData} from '@libs/ReportUtils';
import Navigation from '@navigation/Navigation';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';

const defaultListOptions = {
userToInvite: null,
recentReports: [],
personalDetails: [],
currentUserOption: null,
headerMessage: '',
categoryOptions: [],
tagOptions: [],
taxRatesOptions: [],
};

function getSelectedOptionData(option: Option): OptionData {
return {...option, selected: true, reportID: option.reportID ?? '-1'};
}

type SearchFiltersParticipantsSelectorProps = {
initialAccountIDs: string[];
onFiltersUpdate: (accountIDs: string[]) => void;
};

function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: SearchFiltersParticipantsSelectorProps) {
const {translate} = useLocalize();
const personalDetails = usePersonalDetails();
const {options, areOptionsInitialized} = useOptionsList();

const [betas] = useOnyx(ONYXKEYS.BETAS);
const [selectedOptions, setSelectedOptions] = useState<OptionData[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const cleanSearchTerm = useMemo(() => searchTerm.trim().toLowerCase(), [searchTerm]);

const defaultOptions = useMemo(() => {
if (!areOptionsInitialized) {
return defaultListOptions;
}

return OptionsListUtils.getFilteredOptions(options.reports, options.personalDetails, betas, '', selectedOptions);
Kicu marked this conversation as resolved.
Show resolved Hide resolved
}, [areOptionsInitialized, betas, options.personalDetails, options.reports, selectedOptions]);

const chatOptions = useMemo(() => {
return OptionsListUtils.filterOptions(defaultOptions, cleanSearchTerm, {
betas,
selectedOptions,
excludeLogins: CONST.EXPENSIFY_EMAILS,
maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
});
}, [defaultOptions, cleanSearchTerm, betas, selectedOptions]);

const sections = useMemo(() => {
const newSections: OptionsListUtils.CategorySection[] = [];
if (!areOptionsInitialized) {
return newSections;
}

const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(cleanSearchTerm, selectedOptions, chatOptions.recentReports, chatOptions.personalDetails, personalDetails, true);

newSections.push(formatResults.section);

newSections.push({
title: translate('common.recents'),
data: chatOptions.recentReports,
shouldShow: chatOptions.recentReports.length > 0,
});

newSections.push({
title: translate('common.contacts'),
data: chatOptions.personalDetails,
shouldShow: chatOptions.personalDetails.length > 0,
});

return newSections;
}, [areOptionsInitialized, cleanSearchTerm, selectedOptions, chatOptions.recentReports, chatOptions.personalDetails, personalDetails, translate]);

// This effect handles setting initial selectedOptions based on accountIDs saved in onyx form
useEffect(() => {
if (!initialAccountIDs || initialAccountIDs.length === 0) {
return;
}

const [, recents = {data: []}, contacts = {data: []}] = sections;
const allOptions = [...recents.data, ...contacts.data];
const preSelectedOptions: OptionData[] = allOptions
.filter((option) => {
if (!option.accountID) {
return false;
}
return initialAccountIDs.includes(option.accountID.toString());
})
.map(getSelectedOptionData);

setSelectedOptions(preSelectedOptions);
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- this should react only to changes in form data
}, [initialAccountIDs]);

const handleParticipantSelection = useCallback(
(option: Option) => {
const foundOptionIndex = selectedOptions.findIndex((selectedOption: Option) => {
if (selectedOption.accountID && selectedOption.accountID === option?.accountID) {
return true;
}

if (selectedOption.reportID && selectedOption.reportID === option?.reportID) {
return true;
}

return false;
});

if (foundOptionIndex < 0) {
setSelectedOptions([...selectedOptions, getSelectedOptionData(option)]);
} else {
const newSelectedOptions = [...selectedOptions.slice(0, foundOptionIndex), ...selectedOptions.slice(foundOptionIndex + 1)];
setSelectedOptions(newSelectedOptions);
}
},
[selectedOptions],
);

const footerContent = (
<Button
success
text={translate('common.save')}
pressOnEnter
onPress={() => {
const selectedAccountIDs = selectedOptions.map((option) => (option.accountID ? option.accountID.toString() : undefined)).filter(Boolean) as string[];
onFiltersUpdate(selectedAccountIDs);

Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS);
}}
large
/>
);

return (
<SelectionList
canSelectMultiple
sections={sections}
ListItem={InviteMemberListItem}
textInputLabel={translate('selectionList.nameEmailOrPhoneNumber')}
textInputValue={searchTerm}
footerContent={footerContent}
showScrollIndicator
showLoadingPlaceholder={false}
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
onChangeText={(value) => {
setSearchTerm(value);
}}
onSelectRow={handleParticipantSelection}
// isLoadingNewOptions={!!isSearchingForReports} // Todo do we need this in here?
/>
);
}

SearchFiltersParticipantsSelector.displayName = 'SearchFiltersParticipantsSelector';

export default SearchFiltersParticipantsSelector;
Loading