From b43f7c04e6913ade3960877892e84eb44f781668 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Tue, 13 Aug 2024 12:58:22 +0200 Subject: [PATCH 01/18] pulling from, to filters --- src/pages/Search/AdvancedSearchFilters.tsx | 5 +++ src/pages/Search/SearchFiltersInPage.tsx | 42 +++++++++++++++++++++ src/types/form/SearchAdvancedFiltersForm.ts | 2 + 3 files changed, 49 insertions(+) create mode 100644 src/pages/Search/SearchFiltersInPage.tsx diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index 644ae64466f77..9a7688fdc2685 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -190,6 +190,11 @@ function AdvancedSearchFilters() { description: 'common.to' as const, route: ROUTES.SEARCH_ADVANCED_FILTERS_TO, }, + { + title: getFilterParticipantDisplayTitle(searchAdvancedFilters.in ?? [], personalDetails), + description: 'common.to' as const, + route: ROUTES.SEARCH_ADVANCED_FILTERS_TO, + }, ], [searchAdvancedFilters, translate, cardList, taxRates, personalDetails], ); diff --git a/src/pages/Search/SearchFiltersInPage.tsx b/src/pages/Search/SearchFiltersInPage.tsx new file mode 100644 index 0000000000000..1bcf1ea662484 --- /dev/null +++ b/src/pages/Search/SearchFiltersInPage.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SearchFiltersParticipantsSelector from '@components/Search/SearchFiltersParticipantsSelector'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as SearchActions from '@userActions/Search'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function SearchFiltersStatusPage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); + + return ( + + + + { + SearchActions.updateAdvancedFilters({ + from: selectedAccountIDs, + }); + }} + /> + + + ); +} + +SearchFiltersStatusPage.displayName = 'SearchFiltersStatusPage'; + +export default SearchFiltersStatusPage; diff --git a/src/types/form/SearchAdvancedFiltersForm.ts b/src/types/form/SearchAdvancedFiltersForm.ts index f2643c0c987df..2afc63b7a2d38 100644 --- a/src/types/form/SearchAdvancedFiltersForm.ts +++ b/src/types/form/SearchAdvancedFiltersForm.ts @@ -17,6 +17,7 @@ const FILTER_KEYS = { KEYWORD: 'keyword', FROM: 'from', TO: 'to', + IN: 'in', } as const; type InputID = ValueOf; @@ -39,6 +40,7 @@ type SearchAdvancedFiltersForm = Form< [FILTER_KEYS.TAG]: string[]; [FILTER_KEYS.FROM]: string[]; [FILTER_KEYS.TO]: string[]; + [FILTER_KEYS.IN]: string[]; } >; From e2c5d62c367285586385c96124fb503628934bdf Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Mon, 19 Aug 2024 23:23:52 +0200 Subject: [PATCH 02/18] in filter, selecting working but duplicates --- src/ROUTES.ts | 1 + src/SCREENS.ts | 1 + .../Search/SearchFiltersChatsSelector.tsx | 200 ++++++++++++++++++ src/languages/en.ts | 1 + src/languages/es.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + .../CENTRAL_PANE_TO_RHP_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/SearchUtils.ts | 3 +- src/pages/Search/AdvancedSearchFilters.tsx | 4 +- src/pages/Search/SearchFiltersInPage.tsx | 26 ++- 11 files changed, 227 insertions(+), 13 deletions(-) create mode 100644 src/components/Search/SearchFiltersChatsSelector.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 596346568203e..19529217621ee 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -52,6 +52,7 @@ const ROUTES = { SEARCH_ADVANCED_FILTERS_TAG: 'search/filters/tag', SEARCH_ADVANCED_FILTERS_FROM: 'search/filters/from', SEARCH_ADVANCED_FILTERS_TO: 'search/filters/to', + SEARCH_ADVANCED_FILTERS_IN: 'search/filters/in', SEARCH_REPORT: { route: 'search/view/:reportID', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index aff6fb2f94183..cafbce6f86923 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -45,6 +45,7 @@ const SCREENS = { ADVANCED_FILTERS_TAG_RHP: 'Search_Advanced_Filters_Tag_RHP', ADVANCED_FILTERS_FROM_RHP: 'Search_Advanced_Filters_From_RHP', ADVANCED_FILTERS_TO_RHP: 'Search_Advanced_Filters_To_RHP', + ADVANCED_FILTERS_IN_RHP: 'Search_Advanced_Filters_In_RHP', TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP', BOTTOM_TAB: 'Search_Bottom_Tab', }, diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx new file mode 100644 index 0000000000000..339a492b61579 --- /dev/null +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -0,0 +1,200 @@ +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 UserListItem from '@components/SelectionList/UserListItem'; +import useDebouncedState from '@hooks/useDebouncedState'; +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 * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; + +const defaultListOptions = { + recentReports: [], + personalDetails: [], + userToInvite: null, + currentUserOption: null, + categoryOptions: [], + tagOptions: [], + taxRatesOptions: [], + headerMessage: '', +}; + +function getSelectedOptionData(option: Option): OptionData { + return {...option, selected: true, reportID: option.reportID ?? '-1'}; +} + +type SearchFiltersParticipantsSelectorProps = { + initialAccountIDs: string[]; + onFiltersUpdate: (accountIDs: string[]) => void; + isScreenTransitionEnd: boolean; +}; + +function SearchFiltersChatsSelector({initialAccountIDs, onFiltersUpdate, isScreenTransitionEnd}: SearchFiltersParticipantsSelectorProps) { + const {translate} = useLocalize(); + const personalDetails = usePersonalDetails(); + const {options, areOptionsInitialized} = useOptionsList(); + + const [betas] = useOnyx(ONYXKEYS.BETAS); + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); + const [selectedOptions, setSelectedOptions] = useState([]); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const cleanSearchTerm = useMemo(() => searchTerm.trim().toLowerCase(), [searchTerm]); + + const defaultOptions = useMemo(() => { + if (!areOptionsInitialized || !isScreenTransitionEnd) { + return defaultListOptions; + } + const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); + const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, !!optionList.userToInvite, ''); + return {...optionList, headerMessage: header}; + }, [areOptionsInitialized, betas, isScreenTransitionEnd, options]); + + 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 formattedResults = OptionsListUtils.formatSectionsFromSearchTerm( + cleanSearchTerm, + selectedOptions, + chatOptions.recentReports, + chatOptions.personalDetails, + personalDetails, + true, + ); + + const isCurrentUserSelected = selectedOptions.find((option) => option.accountID === chatOptions.currentUserOption?.accountID); + + newSections.push(formattedResults.section); + + if (chatOptions.currentUserOption && !isCurrentUserSelected) { + newSections.push({ + title: '', + data: [chatOptions.currentUserOption], + shouldShow: true, + }); + } + + newSections.push({ + title: '', + data: chatOptions.recentReports, + shouldShow: chatOptions.recentReports.length > 0, + }); + + newSections.push({ + title: '', + data: chatOptions.personalDetails, + shouldShow: chatOptions.personalDetails.length > 0, + }); + + return newSections; + }, [areOptionsInitialized, chatOptions, cleanSearchTerm, selectedOptions, personalDetails]); + + // This effect handles setting initial selectedOptions based on accountIDs saved in onyx form + useEffect(() => { + if (!initialAccountIDs || initialAccountIDs.length === 0 || !personalDetails) { + return; + } + + const preSelectedOptions = initialAccountIDs + .map((accountID) => { + const participant = personalDetails[accountID]; + if (!participant) { + return; + } + + return getSelectedOptionData(participant); + }) + .filter((option): option is NonNullable => { + return !!option; + }); + + setSelectedOptions(preSelectedOptions); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- this should react only to changes in form data + }, [initialAccountIDs, personalDetails]); + + useEffect(() => { + Report.searchInServer(debouncedSearchTerm.trim()); + }, [debouncedSearchTerm]); + + 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 = ( +