From d15fbc7a5b34a4edb988d1235e0fd29b978348b8 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 14:00:33 +0800 Subject: [PATCH 01/32] allow workspace and status filter to be negatable --- src/CONST/index.ts | 1 - .../AdvancedFilters/CommonFilterContent.tsx | 21 +- .../AdvancedFilters/FilterList.tsx | 3 +- .../SearchAdvancedFiltersContent.tsx | 12 +- .../TextInputFilterContent.tsx | 3 +- .../FilterComponents/CategorySelector.tsx | 12 +- .../FilterComponents/ExportedToSelector.tsx | 10 +- .../Search/FilterComponents/TagSelector.tsx | 12 +- .../FilterComponents/TaxRateSelector.tsx | 16 +- .../FilterComponents/WorkspaceSelector.tsx | 8 +- .../Search/FilterComponents/index.tsx | 18 +- .../Search/FilterDropdowns/CommonPopup.tsx | 8 +- .../CommonFilterContentPopupWrapper.tsx | 5 +- .../SearchAdvancedFiltersPopup/index.tsx | 5 +- .../SearchPageHeader/useSearchFiltersBar.tsx | 10 +- .../Search/SearchResultsProvider.tsx | 2 +- src/components/Search/SearchStaticList.tsx | 4 +- .../Search/hooks/useSearchSnapshot.ts | 6 +- src/components/Search/index.tsx | 4 +- src/components/Search/types.ts | 3 - src/hooks/useAdvancedSearchFilters.ts | 3 +- src/hooks/useAutocompleteSuggestions.ts | 2 +- src/hooks/useExportedToFilterOptions.ts | 8 +- src/hooks/useSearchBulkActions.ts | 13 +- src/hooks/useSearchSections.ts | 4 +- .../ExportSearchItemsToCSVParams.ts | 1 - src/libs/PolicyUtils.ts | 8 +- src/libs/SearchAutocompleteUtils.ts | 2 +- src/libs/SearchParser/searchParser.js | 32 ++- src/libs/SearchParser/searchParser.peggy | 12 +- src/libs/SearchQueryUtils.ts | 187 +++++++++++------- src/libs/SearchUIUtils.ts | 57 +++--- src/libs/actions/IOU/SearchUpdate.ts | 21 +- src/libs/actions/Search.ts | 7 +- src/pages/Search/EmptySearchView.tsx | 8 +- .../CommonFilterContentPageWrapper.tsx | 5 +- .../SearchAdvancedFiltersContentBase.tsx | 3 - .../SearchAdvancedFiltersBase.tsx | 4 +- src/pages/Search/SearchSavePage.tsx | 2 +- .../useSpendOverTimeData.ts | 1 - src/types/form/SearchAdvancedFiltersForm.ts | 53 ++++- src/types/onyx/SearchResults.ts | 38 ++-- tests/actions/IOU/RequestMoneyTest.ts | 1 - tests/actions/IOU/SearchUpdateTest.ts | 14 +- .../useAdvancedSearchFilters.perf-test.tsx | 2 +- .../HomePage/YourSpendSection/rowStateTest.ts | 3 +- .../YourSpendSection/useYourSpendDataTest.ts | 7 +- tests/unit/PolicyUtilsTest.ts | 10 +- .../unit/Search/ExpenseFlatSearchViewTest.tsx | 2 - .../unit/Search/SearchListRenderCountTest.tsx | 2 - tests/unit/Search/SearchQueryUtilsTest.ts | 3 +- tests/unit/Search/SearchUIUtilsTest.ts | 112 ++++------- .../unit/Search/getSpendOverTimeStateTest.ts | 3 +- .../unit/Search/yourSpendQueryBuildersTest.ts | 19 +- tests/unit/SearchActionsTest.ts | 1 - tests/unit/SearchParserTest.ts | 88 +-------- .../hooks/useAdvancedSearchFilters.test.ts | 48 ++--- .../hooks/useAutocompleteSuggestions.test.ts | 2 +- .../hooks/useSearchBulkActionsDeleteTest.ts | 1 - .../useSearchBulkActionsDownloadPDFTest.ts | 1 - .../useSearchBulkActionsDuplicateTest.ts | 2 - .../hooks/useSearchBulkActionsExportTest.ts | 3 +- tests/unit/hooks/useSearchBulkActionsTest.ts | 1 - tests/unit/useSearchHighlightAndScrollTest.ts | 3 +- 64 files changed, 450 insertions(+), 512 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 5eacbfd0d4bd..18ae3d2edeb3 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6779,7 +6779,6 @@ const CONST = { }, SYNTAX_ROOT_KEYS: { TYPE: 'type', - STATUS: 'status', SORT_BY: 'sortBy', SORT_ORDER: 'sortOrder', VIEW: 'view', diff --git a/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx index 39cd1d50570e..525fb0224989 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type {SearchFilterCommonProps} from '@components/Search/types'; +import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import type {FilterComponentsProps} from '..'; import FilterComponents from '..'; @@ -7,31 +8,17 @@ import FilterComponents from '..'; type CommonFilterContentProps = SearchFilterCommonProps & { filterKey: FilterComponentsProps['filterKey']; type: SearchDataTypes | undefined; - policyIDs: string[] | undefined; - policyIDQuery: string[] | undefined; + policyID: PolicyIDFilter; }; -function CommonFilterContent({ - filterKey, - value, - type, - ready, - policyIDs, - policyIDQuery, - autoFocus, - selectionListTextInputStyle, - selectionListStyle, - footer, - onChange, -}: CommonFilterContentProps) { +function CommonFilterContent({filterKey, value, type, ready, policyID, autoFocus, selectionListTextInputStyle, selectionListStyle, footer, onChange}: CommonFilterContentProps) { return ( ; contentContainerStyle?: StyleProp; diff --git a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx index d4365dbb5518..956529c00f5f 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {isAmountFilterKey, isDateFilterKey} from '@libs/SearchUIUtils'; +import {getFilterNegatableValue, isAmountFilterKey, isDateFilterKey} from '@libs/SearchUIUtils'; import type {SearchFilter} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; @@ -18,7 +18,6 @@ type CommonFilterContentWrapperProps = Omit | undefined; - policyIDQuery: string[] | undefined; ready?: boolean; components: { Text: React.ComponentType; @@ -30,13 +29,13 @@ type SearchAdvancedFiltersContentProps = { onChange: (values: Partial) => void; }; -function getFilterFormValue(filterKey: K, value: SearchAdvancedFiltersForm[K] | undefined): Partial { +function getFilterFormValue(filterKey: K, value: FilterComponentsProps['value']): Partial { const update: Partial = {}; - update[filterKey] = value; + update[filterKey] = value as SearchAdvancedFiltersForm[K]; return update; } -function SearchAdvancedFiltersContent({filterKey, values, policyIDQuery, ready, components, onChange}: SearchAdvancedFiltersContentProps) { +function SearchAdvancedFiltersContent({filterKey, values, ready, components, onChange}: SearchAdvancedFiltersContentProps) { const {Text: TextFilter, Amount: AmountFilter, Date: DateFilter, ReportField: ReportFieldFilter, Common: CommonFilter} = components; if ( @@ -116,8 +115,7 @@ function SearchAdvancedFiltersContent({filterKey, values, policyIDQuery, ready, filterKey={filterKey} value={values?.[filterKey]} type={values?.type} - policyIDs={values?.policyID} - policyIDQuery={policyIDQuery} + policyID={getFilterNegatableValue(CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, values)} ready={ready} onChange={(newValue) => onChange(getFilterFormValue(filterKey, newValue))} /> diff --git a/src/components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent.tsx index 71082b1d8ad6..2d1d10410d0f 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent.tsx @@ -31,9 +31,8 @@ function TextInputFilterContent({filterKey, value: initialValue, autoFocus, larg setValue(typeof v === 'string' ? v : undefined)} /> diff --git a/src/components/Search/FilterComponents/CategorySelector.tsx b/src/components/Search/FilterComponents/CategorySelector.tsx index 3c3c076b24c0..0889c0723593 100644 --- a/src/components/Search/FilterComponents/CategorySelector.tsx +++ b/src/components/Search/FilterComponents/CategorySelector.tsx @@ -5,6 +5,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {getDecodedCategoryName} from '@libs/CategoryUtils'; import {sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils'; +import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyCategories, PolicyCategory} from '@src/types/onyx'; @@ -12,10 +13,10 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; import MultiSelect from './MultiSelect'; type CategorySelectorProps = SearchFilterCommonProps & { - policyIDs: string[] | undefined; + policyID: PolicyIDFilter; }; -function CategorySelector({value = [], policyIDs = [], selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: CategorySelectorProps) { +function CategorySelector({value = [], policyID, selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: CategorySelectorProps) { const {translate, localeCompare} = useLocalize(); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); @@ -45,13 +46,16 @@ function CategorySelector({value = [], policyIDs = [], selectionListTextInputSty [availableNonPersonalPolicyCategoriesSelector], ); const selectedPoliciesCategories: PolicyCategory[] = Object.keys(allPolicyCategories ?? {}) - .filter((key) => policyIDs.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`)?.includes(key)) + .filter((key) => { + const isSelected = policyID.value?.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`)?.includes(key); + return policyID.isNegated ? !isSelected : isSelected; + }) .map((key) => Object.values(allPolicyCategories?.[key] ?? {})) .flat(); const categoryItems = [{text: translate('search.noCategory'), value: CONST.SEARCH.CATEGORY_EMPTY_VALUE as string}]; const uniqueCategoryNames = new Set(); - if (policyIDs.length === 0) { + if (!policyID.value?.length) { const categories = Object.values(allPolicyCategories ?? {}).flatMap((policyCategories) => Object.values(policyCategories ?? {})); for (const category of categories) { uniqueCategoryNames.add(category.name); diff --git a/src/components/Search/FilterComponents/ExportedToSelector.tsx b/src/components/Search/FilterComponents/ExportedToSelector.tsx index 2b0fd8bda9f5..f924581e99a0 100644 --- a/src/components/Search/FilterComponents/ExportedToSelector.tsx +++ b/src/components/Search/FilterComponents/ExportedToSelector.tsx @@ -13,6 +13,8 @@ import {getSearchValueForConnection} from '@libs/AccountingUtils'; import {getExportTemplates} from '@libs/actions/Search'; import {getConnectedIntegrationNamesForPolicies} from '@libs/PolicyUtils'; import {getIntegrationIcon} from '@libs/ReportUtils'; +import {getAllPolicyValues} from '@libs/SearchQueryUtils'; +import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -20,7 +22,7 @@ import type IconAsset from '@src/types/utils/IconAsset'; import MultiSelect from './MultiSelect'; type ExportedToSelectorProps = SearchFilterCommonProps & { - policyIDs: string[] | undefined; + policyID: PolicyIDFilter; }; const STANDARD_EXPORT_TEMPLATE_ID_TO_DISPLAY_LABEL: Record = { @@ -28,7 +30,7 @@ const STANDARD_EXPORT_TEMPLATE_ID_TO_DISPLAY_LABEL: Record = { [CONST.REPORT.EXPORT_OPTIONS.EXPENSE_LEVEL_EXPORT]: CONST.REPORT.EXPORT_OPTION_LABELS.EXPENSE_LEVEL_EXPORT, }; -function ExportedToSelector({value = [], policyIDs = [], selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: ExportedToSelectorProps) { +function ExportedToSelector({value = [], policyID, selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: ExportedToSelectorProps) { const styles = useThemeStyles(); const {translate, localeCompare} = useLocalize(); const StyleUtils = useStyleUtils(); @@ -48,7 +50,7 @@ function ExportedToSelector({value = [], policyIDs = [], selectionListTextInputS const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); - const connectedIntegrationNames = getConnectedIntegrationNamesForPolicies(policies, policyIDs.length > 0 ? policyIDs : undefined); + const connectedIntegrationNames = getConnectedIntegrationNamesForPolicies(policies, policyID); const integrationConnectionNames = CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES; @@ -89,7 +91,7 @@ function ExportedToSelector({value = [], policyIDs = [], selectionListTextInputS }); const usedPickerValueKeys = new Set(connectedIntegrationPickerItems.map((item) => item.value)); - const policiesToLoadTemplatesFrom = policyIDs.length > 0 ? policyIDs.map((id) => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${id}`]).filter(Boolean) : Object.values(policies ?? {}); + const policiesToLoadTemplatesFrom = policyID.value?.length ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies ?? {}); const exportTemplatesFromPolicies = policiesToLoadTemplatesFrom.flatMap((policy) => getExportTemplates([], {}, translate, policy, false)); const exportTemplatesFromAccount = getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, undefined, true); const allExportTemplates = [...exportTemplatesFromAccount, ...exportTemplatesFromPolicies]; diff --git a/src/components/Search/FilterComponents/TagSelector.tsx b/src/components/Search/FilterComponents/TagSelector.tsx index bb35fde7ffdb..98f47c640737 100644 --- a/src/components/Search/FilterComponents/TagSelector.tsx +++ b/src/components/Search/FilterComponents/TagSelector.tsx @@ -5,6 +5,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {getCleanedTagName, getTagNamesFromTagsLists} from '@libs/PolicyUtils'; import {sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils'; +import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import passthroughPolicyTagListSelector from '@src/selectors/PolicyTagList'; @@ -13,21 +14,24 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; import MultiSelect from './MultiSelect'; type TagSelectorProps = SearchFilterCommonProps & { - policyIDs: string[] | undefined; + policyID: PolicyIDFilter; }; -function TagSelector({value = [], policyIDs = [], selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: TagSelectorProps) { +function TagSelector({value = [], policyID, selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: TagSelectorProps) { const {translate, localeCompare} = useLocalize(); const [allPolicyTagLists = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: passthroughPolicyTagListSelector}); const selectedPoliciesTagLists = Object.keys(allPolicyTagLists ?? {}) - .filter((key) => policyIDs.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`)?.includes(key)) + .filter((key) => { + const isSelected = policyID.value?.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`)?.includes(key); + return policyID.isNegated ? !isSelected : isSelected; + }) ?.map((key) => getTagNamesFromTagsLists(allPolicyTagLists?.[key] ?? {})) .flat(); const tagItems = [{text: translate('search.noTag'), value: CONST.SEARCH.TAG_EMPTY_VALUE as string}]; const uniqueTagNames = new Set(); - if (policyIDs.length === 0) { + if (!policyID.value?.length) { const tagListsUnpacked = Object.values(allPolicyTagLists ?? {}).filter((item) => !!item); for (const tag of tagListsUnpacked.map(getTagNamesFromTagsLists).flat()) { uniqueTagNames.add(tag); diff --git a/src/components/Search/FilterComponents/TaxRateSelector.tsx b/src/components/Search/FilterComponents/TaxRateSelector.tsx index 92768d9fad60..7b6681423cdd 100644 --- a/src/components/Search/FilterComponents/TaxRateSelector.tsx +++ b/src/components/Search/FilterComponents/TaxRateSelector.tsx @@ -3,28 +3,22 @@ import type {SearchFilterCommonProps} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {getAllTaxRates} from '@libs/PolicyUtils'; +import {getAllPolicyValuesMap} from '@libs/SearchQueryUtils'; +import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy} from '@src/types/onyx'; import MultiSelect from './MultiSelect'; type TaxRateSelectorProps = SearchFilterCommonProps & { - policyIDs: string[] | undefined; + policyID: PolicyIDFilter; }; -function TaxRateSelector({value = [], policyIDs = [], selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: TaxRateSelectorProps) { +function TaxRateSelector({value = [], policyID, selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: TaxRateSelectorProps) { const {localeCompare} = useLocalize(); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const allTaxRates = getAllTaxRates(policies); - const selectedPoliciesMap = policyIDs?.reduce>((acc, policyID) => { - const key = `${ONYXKEYS.COLLECTION.POLICY}${policyID}`; - const policy = policies?.[key]; - if (policy) { - acc[key] = policy; - } - return acc; - }, {}); + const selectedPoliciesMap = getAllPolicyValuesMap(policyID, ONYXKEYS.COLLECTION.POLICY, policies); const scopedTaxRates = !selectedPoliciesMap || Object.keys(selectedPoliciesMap).length === 0 ? allTaxRates : getAllTaxRates(selectedPoliciesMap); const taxItems = Object.entries(scopedTaxRates) .map(([taxRateName, taxRateKeys]) => ({ diff --git a/src/components/Search/FilterComponents/WorkspaceSelector.tsx b/src/components/Search/FilterComponents/WorkspaceSelector.tsx index 5302a712e741..69c5f99f9711 100644 --- a/src/components/Search/FilterComponents/WorkspaceSelector.tsx +++ b/src/components/Search/FilterComponents/WorkspaceSelector.tsx @@ -23,11 +23,9 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import ListFilterView from './ListFilterViewWrapper'; import type {MultiSelectItem} from './MultiSelect'; -type WorkspaceSelectorProps = SearchFilterCommonProps & { - policyIDQuery: string[] | undefined; -}; +type WorkspaceSelectorProps = SearchFilterCommonProps; -function WorkspaceSelector({policyIDQuery, value, selectionListTextInputStyle, selectionListStyle, autoFocus, ready = true, footer, onChange}: WorkspaceSelectorProps) { +function WorkspaceSelector({value, selectionListTextInputStyle, selectionListStyle, autoFocus, ready = true, footer, onChange}: WorkspaceSelectorProps) { const {isOffline} = useNetwork(); const {translate} = useLocalize(); const theme = useTheme(); @@ -45,7 +43,7 @@ function WorkspaceSelector({policyIDQuery, value, selectionListTextInputStyle, s icons: workspace.icons, })); - const policyID = value ?? policyIDQuery ?? []; + const policyID = value ?? []; const updateSelectedItems = (item: ListItem) => { let newValue; diff --git a/src/components/Search/FilterComponents/index.tsx b/src/components/Search/FilterComponents/index.tsx index f3f54af44310..e4af48372e48 100644 --- a/src/components/Search/FilterComponents/index.tsx +++ b/src/components/Search/FilterComponents/index.tsx @@ -5,6 +5,7 @@ import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import {FILTER_VIEW_MAP, getMultiSelectFilterOptions, getSingleSelectFilterOptions} from '@libs/SearchUIUtils'; import type {SearchFilter} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; @@ -29,8 +30,7 @@ type FilterKeys = Exclude & { filterKey: FilterKeys; type?: SearchDataTypes; - policyIDs: string[] | undefined; - policyIDQuery: string[] | undefined; + policyID: PolicyIDFilter | undefined; }; type TextInputFilterComponentsProps = { @@ -103,8 +103,7 @@ function SingleSelectFilterComponents({filterKey, value, selectionListTextInputS function MultiSelectFilterComponents({filterKey, value = [], type = CONST.SEARCH.DATA_TYPES.EXPENSE, selectionListStyle, footer, onChange}: MultiSelectFilterComponentsProps) { const {translate} = useLocalize(); const items = getMultiSelectFilterOptions(filterKey, type, translate); - const normalizedValue = Array.isArray(value) ? value : value.split(','); - const multiSelectValues = items.filter((item) => normalizedValue.includes(item.value)); + const multiSelectValues = items.filter((item) => (value as string[]).includes(item.value)); return ( { - if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS) { - onChange(selectedItems.length > 0 ? selectedItems.map((item) => item.value) : CONST.SEARCH.STATUS.EXPENSE.ALL); - return; - } onChange(selectedItems.map((item) => item.value)); }} /> ); } -function FilterComponents({filterKey, value, type, policyIDs, policyIDQuery, selectionListTextInputStyle, selectionListStyle, autoFocus, ready, footer, onChange}: FilterComponentsProps) { +function FilterComponents({filterKey, value, type, policyID, selectionListTextInputStyle, selectionListStyle, autoFocus, ready, footer, onChange}: FilterComponentsProps) { switch (filterKey) { case CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED: case CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID: @@ -146,7 +141,7 @@ function FilterComponents({filterKey, value, type, policyIDs, policyIDQuery, sel return ( ) => void; }; -function CommonPopup({filterKey, value: initialValue, type, policyIDs, label, policyIDQuery, updateFilterForm, closeOverlay}: CommonPopupProps) { +function CommonPopup({filterKey, value: initialValue, type, policyID, label, updateFilterForm, closeOverlay}: CommonPopupProps) { const [value, setValue] = useState(initialValue); const applyChanges = () => { @@ -34,8 +33,7 @@ function CommonPopup({filterKey, value: initialValue, type, policyIDs, label, po filterKey={filterKey} value={value} type={type} - policyIDs={policyIDs} - policyIDQuery={policyIDQuery} + policyID={policyID} onChange={setValue} /> diff --git a/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/CommonFilterContentPopupWrapper.tsx b/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/CommonFilterContentPopupWrapper.tsx index 699239e715a8..72e5f720f154 100644 --- a/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/CommonFilterContentPopupWrapper.tsx +++ b/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/CommonFilterContentPopupWrapper.tsx @@ -3,15 +3,14 @@ import CommonFilterContent from '@components/Search/FilterComponents/AdvancedFil import type {CommonFilterContentWrapperProps} from '@components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent'; import useThemeStyles from '@hooks/useThemeStyles'; -function CommonFilterContentPopupWrapper({filterKey, value, type, policyIDs, policyIDQuery, onChange}: CommonFilterContentWrapperProps) { +function CommonFilterContentPopupWrapper({filterKey, value, type, policyID, onChange}: CommonFilterContentWrapperProps) { const styles = useThemeStyles(); return ( ; - queryJSON: SearchQueryJSON; closeOverlay: () => void; setPopoverWidth: PopoverComponentProps['setPopoverWidth']; updateFilterForm: (values: Partial) => void; @@ -50,7 +49,7 @@ function getFilterSentryLabel(filterKey: SearchAdvancedFiltersKey | SearchFilter return `Search-Filter-${filterKey}`; } -function FilterPopup({filterKey, searchAdvancedFiltersForm, queryJSON, closeOverlay, setPopoverWidth, updateFilterForm}: FilterPopupProps) { +function FilterPopup({filterKey, searchAdvancedFiltersForm, closeOverlay, setPopoverWidth, updateFilterForm}: FilterPopupProps) { const {translate} = useLocalize(); const label = translate(FILTER_VIEW_MAP[filterKey].labelKey); @@ -110,9 +109,8 @@ function FilterPopup({filterKey, searchAdvancedFiltersForm, queryJSON, closeOver filterKey={filterKey} value={searchAdvancedFiltersForm[filterKey]} type={searchAdvancedFiltersForm.type} - policyIDs={searchAdvancedFiltersForm.policyID} + policyID={getFilterNegatableValue(CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, searchAdvancedFiltersForm)} label={label} - policyIDQuery={queryJSON.policyID} closeOverlay={closeOverlay} updateFilterForm={closeModalAndUpdateFilterForm} /> @@ -128,7 +126,6 @@ function useSearchFiltersBar(queryJSON: SearchQueryJSON): UseSearchFiltersBarRes const {setFilterQueryParams, updateFilterQueryParams} = useUpdateFilterQuery(queryJSON); const filters = mapFiltersFormToLabelValueList( searchAdvancedFiltersForm, - queryJSON.policyID, SKIPPED_SEARCH_FILTERS, translate, localeCompare, @@ -139,7 +136,6 @@ function useSearchFiltersBar(queryJSON: SearchQueryJSON): UseSearchFiltersBarRes hasDeferredWrite(CONST.DEFERRED_LAYOUT_WRITE_KEYS.SEARCH) || Navigation.getIsFullscreenPreInsertedUnderRHP(), ); - const {type, status, sortBy, sortOrder, groupBy} = queryJSON; + const {type, sortBy, sortOrder, groupBy} = queryJSON; const validGroupBy = getValidGroupBy(groupBy); const searchData = searchResults?.data; @@ -104,7 +104,7 @@ function SearchStaticList({ convertToDisplayString, }); - return getSortedSections(type, status, filteredData, localeCompare, translate, sortBy, sortOrder, validGroupBy) + return getSortedSections(type, filteredData, localeCompare, translate, sortBy, sortOrder, validGroupBy) .filter((item): item is TransactionListItemType => 'transactionID' in item && item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) .slice(0, STATIC_LIST_MAX_ITEMS); })(); diff --git a/src/components/Search/hooks/useSearchSnapshot.ts b/src/components/Search/hooks/useSearchSnapshot.ts index 762399014705..6fd0b5b39c7f 100644 --- a/src/components/Search/hooks/useSearchSnapshot.ts +++ b/src/components/Search/hooks/useSearchSnapshot.ts @@ -90,7 +90,7 @@ const hashToString = (queryHash?: number) => (queryHash || queryHash === 0 ? Str * list-level meta and the optimistic-tracking carriers that `` consumes. */ function useSearchSnapshot({queryJSON, searchResults, newSearchResultKeys, transactions, reportActions}: UseSearchSnapshotParams): SearchSnapshotResult { - const {type, status, sortBy, sortOrder, hash, groupBy} = queryJSON; + const {type, sortBy, sortOrder, hash, groupBy} = queryJSON; const {isOffline} = useNetwork(); const {translate, localeCompare, formatPhoneNumber} = useLocalize(); @@ -298,8 +298,8 @@ function useSearchSnapshot({queryJSON, searchResults, newSearchResultKeys, trans if (!shouldComputeSections) { return EMPTY_DATA; } - const sortInput = filteredData as Parameters[2]; - return getSortedSections(type, status, sortInput, localeCompare, translate, sortBy, sortOrder, validGroupBy, { + const sortInput = filteredData as Parameters[1]; + return getSortedSections(type, sortInput, localeCompare, translate, sortBy, sortOrder, validGroupBy, { policyCategories, policyTags, fallbackPolicyID: policyForMovingExpensesID, diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index a010ea25c9dc..5049198ba4f9 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -125,7 +125,7 @@ function Search({ onContentReady, onDestinationVisible, }: SearchProps) { - const {type, status, sortBy, sortOrder, hash, similarSearchHash, groupBy, view} = queryJSON; + const {type, sortBy, sortOrder, hash, similarSearchHash, groupBy, view} = queryJSON; const {isOffline} = useNetwork(); const prevIsOffline = usePrevious(isOffline); @@ -690,7 +690,7 @@ function Search({ const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const isTask = type === CONST.SEARCH.DATA_TYPES.TASK; const canSelectMultiple = !isChat && !isTask && (!isSmallScreenWidth || isMobileSelectionModeEnabled); - const ListItem = getListItem(type, status, validGroupBy); + const ListItem = getListItem(type, validGroupBy); useSaveSortedReportIDs(type, sortedData); diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index abd2d3fcca4b..482824411e01 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -302,7 +302,6 @@ type SearchAmountValues = Record, type SearchFilterKey = | SyntaxFilterKey | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE - | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.COLUMNS @@ -330,13 +329,11 @@ type SearchQueryString = string; type SearchQueryAST = { type: SearchDataTypes; - status: SearchStatus; sortBy: SearchSortBy; sortOrder: SortOrder; groupBy?: SearchGroupBy; view: SearchView; filters: ASTNode; - policyID?: string[]; rawFilterList?: RawQueryFilter[]; columns?: SearchCustomColumnIds | SearchCustomColumnIds[]; limit?: number; diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts index 64c7356381ed..d6df0d9092b1 100644 --- a/src/hooks/useAdvancedSearchFilters.ts +++ b/src/hooks/useAdvancedSearchFilters.ts @@ -5,6 +5,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {isFilterableBankAccount} from '@libs/BankAccountUtils'; import {isPolicyFeatureEnabled} from '@libs/PolicyUtils'; import {getAllPolicyValues} from '@libs/SearchQueryUtils'; +import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CardList, Policy, PolicyCategories, PolicyTagLists} from '@src/types/onyx'; @@ -313,7 +314,7 @@ function shouldDisplayCardFilterSelector(cardList: OnyxEntry) { return shouldDisplayFilter(Object.keys(filterCardsHiddenFromSearch(cardList)).length, true); } -function useAdvancedSearchFilters(type: SearchDataTypes | undefined, policyID: string[] | undefined) { +function useAdvancedSearchFilters(type: SearchDataTypes | undefined, policyID: PolicyIDFilter) { const [shouldDisplayCardFilter] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {selector: shouldDisplayCardFilterSelector}); const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: advancedSearchPoliciesSelector}); const [policyDerived] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policyDerivedSelector}); diff --git a/src/hooks/useAutocompleteSuggestions.ts b/src/hooks/useAutocompleteSuggestions.ts index 997c112161a3..5e438895c185 100644 --- a/src/hooks/useAutocompleteSuggestions.ts +++ b/src/hooks/useAutocompleteSuggestions.ts @@ -331,7 +331,7 @@ function useAutocompleteSuggestions({ ); return filteredViews.map((viewValue) => ({filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.VIEW, text: viewValue})); } - case CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS: { + case CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS: { const statusAutocompleteList = (() => { let suggestedStatuses; switch (currentType) { diff --git a/src/hooks/useExportedToFilterOptions.ts b/src/hooks/useExportedToFilterOptions.ts index 3e3ed1eb6ecc..b35ed8ccabab 100644 --- a/src/hooks/useExportedToFilterOptions.ts +++ b/src/hooks/useExportedToFilterOptions.ts @@ -3,7 +3,7 @@ import {useSearchQueryContext} from '@components/Search/SearchContext'; import {getStandardExportTemplateDisplayName} from '@libs/AccountingUtils'; import {getExportTemplates} from '@libs/actions/Search'; import {getConnectedIntegrationNamesForPolicies} from '@libs/PolicyUtils'; -import {getAllPolicyValues} from '@libs/SearchQueryUtils'; +import {getAllPolicyValues, getPolicyIDFromQuery} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ExportTemplate, Policy} from '@src/types/onyx'; @@ -41,7 +41,7 @@ function exportedToPoliciesSelector(policies: OnyxCollection): OnyxColle */ export default function useExportedToFilterOptions(): UseExportedToFilterDataResult { const {currentSearchQueryJSON} = useSearchQueryContext(); - const policyIDs = currentSearchQueryJSON?.policyID; + const policyIDs = getPolicyIDFromQuery(currentSearchQueryJSON); const {translate} = useLocalize(); const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES); @@ -49,7 +49,7 @@ export default function useExportedToFilterOptions(): UseExportedToFilterDataRes const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: exportedToPoliciesSelector}); // When search is scoped to workspaces, use only those policies otherwise use all. - const policiesToUse = policyIDs !== undefined ? getAllPolicyValues(policyIDs, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies ?? {}); + const policiesToUse = policyIDs.value !== undefined ? getAllPolicyValues(policyIDs, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies ?? {}); const policyLevelExportTemplates = policiesToUse.flatMap((policy) => getExportTemplates([], {}, translate, policy, false)); const accountLevelExportTemplates = getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, undefined, true); const combinedExportTemplates = [...accountLevelExportTemplates, ...policyLevelExportTemplates]; @@ -76,7 +76,7 @@ export default function useExportedToFilterOptions(): UseExportedToFilterDataRes standardAndCustomExportTemplates.push(filterValue); } - const connectedIntegrationNames = policyIDs?.length === 0 ? new Set() : getConnectedIntegrationNamesForPolicies(policies, policyIDs); + const connectedIntegrationNames = policyIDs.value?.length === 0 ? new Set() : getConnectedIntegrationNamesForPolicies(policies, policyIDs); const displayNameToConnectionName = new Map( Object.entries(CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY).map(([connectionName, displayName]) => [displayName, connectionName]), diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 76bf41d25d6d..517077e39744 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -546,7 +546,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { onlyShowPayElsewhere, }); - const {status, hash} = queryJSON ?? {}; + const {hash} = queryJSON ?? {}; const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {}); const firstTransactionID = selectedTransactionsKeys.at(0); const firstTransaction = @@ -709,20 +709,15 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { return; } - if (status === null || status === undefined) { - return; - } - const exportName = translate(isBasicExport ? 'export.basicExport' : 'export.currentView'); if (areAllMatchingItemsSelected) { - if (selectedTransactionsKeys.length === 0 || status == null || !hash) { + if (selectedTransactionsKeys.length === 0 || !hash) { return; } const reportIDList = selectedReports?.map((report) => report?.reportID).filter((reportID) => reportID !== undefined) ?? []; const exportParameters = getCSVExportParameters(isBasicExport, queryJSON); const exportID = queueExportSearchItemsToCSV({ - query: status, jsonQuery: exportParameters.jsonQuery, reportIDList, transactionIDList: selectedTransactionsKeys, @@ -741,7 +736,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const exportParameters = getCSVExportParameters(isBasicExport, queryJSONToExport); await exportSearchItemsToCSV( { - query: status, jsonQuery: exportParameters.jsonQuery, reportIDList: isGroupExport ? [] : reportIDList, transactionIDList: isGroupExport ? [] : selectedTransactionsKeys, @@ -762,7 +756,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { }, [ isOffline, - status, areAllMatchingItemsSelected, queryJSON, selectedReports, @@ -1436,7 +1429,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { }, [selectedReports, currentSearchResults?.data, isTrackIntentUser, policies, selectedTransactions]); const headerButtonsOptions = useMemo(() => { - if (selectedTransactionsKeys.length === 0 || status == null || !hash) { + if (selectedTransactionsKeys.length === 0 || !hash) { return CONST.EMPTY_ARRAY as unknown as Array>; } diff --git a/src/hooks/useSearchSections.ts b/src/hooks/useSearchSections.ts index 1e96a4358705..db5b79ebfad3 100644 --- a/src/hooks/useSearchSections.ts +++ b/src/hooks/useSearchSections.ts @@ -33,7 +33,7 @@ function useSearchSections(): UseSearchSectionsResult { const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const reportAttributesDerivedValue = useReportAttributes(); - const {type, status, sortBy, sortOrder, groupBy} = lastSearchQuery?.queryJSON ?? {}; + const {type, sortBy, sortOrder, groupBy} = lastSearchQuery?.queryJSON ?? {}; const searchResultsData = currentSearchResults?.data; const searchResultsSearch = currentSearchResults?.search; const currentAccountID = currentUserDetails.accountID; @@ -61,7 +61,7 @@ function useSearchSections(): UseSearchSectionsResult { convertToDisplayString, reportAttributesDerivedValue, }); - results = getSortedSections(type, status ?? '', searchData, localeCompare, translate, sortBy, sortOrder, groupBy).map((value) => value.reportID); + results = getSortedSections(type, searchData, localeCompare, translate, sortBy, sortOrder, groupBy).map((value) => value.reportID); } return {allReports: useFilterPendingDeleteReports(results), isSearchLoading: !!currentSearchResults?.search?.isLoading, lastSearchQuery}; diff --git a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts index b7309bf30f30..38c627fbe0af 100644 --- a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts +++ b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts @@ -1,7 +1,6 @@ import type {SearchQueryString, SearchStatus} from '@components/Search/types'; type ExportSearchItemsToCSVParams = { - query: SearchStatus; jsonQuery: SearchQueryString; reportIDList: string[]; transactionIDList: string[]; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index c71b69d5d19a..e372e31e03ac 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -53,6 +53,8 @@ import {getIsOffline} from './NetworkState'; import {formatMemberForList} from './OptionsListUtils'; import type {MemberForList} from './OptionsListUtils'; import {getAccountIDsByLogins, getKnownAccountIDByLogin, getLoginsByAccountIDs, getPersonalDetailByEmail} from './PersonalDetailsUtils'; +import {getAllPolicyValues} from './SearchQueryUtils'; +import type {PolicyIDFilter} from './SearchQueryUtils'; import {getAllSortedTransactions, getCategory, getTag, getTagArrayFromName} from './TransactionUtils'; import {generateAccountID} from './UserUtils'; import {isPublicDomain, isValidAccountRoute} from './ValidationUtils'; @@ -2333,14 +2335,14 @@ function getValidConnectedIntegration(policy: Policy | undefined, connectionName * @param policies - Collection of policies to get connected integrations. * @param policyIDs - Policy IDs to filter by. When provided, only integrations from these policies are included. */ -function getConnectedIntegrationNamesForPolicies(policies: OnyxCollection | undefined, policyIDs?: string[]): Set { +function getConnectedIntegrationNamesForPolicies(policies: OnyxCollection | undefined, policyID: PolicyIDFilter): Set { if (!policies) { return new Set(); } const connectedIntegrationNames = new Set(); - const hasWorkspaceFilter = policyIDs && policyIDs.length > 0; - const policiesToCheck = hasWorkspaceFilter ? policyIDs.map((id) => policies[`${ONYXKEYS.COLLECTION.POLICY}${id}`]) : Object.values(policies); + const hasWorkspaceFilter = !!policyID.value?.length; + const policiesToCheck = hasWorkspaceFilter ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies); for (const policy of policiesToCheck) { const connectedIntegration = getValidConnectedIntegration(policy, getAccountingConnectionNames()); diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index 584d5ecf324e..45f47a94de43 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -206,7 +206,7 @@ function filterOutRangesWithCorrectValue( return withdrawalTypeList.includes(range.value); case CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_STATUS: return withdrawalStatusList.includes(range.value); - case CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS: + case CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS: return statusList.includes(range.value); case CONST.SEARCH.SYNTAX_FILTER_KEYS.ACTION: return actionList.includes(range.value); diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index 68e284cb64e2..db63fd6665c6 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -1200,7 +1200,13 @@ function peg$parse(input, options) { if (s1 === peg$FAILED) { s1 = peg$parsepurchaseAmount(); if (s1 === peg$FAILED) { - s1 = peg$parsereportFieldDynamic(); + s1 = peg$parsepolicyID(); + if (s1 === peg$FAILED) { + s1 = peg$parsestatus(); + if (s1 === peg$FAILED) { + s1 = peg$parsereportFieldDynamic(); + } + } } } } @@ -1280,23 +1286,17 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parsetype(); if (s1 === peg$FAILED) { - s1 = peg$parsestatus(); + s1 = peg$parsesortBy(); if (s1 === peg$FAILED) { - s1 = peg$parsesortBy(); + s1 = peg$parsesortOrder(); if (s1 === peg$FAILED) { - s1 = peg$parsesortOrder(); + s1 = peg$parsegroupBy(); if (s1 === peg$FAILED) { - s1 = peg$parsepolicyID(); + s1 = peg$parsecolumns(); if (s1 === peg$FAILED) { - s1 = peg$parsegroupBy(); + s1 = peg$parselimit(); if (s1 === peg$FAILED) { - s1 = peg$parsecolumns(); - if (s1 === peg$FAILED) { - s1 = peg$parselimit(); - if (s1 === peg$FAILED) { - s1 = peg$parseview(); - } - } + s1 = peg$parseview(); } } } @@ -4735,7 +4735,6 @@ function peg$parse(input, options) { const defaultValues = { type: "expense", - status: "", sortBy: "date", sortOrder: "desc", view: "table", @@ -4788,11 +4787,6 @@ function peg$parse(input, options) { } function updateDefaultValues(field, value) { - if (field === "status" && value === "all") { - defaultValues[field] = ""; - return; - } - // Track if user explicitly provided a custom (non-default) sortBy if (field === "sortBy" && !isDefaultSortValue(value)) { userProvidedSortBy = true; diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index 6b3df3a7a508..d77e395bea5c 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -10,7 +10,7 @@ // freeTextFilter: rule to process the free text search values returned by the identifier rule. It builds filter Object. // standardFilter: rule to process the values returned by the key rule. It builds filter Object. // key: rule to match pre-defined search syntax fields, e.g. amount, merchant, etc -// defaultKey: rule to match pre-defined search syntax fields that are used to update default values, e.g. type, status, etc +// defaultKey: rule to match pre-defined search syntax fields that are used to update default values, e.g. type, sortBy, etc // identifier: composite rule to match patterns defined by the quotedString and alphanumeric rules // filter, logicalAnd, operator, alphanumeric, quotedStrig are defined in baseRules.peggy grammar @@ -60,7 +60,6 @@ const defaultValues = { type: "expense", - status: "", sortBy: "date", sortOrder: "desc", view: "table", @@ -113,11 +112,6 @@ } function updateDefaultValues(field, value) { - if (field === "status" && value === "all") { - defaultValues[field] = ""; - return; - } - // Track if user explicitly provided a custom (non-default) sortBy if (field === "sortBy" && !isDefaultSortValue(value)) { userProvidedSortBy = true; @@ -304,6 +298,8 @@ key "key" / is / purchaseCurrency / purchaseAmount + / policyID + / status / reportFieldDynamic ) @@ -313,7 +309,7 @@ filterKey return k; } -defaultKey "default key" = @(type / status / sortBy / sortOrder / policyID / groupBy / columns / limit / view) +defaultKey "default key" = @(type / sortBy / sortOrder / groupBy / columns / limit / view) identifier = (","+)? parts:(values / quotedString / alphanumeric)|1.., ","+| empty:(","+)? { diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index eaf59716c165..38484f7ffbae 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -21,6 +21,7 @@ import type { SearchQueryString, SearchStatus, SearchWithdrawalType, + SingularSearchStatus, SyntaxFilterKey, UserFriendlyKey, UserFriendlyValue, @@ -32,7 +33,7 @@ import type {OnyxCollectionKey, OnyxCollectionValuesMapping} from '@src/ONYXKEYS import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; -import FILTER_KEYS, {ALLOWED_TYPE_FILTERS, AMOUNT_FILTER_KEYS, DATE_FILTER_KEYS} from '@src/types/form/SearchAdvancedFiltersForm'; +import FILTER_KEYS, {ALLOWED_TYPE_FILTERS, AMOUNT_FILTER_KEYS, DATE_FILTER_KEYS, NEGATABLE_FILTERS} from '@src/types/form/SearchAdvancedFiltersForm'; import type { ExpenseTypeValue, ExpenseTypeValues, @@ -42,6 +43,7 @@ import type { IsFilterValues, ReceiptTypeValue, SearchAdvancedFiltersKey, + SearchNegatableFilterKeys, } from '@src/types/form/SearchAdvancedFiltersForm'; import type * as OnyxTypes from '@src/types/onyx'; import type {SearchDataTypes, SearchResultDataType} from '@src/types/onyx/SearchResults'; @@ -60,6 +62,7 @@ import {getDisplayNameOrDefault, getPersonalDetailByEmail} from './PersonalDetai import {getCleanedTagName} from './PolicyUtils'; import {getReportName} from './ReportNameUtils'; import {parse as parseSearchQuery} from './SearchParser/searchParser'; +import {getStatusOptions} from './SearchUIUtils'; import StringUtils from './StringUtils'; import {hashText} from './UserUtils'; import {isValidDate} from './ValidationUtils'; @@ -425,6 +428,41 @@ function getFilters(queryJSON: SearchQueryJSON) { return filters; } +function getStatusFromQuery(queryJSON: SearchQueryJSON | undefined): SearchStatus { + const statusFilters = queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS)?.filters; + if (!statusFilters) { + return CONST.SEARCH.STATUS.EXPENSE.ALL; + } + + const isNegated = statusFilters.at(0)?.operator === CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO; + const values = statusFilters.map((filter) => filter.value) as SearchStatus; + + if (!isNegated) { + return values; + } + + const statusOptions = getStatusOptions(() => '', queryJSON.type); + return statusOptions.reduce((acc, curr) => { + if (!values.includes(curr.value)) { + acc.push(curr.value); + } + return acc; + }, [] as SingularSearchStatus[]); +} + +type PolicyIDFilter = { + value: string[] | undefined; + isNegated: boolean; +}; + +function getPolicyIDFromQuery(queryJSON: SearchQueryJSON | undefined): PolicyIDFilter { + const policyIDFilters = queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID)?.filters; + const isNegated = policyIDFilters?.at(0)?.operator === CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO; + const value = policyIDFilters?.map((filter) => filter.value.toString()); + + return {value, isNegated}; +} + /** * @private * Returns an updated filter value for some query filters. @@ -524,13 +562,8 @@ function wasViewExplicitlySet(queryJSON?: SearchQueryJSON | Readonly(orderedQuery); // Certain filters shouldn't affect whether two searchers are similar or not, since they dont @@ -651,11 +684,6 @@ function getCachedSearchQueryJSON(query: SearchQueryString, rawQuery?: SearchQue result.flatFilters = flatFilters; result.isViewExplicitlySet = rawFilterList?.some((filter) => filter.key === CONST.SEARCH.SYNTAX_ROOT_KEYS.VIEW) ?? false; - if (result.policyID && typeof result.policyID === 'string') { - // Ensure policyID is always an array for consistency - result.policyID = [result.policyID]; - } - // Normalize limit before computing hashes to ensure invalid values don't affect hash if (result.limit !== undefined) { const num = Number(result.limit); @@ -720,10 +748,6 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON | Readonly(status)]; - filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS}:${filterValueArray.map(sanitizeSearchValue).join(',')}`); - } - if (columns?.length) { const filterValueArray = [...new Set(columns)]; filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.COLUMNS}:${filterValueArray.map(sanitizeSearchValue).join(',')}`); @@ -989,6 +1003,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial 0 @@ -1032,7 +1047,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial( - policyID: string[] | undefined, + {value: policyID, isNegated}: PolicyIDFilter, key: T, policyData: OnyxCollection, ): Array { @@ -1040,9 +1055,55 @@ function getAllPolicyValues( return []; } + if (isNegated) { + return Object.keys(policyData).reduce( + (acc, curr) => { + const id = curr.replace(key, ''); + if (!policyID.includes(id) && policyData[curr]) { + acc.push(policyData[curr]); + } + return acc; + }, + [] as Array, + ); + } + return policyID.map((id) => policyData?.[`${key}${id}`]).filter((data) => !!data) as Array; } +function getAllPolicyValuesMap( + {value: policyID, isNegated}: PolicyIDFilter, + key: T, + policyData: OnyxCollection, +): OnyxCollection { + if (!policyData || !policyID) { + return {}; + } + + if (isNegated) { + return Object.keys(policyData).reduce( + (acc, curr) => { + const id = curr.replace(key, ''); + if (!policyID.includes(id) && policyData[curr]) { + acc[curr] = policyData[curr]; + } + return acc; + }, + {} as Exclude, undefined>, + ); + } + + return policyID.reduce( + (acc, curr) => { + if (policyData?.[`${key}${curr}`]) { + acc[`${key}${curr}`] = policyData?.[`${key}${curr}`]; + } + return acc; + }, + {} as Exclude, undefined>, + ); +} + function getEarlierDate(someDate: string | undefined, otherDate: string | undefined) { if (someDate && otherDate) { return someDate < otherDate ? someDate : otherDate; @@ -1116,7 +1177,7 @@ function buildFilterFormValuesFromQuery( ) { const filters = queryJSON.flatFilters; const filtersForm = {} as Partial; - const policyID = queryJSON.policyID; + const policyID = getPolicyIDFromQuery(queryJSON); // Pre-compute dynamic validation Sets once (avoids recreating per filter iteration) const validCurrencies = new Set(Object.keys(currencyList)); @@ -1131,6 +1192,12 @@ function buildFilterFormValuesFromQuery( const isNegated = filterList.some((item) => item.operator === CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO); const key = isNegated ? (`${filterKey}${CONST.SEARCH.NOT_MODIFIER}` as const) : filterKey; + if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID) { + filtersForm[key as typeof filterKey] = filterValues; + } + if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS) { + filtersForm[key as typeof filterKey] = filterValues; + } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_ID || filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_ID) { filtersForm[key as typeof filterKey] = filterValues.join(','); } @@ -1237,7 +1304,7 @@ function buildFilterFormValuesFromQuery( } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG) { const uniqueTags = new Set(); - const tagLists = policyID ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_TAGS, policyTags) : Object.values(policyTags ?? {}).filter((item) => !!item); + const tagLists = policyID.value ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_TAGS, policyTags) : Object.values(policyTags ?? {}).filter((item) => !!item); for (const tagList of tagLists) { for (const policyTagList of Object.values(tagList ?? {})) { for (const tag of Object.values(policyTagList.tags ?? {})) { @@ -1250,7 +1317,7 @@ function buildFilterFormValuesFromQuery( } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY) { const uniqueCategories = new Set(); - const categoryLists = policyID ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_CATEGORIES, policyCategories) : Object.values(policyCategories ?? {}); + const categoryLists = policyID.value ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_CATEGORIES, policyCategories) : Object.values(policyCategories ?? {}); for (const item of categoryLists) { for (const category of Object.values(item ?? {})) { uniqueCategories.add(category.name); @@ -1422,27 +1489,9 @@ function buildFilterFormValuesFromQuery( filtersForm[afterKey] = undefined; } - const [typeKey, typeValue] = Object.entries(CONST.SEARCH.DATA_TYPES).find(([, value]) => value === queryJSON.type) ?? []; + const typeValue = Object.values(CONST.SEARCH.DATA_TYPES).find((value) => value === queryJSON.type); filtersForm[FILTER_KEYS.TYPE] = typeValue ? queryJSON.type : CONST.SEARCH.DATA_TYPES.EXPENSE; - if (typeKey) { - if (Array.isArray(queryJSON.status)) { - const validStatuses = queryJSON.status.filter((status) => Object.values(CONST.SEARCH.STATUS[typeKey as keyof typeof CONST.SEARCH.DATA_TYPES]).includes(status)); - - if (validStatuses.length) { - filtersForm[FILTER_KEYS.STATUS] = queryJSON.status.join(','); - } else { - filtersForm[FILTER_KEYS.STATUS] = CONST.SEARCH.STATUS.EXPENSE.ALL; - } - } else { - filtersForm[FILTER_KEYS.STATUS] = queryJSON.status; - } - } - - if (queryJSON.policyID) { - filtersForm[FILTER_KEYS.POLICY_ID] = queryJSON.policyID; - } - if (queryJSON.groupBy) { filtersForm[FILTER_KEYS.GROUP_BY] = queryJSON.groupBy; @@ -1711,8 +1760,8 @@ function formatDefaultRawFilterSegment(rawFilter: RawQueryFilter, policies: Onyx case CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE: userFriendlyKey = getUserFriendlyKey(CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE); break; - case CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS: - userFriendlyKey = getUserFriendlyKey(CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS); + case CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS: + userFriendlyKey = getUserFriendlyKey(CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS); break; case CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY: userFriendlyKey = getUserFriendlyKey(CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY); @@ -1774,7 +1823,7 @@ function buildUserReadableQueryString({ reportAttributes, bankAccountList, }: BuildUserReadableQueryStringParams) { - const {type, status, groupBy, view, columns, policyID, rawFilterList, flatFilters: filters = [], limit} = queryJSON; + const {type, groupBy, view, columns, rawFilterList, flatFilters: filters = [], limit} = queryJSON; if (rawFilterList && rawFilterList.length > 0) { const segments: string[] = []; @@ -1837,9 +1886,7 @@ function buildUserReadableQueryString({ } } - let title = status - ? `type:${getUserFriendlyValue(type)} status:${Array.isArray(status) ? status.map(getUserFriendlyValue).join(',') : getUserFriendlyValue(status)}` - : `type:${getUserFriendlyValue(type)}`; + let title = `type:${getUserFriendlyValue(type)}`; if (groupBy) { title += ` group-by:${getUserFriendlyValue(groupBy)}`; @@ -1849,10 +1896,6 @@ function buildUserReadableQueryString({ title += ` view:${getUserFriendlyValue(view)}`; } - if (policyID && policyID.length > 0) { - title += ` workspace:${policyID.map((id) => sanitizeSearchValue(getPolicyNameWithFallback(id, policies, reports))).join(',')}`; - } - if (columns && columns.length > 0) { const columnValue = Array.isArray(columns) ? columns.map((column) => getUserFriendlyValue(column)).join(',') : getUserFriendlyValue(columns); title += ` columns:${columnValue}`; @@ -1930,11 +1973,11 @@ function buildCannedSearchQuery({ } function isDefaultExpensesQuery(queryJSON: SearchQueryJSON | Readonly) { - return queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE && !queryJSON.status && !queryJSON.filters && !queryJSON.groupBy && !queryJSON.policyID; + return queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE && !queryJSON.filters && !queryJSON.groupBy; } function isDefaultExpenseReportsQuery(queryJSON: SearchQueryJSON | Readonly) { - return queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && !queryJSON.status && !queryJSON.filters && !queryJSON.groupBy && !queryJSON.policyID; + return queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && !queryJSON.filters && !queryJSON.groupBy; } /** @@ -2299,11 +2342,7 @@ function getEmptyDateValues(): SearchDateValues { function getAdvancedFiltersToReset(searchAdvancedFiltersForm: Partial) { const isTypeExpense = searchAdvancedFiltersForm.type === CONST.SEARCH.DATA_TYPES.EXPENSE; return Object.keys(searchAdvancedFiltersForm).reduce((acc, filterKey) => { - if (filterKey === FILTER_KEYS.STATUS) { - if (searchAdvancedFiltersForm[filterKey] !== CONST.SEARCH.STATUS.EXPENSE.ALL) { - acc[filterKey] = CONST.SEARCH.STATUS.EXPENSE.ALL; - } - } else if (filterKey === FILTER_KEYS.TYPE) { + if (filterKey === FILTER_KEYS.TYPE) { if (!isTypeExpense) { acc[filterKey] = CONST.SEARCH.DATA_TYPES.EXPENSE; } @@ -2369,7 +2408,15 @@ function serializeQueryJSONForBackend { return isValidExpenseStatus(expenseStatus) ? expenseStatusActionMapping[expenseStatus](report, transactionItem.reportID) : false; @@ -2819,7 +2821,7 @@ function getReportSections({ const isActionLoading = isActionLoadingSet?.has(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportItem.reportID}`); if (currentQueryJSON && !isActionLoading) { if (currentQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) { - const status = currentQueryJSON.status; + const status = getStatusFromQuery(currentQueryJSON); if (Array.isArray(status)) { shouldShow = status.some((expenseStatus) => { @@ -3584,7 +3586,7 @@ function getQuarterSections(data: OnyxTypes.SearchResults['data'], queryJSON: Se /** * Returns the appropriate list item component based on the type and status of the search data. */ -function getListItem(type: SearchDataTypes, status: SearchStatus, groupBy?: SearchGroupBy): ListItemType { +function getListItem(type: SearchDataTypes, groupBy?: SearchGroupBy): ListItemType { if (type === CONST.SEARCH.DATA_TYPES.CHAT) { return ChatListItem; } @@ -3754,8 +3756,7 @@ const groupByRequiredColumns: Partial> */ function getSortedSections( type: SearchDataTypes, - status: SearchStatus, - data: ListItemDataType, + data: ListItemDataType, localeCompare: LocaleContextProps['localeCompare'], translate: LocaleContextProps['translate'], sortBy?: SearchSortBy, @@ -4763,14 +4764,7 @@ function shouldShowEmptyState(isDataLoaded: boolean, dataLength: number, type: S } function isSearchDataLoaded(searchResults: SearchResults | undefined, queryJSON: Readonly | undefined) { - const {status} = queryJSON ?? {}; - - const sortedSearchResultStatus = !Array.isArray(searchResults?.search?.status) - ? searchResults?.search?.status?.split(',').sort().join(',') - : searchResults?.search?.status?.sort().join(','); - const sortedQueryJSONStatus = Array.isArray(status) ? status.sort().join(',') : status; - const isDataLoaded = - (searchResults?.data != null || searchResults?.errors != null) && searchResults?.search?.type === queryJSON?.type && sortedSearchResultStatus === sortedQueryJSONStatus; + const isDataLoaded = (searchResults?.data != null || searchResults?.errors != null) && searchResults?.search?.type === queryJSON?.type && searchResults.search.hash === queryJSON?.hash; return isDataLoaded; } @@ -5352,7 +5346,6 @@ function getDisplayValue( key: SearchAdvancedFiltersKey, form: Partial, type: SearchDataTypes, - policyIDQuery: string[] | undefined, translate: LocalizedTranslate, localeCompare: LocaleContextProps['localeCompare'], ) { @@ -5418,20 +5411,13 @@ function getDisplayValue( key === FILTER_KEYS.ASSIGNEE || key === FILTER_KEYS.TAX_RATE || key === FILTER_KEYS.IN || - key === FILTER_KEYS.BANK_ACCOUNT + key === FILTER_KEYS.BANK_ACCOUNT || + key === FILTER_KEYS.POLICY_ID || + key === FILTER_KEYS.POLICY_ID_NOT ) { return form[key]; } - if (key === FILTER_KEYS.POLICY_ID) { - const policyID = form[key]; - const policyIDs = policyID ?? policyIDQuery; - if (policyIDs) { - return Array.isArray(policyIDs) ? policyIDs : [policyIDs]; - } - return undefined; - } - if (key === FILTER_KEYS.EXPENSE_TYPE) { return form[key]?.map((expenseType) => translate(getExpenseTypeTranslationKey(expenseType))).join(', '); } @@ -5444,6 +5430,22 @@ function getDisplayValue( return Array.isArray(formValue) ? formValue.join(', ') : formValue; } +function getFilterNegatableValue( + filterKey: K, + values: Partial | undefined, +): { + isNegated: boolean; + value: SearchAdvancedFiltersForm[K] | undefined; +} { + if (!isFilterNegatable(filterKey)) { + return {isNegated: false, value: values?.[filterKey]}; + } + const negatedFilterKey = `${filterKey}${CONST.SEARCH.NOT_MODIFIER}` as K; + const negatedValue = values?.[negatedFilterKey]; + const value = negatedValue ?? values?.[filterKey]; + return {isNegated: !!negatedValue, value}; +} + function shouldShowFilter(skipFilters: Set | undefined, key: SearchAdvancedFiltersKey, value: ValueOf, type: SearchDataTypes) { return !skipFilters?.has(key) && isFilterSupported(key, type) && value && (!Array.isArray(value) || value.length > 0); } @@ -5464,7 +5466,6 @@ type SearchFilter = { function mapFiltersFormToLabelValueList>( searchAdvancedFiltersForm: Partial, - policyIDQuery: string[] | undefined, skipFilters: Set | undefined, translate: LocalizedTranslate, localeCompare: LocaleContextProps['localeCompare'], @@ -5518,7 +5519,7 @@ function mapFiltersFormToLabelValueList>( // Handle regular filters const label = key in FILTER_VIEW_MAP ? FILTER_VIEW_MAP[key as keyof typeof FILTER_VIEW_MAP].labelKey : undefined; - const value = getDisplayValue(key, searchAdvancedFiltersForm, type, policyIDQuery, translate, localeCompare); + const value = getDisplayValue(key, searchAdvancedFiltersForm, type, translate, localeCompare); if (label && value && !(Array.isArray(value) && value.length === 0)) { const filterLabelMapKey = key as keyof typeof FILTER_VIEW_MAP; @@ -6404,6 +6405,7 @@ export { getTypeOptions, getSortByOptions, getSortOrderOptions, + getStatusOptions, getGroupBySections, getViewOptions, getCurrencyOptions, @@ -6434,6 +6436,7 @@ export { getSelectedGroupFilterEntry, adjustTimeRangeToDateFilters, getDateDisplayValue, + getFilterNegatableValue, shouldShowFilter, mapFiltersFormToLabelValueList, isAmountFilterKey, diff --git a/src/libs/actions/IOU/SearchUpdate.ts b/src/libs/actions/IOU/SearchUpdate.ts index cf9d238ba918..4416aaebf9f6 100644 --- a/src/libs/actions/IOU/SearchUpdate.ts +++ b/src/libs/actions/IOU/SearchUpdate.ts @@ -3,7 +3,7 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {SearchQueryJSON} from '@components/Search/types'; import {isExpenseReport, isOptimisticPersonalDetail} from '@libs/ReportUtils'; -import {buildSearchQueryJSON, buildSearchQueryString, getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils'; +import {buildSearchQueryJSON, buildSearchQueryString, getCurrentSearchQueryJSON, getPolicyIDFromQuery, getStatusFromQuery} from '@libs/SearchQueryUtils'; import {getSuggestedSearches} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -55,7 +55,7 @@ function shouldOptimisticallyUpdateSearch( return false; } let shouldOptimisticallyUpdateByStatus; - const status = currentSearchQueryJSON.status; + const status = getStatusFromQuery(currentSearchQueryJSON); const transactionReportID = transaction?.reportID; if (Array.isArray(status)) { shouldOptimisticallyUpdateByStatus = status.some((val) => { @@ -67,8 +67,13 @@ function shouldOptimisticallyUpdateSearch( shouldOptimisticallyUpdateByStatus = expenseReportStatusFilterMapping[expenseStatus](iouReport, transactionReportID); } - if (currentSearchQueryJSON.policyID?.length && iouReport?.policyID) { - if (!currentSearchQueryJSON.policyID.includes(iouReport.policyID)) { + const currentSearchPolicyIDs = getPolicyIDFromQuery(currentSearchQueryJSON); + if (currentSearchPolicyIDs.value?.length && iouReport?.policyID) { + if (!currentSearchPolicyIDs.isNegated && !currentSearchPolicyIDs.value.includes(iouReport.policyID)) { + return false; + } + + if (currentSearchPolicyIDs.isNegated && currentSearchPolicyIDs.value.includes(iouReport.policyID)) { return false; } } @@ -88,7 +93,11 @@ function shouldOptimisticallyUpdateSearch( (isInvoice && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.INVOICE) || (iouReport?.type === CONST.REPORT.TYPE.EXPENSE && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT); - const hasNoFlatFilters = currentSearchQueryJSON.flatFilters.length === 0; + // `status` and `policyID` are regular filters now, but they used to be root keys. They are already accounted for by the + // status/policyID checks above, so they don't count as restrictive flat filters when deciding whether to optimistically update. + const hasNoFlatFilters = currentSearchQueryJSON.flatFilters.every( + (filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS || filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, + ); const matchesSubmitQuery = submitQueryJSON?.similarSearchHash === currentSearchQueryJSON.similarSearchHash && expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.DRAFTS](iouReport); @@ -192,7 +201,6 @@ function getSearchOnyxUpdate({ value: { search: { type: currentSearchQueryJSON.type, - status: currentSearchQueryJSON.status, hasResults: true, isLoading: false, }, @@ -223,7 +231,6 @@ function getSearchOnyxUpdate({ value: { search: { type: groupTransactionsQueryJSON.type, - status: groupTransactionsQueryJSON.status, offset: 0, hasMoreResults: false, hasResults: true, diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index c459280fba84..1836b312b0dd 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -691,7 +691,6 @@ function getOnyxLoadingData( value: { ...(isOffline ? {} : {data: null}), search: { - status: queryJSON?.status, type: queryJSON?.type, ...(isSearchAPI && {isLoading: false}), }, @@ -1303,7 +1302,7 @@ function rejectMoneyRequestsOnSearch( type Params = Record; function exportSearchItemsToCSV( - {query, jsonQuery, reportIDList, transactionIDList, isBasicExport, exportColumnLabels, exportName}: ExportSearchItemsToCSVParams, + {jsonQuery, reportIDList, transactionIDList, isBasicExport, exportColumnLabels, exportName}: ExportSearchItemsToCSVParams, onDownloadFailed: () => void, translate: LocalizedTranslate, ) { @@ -1333,7 +1332,6 @@ function exportSearchItemsToCSV( } const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV, { - query, jsonQuery, reportIDList: Array.from(reportIDSet), transactionIDList, @@ -1364,7 +1362,7 @@ function exportSearchItemsToCSV( ); } -function queueExportSearchItemsToCSV({query, jsonQuery, reportIDList, transactionIDList, isBasicExport, exportColumnLabels, exportName}: ExportSearchItemsToCSVParams): string { +function queueExportSearchItemsToCSV({jsonQuery, reportIDList, transactionIDList, isBasicExport, exportColumnLabels, exportName}: ExportSearchItemsToCSVParams): string { const exportID = rand64(); const onyxKey = `${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}${exportID}` as const; @@ -1390,7 +1388,6 @@ function queueExportSearchItemsToCSV({query, jsonQuery, reportIDList, transactio }, ]; const finalParameters = enhanceParameters(WRITE_COMMANDS.QUEUE_EXPORT_SEARCH_ITEMS_TO_CSV, { - query, jsonQuery, reportIDList, transactionIDList, diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index b968026df066..c5491bf60ca7 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -28,7 +28,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {canSendInvoice, getDefaultChatEnabledPolicy, getGroupPoliciesWhereReportCanBeCreated} from '@libs/PolicyUtils'; import {generateReportID, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; -import {isDefaultExpenseReportsQuery, isDefaultExpensesQuery} from '@libs/SearchQueryUtils'; +import {getAllPolicyValues, getPolicyIDFromQuery, isDefaultExpenseReportsQuery, isDefaultExpensesQuery} from '@libs/SearchQueryUtils'; import type {SearchTypeMenuSection} from '@libs/SearchUIUtils'; import {TODO_SEARCH_KEYS} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; @@ -145,10 +145,10 @@ function EmptySearchViewContent({ const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy); - const filteredPolicyID = queryJSON?.policyID; + const filteredPolicyID = getPolicyIDFromQuery(queryJSON); let isFilteredWorkspaceAccessible = true; - if (filteredPolicyID) { - const policyIDToCheck = Array.isArray(filteredPolicyID) ? filteredPolicyID.at(0) : filteredPolicyID; + if (filteredPolicyID.value) { + const policyIDToCheck = getAllPolicyValues(filteredPolicyID, ONYXKEYS.COLLECTION.POLICY, allPolicies).at(0); const filteredPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyIDToCheck}`]; isFilteredWorkspaceAccessible = !!filteredPolicy; } diff --git a/src/pages/Search/SearchAdvancedFiltersContentPage/CommonFilterContentPageWrapper.tsx b/src/pages/Search/SearchAdvancedFiltersContentPage/CommonFilterContentPageWrapper.tsx index 18b93a046f58..97f9fdd8cf6b 100644 --- a/src/pages/Search/SearchAdvancedFiltersContentPage/CommonFilterContentPageWrapper.tsx +++ b/src/pages/Search/SearchAdvancedFiltersContentPage/CommonFilterContentPageWrapper.tsx @@ -5,7 +5,7 @@ import CommonFilterContent from '@components/Search/FilterComponents/AdvancedFil import type {CommonFilterContentWrapperProps} from '@components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent'; import useLocalize from '@hooks/useLocalize'; -function CommonFilterContentPageWrapper({filterKey, value: initialValue, type, policyIDs, policyIDQuery, ready, onChange}: CommonFilterContentWrapperProps) { +function CommonFilterContentPageWrapper({filterKey, value: initialValue, type, policyID, ready, onChange}: CommonFilterContentWrapperProps) { const {translate} = useLocalize(); const [value, setValue] = useState(initialValue); @@ -14,8 +14,7 @@ function CommonFilterContentPageWrapper({filterKey, value: initialValue, type, p filterKey={filterKey} value={value} type={type} - policyIDs={policyIDs} - policyIDQuery={policyIDQuery} + policyID={policyID} autoFocus ready={ready} onChange={setValue} diff --git a/src/pages/Search/SearchAdvancedFiltersContentPage/SearchAdvancedFiltersContentBase.tsx b/src/pages/Search/SearchAdvancedFiltersContentPage/SearchAdvancedFiltersContentBase.tsx index 67c749fd6094..7d3c79fb8360 100644 --- a/src/pages/Search/SearchAdvancedFiltersContentPage/SearchAdvancedFiltersContentBase.tsx +++ b/src/pages/Search/SearchAdvancedFiltersContentPage/SearchAdvancedFiltersContentBase.tsx @@ -5,7 +5,6 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SearchAdvancedFiltersContent from '@components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent'; -import {useSearchQueryContext} from '@components/Search/SearchContext'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; @@ -31,7 +30,6 @@ function SearchAdvancedFiltersContentBase() { const styles = useThemeStyles(); const {translate} = useLocalize(); - const {currentSearchQueryJSON} = useSearchQueryContext(); const filterKey = route.params.filterKey; const {currentDraftFilters} = useContext(SearchAdvancedFiltersContext); const {setDraftFilters} = useContext(SearchAdvancedFiltersActionContext); @@ -61,7 +59,6 @@ function SearchAdvancedFiltersContentBase() { Navigation.navigate(ROUTES.SEARCH_ADVANCED_FILTERS_CONTENT.getRoute(filterKey))} /> {shouldShowResetFilters && ( diff --git a/src/pages/Search/SearchSavePage.tsx b/src/pages/Search/SearchSavePage.tsx index 1befef88e66c..099eaaeb39f4 100644 --- a/src/pages/Search/SearchSavePage.tsx +++ b/src/pages/Search/SearchSavePage.tsx @@ -165,7 +165,7 @@ function SearchSavePage() { Navigation.goBack(); }; - const appliedFilters = mapFiltersFormToLabelValueList(searchAdvancedFiltersForm, undefined, undefined, translate, localeCompare, convertToDisplayStringWithoutCurrency); + const appliedFilters = mapFiltersFormToLabelValueList(searchAdvancedFiltersForm, undefined, translate, localeCompare, convertToDisplayStringWithoutCurrency); const appliedDisplays = getAppliedDisplays(searchAdvancedFiltersForm, currentSearchQueryJSON, translate); const {inputCallbackRef} = useAutoFocusInput(); diff --git a/src/pages/home/SpendOverTimeSection/useSpendOverTimeData.ts b/src/pages/home/SpendOverTimeSection/useSpendOverTimeData.ts index 602b851ced64..9ff8606cf603 100644 --- a/src/pages/home/SpendOverTimeSection/useSpendOverTimeData.ts +++ b/src/pages/home/SpendOverTimeSection/useSpendOverTimeData.ts @@ -87,7 +87,6 @@ function useSpendOverTimeData() { searchResults?.data && queryJSON && groupBy && login ? (getSortedSections( queryJSON.type, - queryJSON.status, getSections({ type: queryJSON.type, data: searchResults.data, diff --git a/src/types/form/SearchAdvancedFiltersForm.ts b/src/types/form/SearchAdvancedFiltersForm.ts index 390560d95c9d..50b45d3ac5ae 100644 --- a/src/types/form/SearchAdvancedFiltersForm.ts +++ b/src/types/form/SearchAdvancedFiltersForm.ts @@ -1,4 +1,4 @@ -import type {ValueOf} from 'type-fest'; +import type {TupleToUnion, ValueOf} from 'type-fest'; import type { ReportFieldDateKey, ReportFieldKey, @@ -28,12 +28,32 @@ const DATE_FILTER_KEYS: SearchDateFilterKeys[] = [ const AMOUNT_FILTER_KEYS: SearchAmountFilterKeys[] = [CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT, CONST.SEARCH.SYNTAX_FILTER_KEYS.TOTAL, CONST.SEARCH.SYNTAX_FILTER_KEYS.PURCHASE_AMOUNT]; +const NEGATABLE_FILTER_KEYS = [ + CONST.SEARCH.SYNTAX_FILTER_KEYS.TO, + CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM, + CONST.SEARCH.SYNTAX_FILTER_KEYS.HAS, + CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY, + CONST.SEARCH.SYNTAX_FILTER_KEYS.PURCHASE_CURRENCY, + CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT, + CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPORTED_TO, + CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, + CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, +] as const; + +type SearchNegatableFilterKeys = TupleToUnion; + +const NEGATABLE_FILTERS = new Set(NEGATABLE_FILTER_KEYS); + const FILTER_KEYS = { - POLICY_ID: 'policyID', GROUP_BY: 'groupBy', VIEW: 'view', TYPE: 'type', + STATUS: 'status', + STATUS_NOT: 'statusNot', + + POLICY_ID: 'policyID', + POLICY_ID_NOT: 'policyIDNot', DATE_NOT: 'dateNot', DATE_ON: 'dateOn', @@ -196,12 +216,14 @@ const ALLOWED_TYPE_FILTERS: Record> = { [CONST.SEARCH.DATA_TYPES.EXPENSE]: new Set([ FILTER_KEYS.TYPE, FILTER_KEYS.STATUS, + FILTER_KEYS.STATUS_NOT, FILTER_KEYS.FROM, FILTER_KEYS.FROM_NOT, FILTER_KEYS.TO, FILTER_KEYS.TO_NOT, FILTER_KEYS.KEYWORD, FILTER_KEYS.POLICY_ID, + FILTER_KEYS.POLICY_ID_NOT, FILTER_KEYS.EXPENSE_TYPE, FILTER_KEYS.EXPENSE_TYPE_NOT, FILTER_KEYS.RECEIPT_TYPE, @@ -309,12 +331,14 @@ const ALLOWED_TYPE_FILTERS: Record> = { [CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT]: new Set([ FILTER_KEYS.TYPE, FILTER_KEYS.STATUS, + FILTER_KEYS.STATUS_NOT, FILTER_KEYS.FROM, FILTER_KEYS.FROM_NOT, FILTER_KEYS.TO, FILTER_KEYS.TO_NOT, FILTER_KEYS.KEYWORD, FILTER_KEYS.POLICY_ID, + FILTER_KEYS.POLICY_ID_NOT, FILTER_KEYS.DATE_ON, FILTER_KEYS.DATE_NOT, FILTER_KEYS.DATE_AFTER, @@ -375,12 +399,14 @@ const ALLOWED_TYPE_FILTERS: Record> = { [CONST.SEARCH.DATA_TYPES.INVOICE]: new Set([ FILTER_KEYS.TYPE, FILTER_KEYS.STATUS, + FILTER_KEYS.STATUS_NOT, FILTER_KEYS.FROM, FILTER_KEYS.FROM_NOT, FILTER_KEYS.TO, FILTER_KEYS.TO_NOT, FILTER_KEYS.KEYWORD, FILTER_KEYS.POLICY_ID, + FILTER_KEYS.POLICY_ID_NOT, FILTER_KEYS.MERCHANT, FILTER_KEYS.MERCHANT_NOT, FILTER_KEYS.DATE_ON, @@ -469,12 +495,14 @@ const ALLOWED_TYPE_FILTERS: Record> = { [CONST.SEARCH.DATA_TYPES.TRIP]: new Set([ FILTER_KEYS.TYPE, FILTER_KEYS.STATUS, + FILTER_KEYS.STATUS_NOT, FILTER_KEYS.FROM, FILTER_KEYS.FROM_NOT, FILTER_KEYS.TO, FILTER_KEYS.TO_NOT, FILTER_KEYS.KEYWORD, FILTER_KEYS.POLICY_ID, + FILTER_KEYS.POLICY_ID_NOT, FILTER_KEYS.MERCHANT, FILTER_KEYS.MERCHANT_NOT, FILTER_KEYS.DATE_ON, @@ -562,6 +590,7 @@ const ALLOWED_TYPE_FILTERS: Record> = { FILTER_KEYS.IN_NOT, FILTER_KEYS.KEYWORD, FILTER_KEYS.POLICY_ID, + FILTER_KEYS.POLICY_ID_NOT, FILTER_KEYS.DATE_AFTER, FILTER_KEYS.DATE_BEFORE, FILTER_KEYS.DATE_ON, @@ -576,6 +605,7 @@ const ALLOWED_TYPE_FILTERS: Record> = { [CONST.SEARCH.DATA_TYPES.TASK]: new Set([ FILTER_KEYS.TYPE, FILTER_KEYS.STATUS, + FILTER_KEYS.STATUS_NOT, FILTER_KEYS.TITLE, FILTER_KEYS.TITLE_NOT, FILTER_KEYS.DESCRIPTION, @@ -614,7 +644,8 @@ type SearchAdvancedFiltersForm = Form< [FILTER_KEYS.TYPE]: SearchDataTypes; [FILTER_KEYS.COLUMNS]: SearchCustomColumnIds[]; - [FILTER_KEYS.STATUS]: string[] | string; + [FILTER_KEYS.STATUS]: string[]; + [FILTER_KEYS.STATUS_NOT]: string[]; [FILTER_KEYS.DATE_ON]: string; [FILTER_KEYS.DATE_NOT]: string; @@ -672,6 +703,7 @@ type SearchAdvancedFiltersForm = Form< [FILTER_KEYS.CATEGORY_NOT]: string[]; [FILTER_KEYS.POLICY_ID]: string[]; + [FILTER_KEYS.POLICY_ID_NOT]: string[]; [FILTER_KEYS.CARD_ID]: string[]; [FILTER_KEYS.CARD_ID_NOT]: string[]; @@ -776,6 +808,17 @@ type SearchAdvancedFiltersForm = Form< Record >; -export type {SearchAdvancedFiltersForm, SearchAdvancedFiltersKey, HasFilterValue, HasFilterValues, IsFilterValue, IsFilterValues, ExpenseTypeValue, ExpenseTypeValues, ReceiptTypeValue}; +export type { + SearchAdvancedFiltersForm, + SearchAdvancedFiltersKey, + HasFilterValue, + HasFilterValues, + IsFilterValue, + IsFilterValues, + ExpenseTypeValue, + ExpenseTypeValues, + ReceiptTypeValue, + SearchNegatableFilterKeys, +}; export default FILTER_KEYS; -export {DATE_FILTER_KEYS, ALLOWED_TYPE_FILTERS, AMOUNT_FILTER_KEYS}; +export {DATE_FILTER_KEYS, ALLOWED_TYPE_FILTERS, AMOUNT_FILTER_KEYS, NEGATABLE_FILTERS}; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 375831bd7b8d..fc725c36196d 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -1,9 +1,17 @@ import type {ValueOf} from 'type-fest'; import type ChatListItem from '@components/Search/SearchList/ListItem/ChatListItem'; +import type ExpenseReportListItem from '@components/Search/SearchList/ListItem/ExpenseReportListItem'; +import type TaskListItem from '@components/Search/SearchList/ListItem/TaskListItem'; import type TransactionGroupListItem from '@components/Search/SearchList/ListItem/TransactionGroupListItem'; import type TransactionListItem from '@components/Search/SearchList/ListItem/TransactionListItem'; -import type {ReportActionListItemType, TaskListItemType, TransactionGroupListItemType, TransactionListItemType} from '@components/Search/SearchList/ListItem/types'; -import type {SearchStatus} from '@components/Search/types'; +import type { + ReportActionListItemType, + TaskListItemType, + TransactionGroupListItemType, + TransactionListItemType, + TransactionReportGroupListItemType, +} from '@components/Search/SearchList/ListItem/types'; +import type {SearchGroupBy, SearchStatus} from '@components/Search/types'; import type CONST from '@src/CONST'; import type ONYXKEYS from '@src/ONYXKEYS'; import type PrefixedRecord from '@src/types/utils/PrefixedRecord'; @@ -22,20 +30,26 @@ import type {TransactionViolation} from './TransactionViolation'; type SearchDataTypes = ValueOf; /** Model of search result list item */ -type ListItemType = C extends typeof CONST.SEARCH.DATA_TYPES.CHAT +type ListItemType = C extends typeof CONST.SEARCH.DATA_TYPES.CHAT ? typeof ChatListItem - : T extends typeof CONST.SEARCH.STATUS.EXPENSE.ALL - ? typeof TransactionListItem - : typeof TransactionGroupListItem; + : C extends typeof CONST.SEARCH.DATA_TYPES.TASK + ? typeof TaskListItem + : C extends typeof CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT + ? typeof ExpenseReportListItem + : G extends SearchGroupBy + ? typeof TransactionGroupListItem + : typeof TransactionListItem; /** Model of search list item data type */ -type ListItemDataType = C extends typeof CONST.SEARCH.DATA_TYPES.CHAT +type ListItemDataType = C extends typeof CONST.SEARCH.DATA_TYPES.CHAT ? ReportActionListItemType[] : C extends typeof CONST.SEARCH.DATA_TYPES.TASK ? TaskListItemType[] - : T extends typeof CONST.SEARCH.STATUS.EXPENSE.ALL - ? TransactionListItemType[] - : TransactionGroupListItemType[]; + : C extends typeof CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT + ? TransactionReportGroupListItemType[] + : G extends SearchGroupBy + ? TransactionGroupListItemType[] + : TransactionListItemType[]; /** Model of search result state */ type SearchResultsInfo = { @@ -45,8 +59,8 @@ type SearchResultsInfo = { /** Type of search */ type: SearchDataTypes; - /** The status filter for the current search */ - status: SearchStatus; + /** The hash of the current search */ + hash: number; /** Whether the user can fetch more search results */ hasMoreResults: boolean; diff --git a/tests/actions/IOU/RequestMoneyTest.ts b/tests/actions/IOU/RequestMoneyTest.ts index 4357fcfb4176..abbb177f77da 100644 --- a/tests/actions/IOU/RequestMoneyTest.ts +++ b/tests/actions/IOU/RequestMoneyTest.ts @@ -2470,7 +2470,6 @@ describe('actions/IOU', () => { it('adds grouped from snapshot optimistic data for grouped search queries', async () => { const currentSearchQueryJSON = { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING] as SearchStatus, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, groupBy: CONST.SEARCH.GROUP_BY.FROM, diff --git a/tests/actions/IOU/SearchUpdateTest.ts b/tests/actions/IOU/SearchUpdateTest.ts index 99a1216878e9..adfd517ddc44 100644 --- a/tests/actions/IOU/SearchUpdateTest.ts +++ b/tests/actions/IOU/SearchUpdateTest.ts @@ -135,7 +135,6 @@ describe('actions/IOU', () => { }; const currentSearchQueryJSON = { type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - status: '' as SearchStatus, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, filters: { @@ -193,7 +192,6 @@ describe('actions/IOU', () => { }; const currentSearchQueryJSON = { type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - status: '' as SearchStatus, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, filters: { @@ -253,7 +251,6 @@ describe('actions/IOU', () => { }; const currentSearchQueryJSON = { type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - status: '' as SearchStatus, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, filters: { @@ -297,13 +294,16 @@ describe('actions/IOU', () => { const policyID = '12345'; const currentSearchQueryJSON = { type: 'expense', - status: '', sortBy: 'date', sortOrder: 'desc', - policyID: [policyID], - filters: null, + filters: {operator: 'eq', left: 'policyID', right: policyID}, inputQuery: `type:expense sortBy:date sortOrder:desc policyID:${policyID}`, - flatFilters: [], + flatFilters: [ + { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, + filters: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, value: policyID}], + }, + ], hash: 591785022, recentSearchHash: 714245044, similarSearchHash: 1023624110, diff --git a/tests/perf-test/useAdvancedSearchFilters.perf-test.tsx b/tests/perf-test/useAdvancedSearchFilters.perf-test.tsx index e8fd48a10b65..a07a852a145c 100644 --- a/tests/perf-test/useAdvancedSearchFilters.perf-test.tsx +++ b/tests/perf-test/useAdvancedSearchFilters.perf-test.tsx @@ -70,7 +70,7 @@ jest.mock('@react-navigation/native', () => { function TestComponent() { const currentType = 'expense'; - const filters = useAdvancedSearchFilters(currentType, undefined); + const filters = useAdvancedSearchFilters(currentType, {value: undefined, isNegated: false}); return ; } diff --git a/tests/unit/HomePage/YourSpendSection/rowStateTest.ts b/tests/unit/HomePage/YourSpendSection/rowStateTest.ts index cd834140def0..48fe0fc25f21 100644 --- a/tests/unit/HomePage/YourSpendSection/rowStateTest.ts +++ b/tests/unit/HomePage/YourSpendSection/rowStateTest.ts @@ -9,7 +9,6 @@ const makeSearchResults = (overrides: Partial = {}): SearchResult search: { offset: 0, type: 'expense', - status: '', hasMoreResults: false, hasResults: true, isLoading: false, @@ -23,8 +22,8 @@ const makeSearchResultsWithCount = (count: number): SearchResults => makeSearchResults({ search: { offset: 0, + hash: 0, type: 'expense', - status: '', hasMoreResults: false, hasResults: count > 0, isLoading: false, diff --git a/tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts b/tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts index 2756a910a4ec..54b1736eb351 100644 --- a/tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts +++ b/tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts @@ -136,7 +136,6 @@ function makeSearchResultsWithCount(count: number): SearchResults { return { search: { type: 'expense', - status: '', offset: 0, hasMoreResults: false, hasResults: count > 0, @@ -587,7 +586,7 @@ describe('useYourSpendData — third-party cardRows', () => { mockedGetDisplayableThirdPartyCards.mockReturnValue(makeThirdPartyCards([{cardID: THIRD_PARTY_CARD_ID_1, lastFourPAN: THIRD_PARTY_LAST_FOUR_1}])); // First render: READY snapshot with count > 0 → row produced and total cached. setupCardSnapshot(THIRD_PARTY_CARD_ID_1, { - search: {type: 'expense', status: '', offset: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: 3, total: 1234, currency: 'USD'}, + search: {type: 'expense', offset: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: 3, total: 1234, currency: 'USD'}, data: {}, } as SearchResults); const {result, rerender} = renderHook(() => useYourSpendData()); @@ -627,11 +626,11 @@ describe('useYourSpendData — third-party cardRows', () => { ]), ); setupCardSnapshot(THIRD_PARTY_CARD_ID_1, { - search: {type: 'expense', status: '', offset: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: 2, total: 500, currency: 'USD'}, + search: {type: 'expense', offset: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: 2, total: 500, currency: 'USD'}, data: {}, } as SearchResults); setupCardSnapshot(THIRD_PARTY_CARD_ID_2, { - search: {type: 'expense', status: '', offset: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: 3, total: 2200, currency: 'EUR'}, + search: {type: 'expense', offset: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: 3, total: 2200, currency: 'EUR'}, data: {}, } as SearchResults); const {result} = renderHook(() => useYourSpendData()); diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index 6b4babced2cd..2646b2643b03 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -2634,11 +2634,11 @@ describe('PolicyUtils', () => { describe('getConnectedIntegrationNamesForPolicies', () => { it('returns empty Set when policies is undefined', () => { - expect(getConnectedIntegrationNamesForPolicies(undefined)).toEqual(new Set()); + expect(getConnectedIntegrationNamesForPolicies(undefined, {value: undefined, isNegated: false})).toEqual(new Set()); }); it('returns empty Set when policies is empty object', () => { - expect(getConnectedIntegrationNamesForPolicies({})).toEqual(new Set()); + expect(getConnectedIntegrationNamesForPolicies({}, {value: undefined, isNegated: false})).toEqual(new Set()); }); it('returns Set with connection name when policy has verified connection', () => { @@ -2653,7 +2653,7 @@ describe('PolicyUtils', () => { const policies: OnyxCollection = { [`${ONYXKEYS.COLLECTION.POLICY}1`]: policyWithXero, }; - expect(getConnectedIntegrationNamesForPolicies(policies)).toEqual(new Set([CONST.POLICY.CONNECTIONS.NAME.XERO])); + expect(getConnectedIntegrationNamesForPolicies(policies, {value: undefined, isNegated: false})).toEqual(new Set([CONST.POLICY.CONNECTIONS.NAME.XERO])); }); it('filters by policyIDs when provided', () => { @@ -2673,7 +2673,7 @@ describe('PolicyUtils', () => { [`${ONYXKEYS.COLLECTION.POLICY}1`]: policy1WithQBO, [`${ONYXKEYS.COLLECTION.POLICY}2`]: policy2WithXero, }; - expect(getConnectedIntegrationNamesForPolicies(policies, ['1'])).toEqual(new Set([CONST.POLICY.CONNECTIONS.NAME.QBO])); + expect(getConnectedIntegrationNamesForPolicies(policies, {value: ['1'], isNegated: false})).toEqual(new Set([CONST.POLICY.CONNECTIONS.NAME.QBO])); }); it('returns all connection names when policies have different connections', () => { @@ -2695,7 +2695,7 @@ describe('PolicyUtils', () => { [`${ONYXKEYS.COLLECTION.POLICY}1`]: policy1, [`${ONYXKEYS.COLLECTION.POLICY}2`]: policy2, }; - const result = getConnectedIntegrationNamesForPolicies(policies); + const result = getConnectedIntegrationNamesForPolicies(policies, {value: ['1'], isNegated: false}); expect(result).toContain(CONST.POLICY.CONNECTIONS.NAME.QBO); expect(result).toContain(CONST.POLICY.CONNECTIONS.NAME.XERO); expect(result.size).toBe(2); diff --git a/tests/unit/Search/ExpenseFlatSearchViewTest.tsx b/tests/unit/Search/ExpenseFlatSearchViewTest.tsx index 67f7124f6440..7cad925006fc 100644 --- a/tests/unit/Search/ExpenseFlatSearchViewTest.tsx +++ b/tests/unit/Search/ExpenseFlatSearchViewTest.tsx @@ -112,14 +112,12 @@ const STABLE_QUERY_JSON: SearchQueryJSON = { similarSearchHash: 0, groupBy: undefined, type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: 'desc', view: CONST.SEARCH.VIEW.TABLE, flatFilters: [], inputQuery: '', filters: {operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, left: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, right: ''}, - policyID: undefined, columns: undefined, limit: undefined, rawFilterList: undefined, diff --git a/tests/unit/Search/SearchListRenderCountTest.tsx b/tests/unit/Search/SearchListRenderCountTest.tsx index 40bbb1a49344..9f683c52932f 100644 --- a/tests/unit/Search/SearchListRenderCountTest.tsx +++ b/tests/unit/Search/SearchListRenderCountTest.tsx @@ -108,14 +108,12 @@ const STABLE_QUERY_JSON: SearchQueryJSON = { similarSearchHash: 0, groupBy: undefined, type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: 'desc', view: CONST.SEARCH.VIEW.TABLE, flatFilters: [], inputQuery: '', filters: {operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, left: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, right: ''}, - policyID: undefined, columns: undefined, limit: undefined, rawFilterList: undefined, diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 87b4f24be70b..89a63851b0a7 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -26,6 +26,7 @@ import { getQueryWithUpdatedValues, getRangeBoundariesFromFormValue, getRoutes, + getStatusFromQuery, isSearchRootParams, serializeQueryJSONForBackend, shouldHighlight, @@ -2317,7 +2318,7 @@ describe('SearchQueryUtils', () => { const newQueryJSON = buildSearchQueryJSON(result); const keywordFilter = newQueryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD); expect(keywordFilter?.filters.at(0)?.value).toBe('status:done'); - expect(newQueryJSON?.status).toBe(CONST.SEARCH.STATUS.EXPENSE.ALL); + expect(getStatusFromQuery(newQueryJSON)).toBe(CONST.SEARCH.STATUS.EXPENSE.ALL); }); test('does not add quotes to non-keyword filter values', () => { diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 39e12115c262..3f6f1825347b 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -545,7 +545,7 @@ const searchResults: OnyxTypes.SearchResults = { hasMoreResults: false, hasResults: true, offset: 0, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, isLoading: false, type: 'expense', }, @@ -586,7 +586,7 @@ const searchResultsGroupByFrom: OnyxTypes.SearchResults = { hasMoreResults: false, hasResults: true, offset: 0, - status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], + hash: 0, total: 100, isLoading: false, type: 'expense', @@ -636,7 +636,7 @@ const searchResultsGroupByCard: OnyxTypes.SearchResults = { hasMoreResults: false, hasResults: true, offset: 0, - status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], + hash: 0, total: 60, isLoading: false, type: 'expense', @@ -673,7 +673,7 @@ const searchResultsGroupByWithdrawalID: OnyxTypes.SearchResults = { hasMoreResults: false, hasResults: true, offset: 0, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, total: 60, isLoading: false, type: 'expense', @@ -705,7 +705,7 @@ const searchResultsGroupByCategory: OnyxTypes.SearchResults = { hasMoreResults: false, hasResults: true, offset: 0, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, total: 325, isLoading: false, type: 'expense', @@ -1900,7 +1900,7 @@ const searchResultsGroupByMerchant: OnyxTypes.SearchResults = { hasMoreResults: false, hasResults: true, offset: 0, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, total: 470, isLoading: false, type: 'expense', @@ -1957,7 +1957,7 @@ const searchResultsGroupByTag: OnyxTypes.SearchResults = { hasMoreResults: false, hasResults: true, offset: 0, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, total: 325, isLoading: false, type: 'expense', @@ -2038,7 +2038,7 @@ const searchResultsGroupByMonth: OnyxTypes.SearchResults = { hasMoreResults: false, hasResults: true, offset: 0, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, total: 325, isLoading: false, type: 'expense', @@ -2067,7 +2067,7 @@ const searchResultsGroupByYear: OnyxTypes.SearchResults = { hasMoreResults: false, hasResults: true, offset: 0, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, total: 325, isLoading: false, type: 'expense', @@ -2098,7 +2098,7 @@ const searchResultsGroupByQuarter: OnyxTypes.SearchResults = { hasMoreResults: false, hasResults: true, offset: 0, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, total: 325, isLoading: false, type: 'expense', @@ -2127,7 +2127,7 @@ const searchResultsGroupByWeek: OnyxTypes.SearchResults = { hasMoreResults: false, hasResults: true, offset: 0, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, total: 325, isLoading: false, type: 'expense', @@ -2577,43 +2577,43 @@ describe('SearchUIUtils', () => { describe('Test getListItem', () => { it('should return ChatListItem when type is CHAT', () => { - expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.EXPENSE.ALL)).toStrictEqual(ChatListItem); + expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.CHAT)).toStrictEqual(ChatListItem); }); it('should return TransactionListItem when groupBy is undefined', () => { - expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.ALL, undefined)).toStrictEqual(TransactionListItem); + expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.EXPENSE, undefined)).toStrictEqual(TransactionListItem); }); it('should return ExpenseReportListItem when type is EXPENSE-REPORT', () => { - expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, CONST.SEARCH.STATUS.EXPENSE.ALL)).toStrictEqual(ExpenseReportListItem); + expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT)).toStrictEqual(ExpenseReportListItem); }); it('should return TransactionListItem when type is TRIP', () => { - expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.STATUS.EXPENSE.ALL)).toStrictEqual(TransactionListItem); + expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.TRIP)).toStrictEqual(TransactionListItem); }); it('should return TransactionListItem when type is INVOICE', () => { - expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.INVOICE, CONST.SEARCH.STATUS.EXPENSE.ALL)).toStrictEqual(TransactionListItem); + expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.INVOICE)).toStrictEqual(TransactionListItem); }); it('should return TransactionGroupListItem when type is EXPENSE and groupBy is member', () => { - expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.ALL, CONST.SEARCH.GROUP_BY.FROM)).toStrictEqual(TransactionGroupListItem); + expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.GROUP_BY.FROM)).toStrictEqual(TransactionGroupListItem); }); it('should return TransactionGroupListItem when type is TRIP and groupBy is member', () => { - expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.STATUS.EXPENSE.ALL, CONST.SEARCH.GROUP_BY.FROM)).toStrictEqual(TransactionGroupListItem); + expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.GROUP_BY.FROM)).toStrictEqual(TransactionGroupListItem); }); it('should return TransactionGroupListItem when type is INVOICE and groupBy is member', () => { - expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.INVOICE, CONST.SEARCH.STATUS.EXPENSE.ALL, CONST.SEARCH.GROUP_BY.FROM)).toStrictEqual(TransactionGroupListItem); + expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.INVOICE, CONST.SEARCH.GROUP_BY.FROM)).toStrictEqual(TransactionGroupListItem); }); it('should return TransactionGroupListItem when type is EXPENSE and groupBy is category', () => { - expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.ALL, CONST.SEARCH.GROUP_BY.CATEGORY)).toStrictEqual(TransactionGroupListItem); + expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.GROUP_BY.CATEGORY)).toStrictEqual(TransactionGroupListItem); }); it('should return TransactionGroupListItem when type is EXPENSE and groupBy is merchant', () => { - expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.ALL, CONST.SEARCH.GROUP_BY.MERCHANT)).toStrictEqual(TransactionGroupListItem); + expect(SearchUIUtils.getListItem(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.GROUP_BY.MERCHANT)).toStrictEqual(TransactionGroupListItem); }); }); @@ -4128,7 +4128,6 @@ describe('SearchUIUtils', () => { groupBy: CONST.SEARCH.GROUP_BY.CATEGORY, queryJSON: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: '', sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, @@ -4386,7 +4385,6 @@ describe('SearchUIUtils', () => { groupBy: CONST.SEARCH.GROUP_BY.MERCHANT, queryJSON: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: '', sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, @@ -4435,7 +4433,6 @@ describe('SearchUIUtils', () => { groupBy: CONST.SEARCH.GROUP_BY.MERCHANT, queryJSON: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: '', sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, @@ -4484,7 +4481,6 @@ describe('SearchUIUtils', () => { groupBy: CONST.SEARCH.GROUP_BY.MERCHANT, queryJSON: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: '', sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, @@ -4533,7 +4529,6 @@ describe('SearchUIUtils', () => { groupBy: CONST.SEARCH.GROUP_BY.MERCHANT, queryJSON: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: '', sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, @@ -4603,7 +4598,6 @@ describe('SearchUIUtils', () => { groupBy: CONST.SEARCH.GROUP_BY.MERCHANT, queryJSON: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: '', sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, @@ -4907,7 +4901,6 @@ describe('SearchUIUtils', () => { groupBy: CONST.SEARCH.GROUP_BY.TAG, queryJSON: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: '', sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, @@ -5471,14 +5464,19 @@ describe('SearchUIUtils', () => { const expenseType: typeof CONST.SEARCH.DATA_TYPES.EXPENSE = CONST.SEARCH.DATA_TYPES.EXPENSE; function makeExpenseQueryJSON(status: string | string[]) { + const statusValues = Array.isArray(status) ? status : [status]; return { type: expenseType, - status, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, hash: TEST_QUERY_HASH, - flatFilters: [], + flatFilters: [ + { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, + filters: statusValues.map((value) => ({operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, value})), + }, + ], inputQuery: 'type:expense' as const, recentSearchHash: TEST_QUERY_HASH, similarSearchHash: TEST_QUERY_HASH, @@ -6002,13 +6000,7 @@ describe('SearchUIUtils', () => { describe('Test getSortedSections', () => { it('should return getSortedReportActionData result when type is CHAT', () => { - const sortedActions = SearchUIUtils.getSortedSections( - CONST.SEARCH.DATA_TYPES.CHAT, - CONST.SEARCH.STATUS.EXPENSE.ALL, - reportActionListItems, - localeCompare, - translateLocal, - ) as ReportActionListItemType[]; + const sortedActions = SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.CHAT, reportActionListItems, localeCompare, translateLocal) as ReportActionListItemType[]; // Should return all report actions sorted by creation date in descending order (newest first) expect(sortedActions).toHaveLength(5); expect(sortedActions.at(0)?.created).toBe('2024-12-21 13:05:24'); // reportAction4 (newest) @@ -6016,7 +6008,7 @@ describe('SearchUIUtils', () => { }); it('should return getSortedTransactionData result when groupBy is undefined', () => { - expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE, '', transactionsListItems, localeCompare, translateLocal, 'date', 'asc', undefined)).toStrictEqual( + expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE, transactionsListItems, localeCompare, translateLocal, 'date', 'asc', undefined)).toStrictEqual( transactionsListItems, ); }); @@ -6085,7 +6077,6 @@ describe('SearchUIUtils', () => { const ascendingResult = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', [...unorderedTransactions], localeCompare, translateLocal, @@ -6096,7 +6087,6 @@ describe('SearchUIUtils', () => { ); const descendingResult = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', [...unorderedTransactions], localeCompare, translateLocal, @@ -6111,41 +6101,32 @@ describe('SearchUIUtils', () => { }); it('should return getSortedReportData result when type is expense-report', () => { - expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, '', transactionReportGroupListItems, localeCompare, translateLocal, 'date', 'asc')).toStrictEqual( + expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, transactionReportGroupListItems, localeCompare, translateLocal, 'date', 'asc')).toStrictEqual( transactionReportGroupListItems, ); }); it('should return getSortedReportData result when type is TRIP and groupBy is report', () => { - expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.TRIP, '', transactionReportGroupListItems, localeCompare, translateLocal, 'date', 'asc')).toStrictEqual( + expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.TRIP, transactionReportGroupListItems, localeCompare, translateLocal, 'date', 'asc')).toStrictEqual( transactionReportGroupListItems, ); }); it('should return getSortedReportData result when type is INVOICE and groupBy is report', () => { - expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.INVOICE, '', transactionReportGroupListItems, localeCompare, translateLocal, 'date', 'asc')).toStrictEqual( + expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.INVOICE, transactionReportGroupListItems, localeCompare, translateLocal, 'date', 'asc')).toStrictEqual( transactionReportGroupListItems, ); }); it('should sort member group data when type is EXPENSE and groupBy is member', () => { expect( - SearchUIUtils.getSortedSections( - CONST.SEARCH.DATA_TYPES.EXPENSE, - '', - transactionMemberGroupListItems, - localeCompare, - translateLocal, - 'date', - 'asc', - CONST.SEARCH.GROUP_BY.FROM, - ), + SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE, transactionMemberGroupListItems, localeCompare, translateLocal, 'date', 'asc', CONST.SEARCH.GROUP_BY.FROM), ).toStrictEqual(transactionMemberGroupListItemsSorted); }); it('should sort card group data when type is EXPENSE and groupBy is card', () => { expect( - SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE, '', transactionCardGroupListItems, localeCompare, translateLocal, 'date', 'asc', CONST.SEARCH.GROUP_BY.CARD), + SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE, transactionCardGroupListItems, localeCompare, translateLocal, 'date', 'asc', CONST.SEARCH.GROUP_BY.CARD), ).toStrictEqual(transactionCardGroupListItemsSorted); }); @@ -6153,7 +6134,6 @@ describe('SearchUIUtils', () => { expect( SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionWithdrawalIDGroupListItems, localeCompare, translateLocal, @@ -6168,7 +6148,6 @@ describe('SearchUIUtils', () => { expect( SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionCategoryGroupListItems, localeCompare, translateLocal, @@ -6182,7 +6161,6 @@ describe('SearchUIUtils', () => { it('should sort category data by category name in ascending order', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionCategoryGroupListItems, localeCompare, translateLocal, @@ -6199,7 +6177,6 @@ describe('SearchUIUtils', () => { it('should sort category data by category name in descending order', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionCategoryGroupListItems, localeCompare, translateLocal, @@ -6216,7 +6193,6 @@ describe('SearchUIUtils', () => { it('should sort category data by total amount', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionCategoryGroupListItems, localeCompare, translateLocal, @@ -6235,7 +6211,6 @@ describe('SearchUIUtils', () => { // This test verifies that the sorting works with the parser's default value const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionCategoryGroupListItems, localeCompare, translateLocal, @@ -6252,7 +6227,6 @@ describe('SearchUIUtils', () => { it('should sort category data by expenses count', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionCategoryGroupListItems, localeCompare, translateLocal, @@ -6271,7 +6245,6 @@ describe('SearchUIUtils', () => { expect( SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionMerchantGroupListItems, localeCompare, translateLocal, @@ -6285,7 +6258,6 @@ describe('SearchUIUtils', () => { it('should sort merchant data by merchant name in ascending order', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionMerchantGroupListItems, localeCompare, translateLocal, @@ -6302,7 +6274,6 @@ describe('SearchUIUtils', () => { it('should sort merchant data by merchant name in descending order', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionMerchantGroupListItems, localeCompare, translateLocal, @@ -6319,7 +6290,6 @@ describe('SearchUIUtils', () => { it('should sort merchant data by total amount', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionMerchantGroupListItems, localeCompare, translateLocal, @@ -6338,7 +6308,6 @@ describe('SearchUIUtils', () => { // This test verifies that the sorting works with the parser's default value const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionMerchantGroupListItems, localeCompare, translateLocal, @@ -6355,7 +6324,6 @@ describe('SearchUIUtils', () => { it('should sort merchant data by expenses count', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionMerchantGroupListItems, localeCompare, translateLocal, @@ -6409,7 +6377,6 @@ describe('SearchUIUtils', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', merchantDataWithEmpty, localeCompare, translateLocal, @@ -6429,7 +6396,6 @@ describe('SearchUIUtils', () => { expect( SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionTagGroupListItems, localeCompare, translateLocal, @@ -6443,7 +6409,6 @@ describe('SearchUIUtils', () => { it('should sort tag data by tag name in ascending order', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionTagGroupListItems, localeCompare, translateLocal, @@ -6460,7 +6425,6 @@ describe('SearchUIUtils', () => { it('should sort tag data by tag name in descending order', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionTagGroupListItems, localeCompare, translateLocal, @@ -6477,7 +6441,6 @@ describe('SearchUIUtils', () => { it('should sort tag data by total amount', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionTagGroupListItems, localeCompare, translateLocal, @@ -6536,7 +6499,6 @@ describe('SearchUIUtils', () => { // Then sort the sections const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', sections, localeCompare, translateLocal, @@ -7676,7 +7638,7 @@ describe('SearchUIUtils', () => { }, search: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, offset: 0, hasMoreResults: false, hasResults: true, @@ -7808,7 +7770,7 @@ describe('SearchUIUtils', () => { }, search: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, offset: 0, hasMoreResults: false, hasResults: true, diff --git a/tests/unit/Search/getSpendOverTimeStateTest.ts b/tests/unit/Search/getSpendOverTimeStateTest.ts index 1b053736d071..f4c0b6e820bd 100644 --- a/tests/unit/Search/getSpendOverTimeStateTest.ts +++ b/tests/unit/Search/getSpendOverTimeStateTest.ts @@ -5,7 +5,6 @@ import type SearchResults from '@src/types/onyx/SearchResults'; const queryJSON: SearchQueryJSON = { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, groupBy: CONST.SEARCH.GROUP_BY.MONTH, view: CONST.SEARCH.VIEW.LINE, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH, @@ -14,7 +13,7 @@ const queryJSON: SearchQueryJSON = { const makeSearchResults = (overrides: Partial = {}): SearchResults => ({ - search: {offset: 0, type: queryJSON.type, status: queryJSON.status, hasMoreResults: false, hasResults: true, isLoading: false}, + search: {offset: 0, type: queryJSON.type, hasMoreResults: false, hasResults: true, isLoading: false}, data: {}, ...overrides, }) as SearchResults; diff --git a/tests/unit/Search/yourSpendQueryBuildersTest.ts b/tests/unit/Search/yourSpendQueryBuildersTest.ts index 95aa2289af50..a12e011a7f63 100644 --- a/tests/unit/Search/yourSpendQueryBuildersTest.ts +++ b/tests/unit/Search/yourSpendQueryBuildersTest.ts @@ -8,7 +8,7 @@ */ import {buildAwaitingApprovalQuery, buildRecentCardTransactionsQuery, buildRepaidLast30DaysQuery} from '@pages/home/YourSpendSection/queries'; import CONST from '@src/CONST'; -import {buildSearchQueryJSON} from '@src/libs/SearchQueryUtils'; +import {buildSearchQueryJSON, getPolicyIDFromQuery, getStatusFromQuery} from '@src/libs/SearchQueryUtils'; const ACCOUNT_ID = 12345; const CARD_ID = 67890; @@ -48,7 +48,7 @@ describe('buildAwaitingApprovalQuery', () => { it('produces status:outstanding', () => { const queryJSON = buildSearchQueryJSON(queryString); - expect(queryJSON?.status).toBe(CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING); + expect(getStatusFromQuery(queryJSON)).toEqual([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]); }); it('resolves from to the numeric accountID (not literal [me])', () => { @@ -73,24 +73,17 @@ describe('buildAwaitingApprovalQuery', () => { expect(dateFilters).toHaveLength(0); }); - it('omits the policyID filter when the list is empty', () => { - // policyID is parsed onto the root of the query JSON (like type/status), not into flatFilters. - const queryJSON = buildSearchQueryJSON(queryString); - expect(queryJSON?.policyID).toBeUndefined(); - expect(queryString).not.toContain(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:`); - }); - it('emits the policyID filter for a single policy', () => { const scoped = buildAwaitingApprovalQuery(ACCOUNT_ID, ['policy_a']); const queryJSON = buildSearchQueryJSON(scoped); - expect(queryJSON?.policyID).toEqual(['policy_a']); + expect(getPolicyIDFromQuery(queryJSON).value).toEqual(['policy_a']); expect(scoped).toContain(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:policy_a`); }); it('emits the policyID filter for multiple policies, preserving the provided order', () => { const scoped = buildAwaitingApprovalQuery(ACCOUNT_ID, ['policy_a', 'policy_b', 'policy_c']); const queryJSON = buildSearchQueryJSON(scoped); - expect(queryJSON?.policyID).toEqual(['policy_a', 'policy_b', 'policy_c']); + expect(getPolicyIDFromQuery(queryJSON).value).toEqual(['policy_a', 'policy_b', 'policy_c']); expect(scoped).toContain(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:policy_a,policy_b,policy_c`); }); @@ -98,7 +91,7 @@ describe('buildAwaitingApprovalQuery', () => { const scoped = buildAwaitingApprovalQuery(ACCOUNT_ID, ['policy_a']); const queryJSON = buildSearchQueryJSON(scoped); expect(queryJSON?.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); - expect(queryJSON?.status).toBe(CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING); + expect(getStatusFromQuery(queryJSON)).toEqual([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]); const fromValues = getRawFiltersForKey(scoped, CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM).flatMap((f) => (Array.isArray(f.value) ? f.value : [f.value])); expect(fromValues).toContain(String(ACCOUNT_ID)); const reimbursableValues = getRawFiltersForKey(scoped, CONST.SEARCH.SYNTAX_FILTER_KEYS.REIMBURSABLE).flatMap((f) => (Array.isArray(f.value) ? f.value : [f.value])); @@ -134,7 +127,7 @@ describe('buildRepaidLast30DaysQuery', () => { it('produces status:paid', () => { const queryJSON = buildSearchQueryJSON(queryString); - expect(queryJSON?.status).toBe(CONST.SEARCH.STATUS.EXPENSE.PAID); + expect(getStatusFromQuery(queryJSON)).toEqual([CONST.SEARCH.STATUS.EXPENSE.PAID]); }); it('resolves from to the numeric accountID', () => { diff --git a/tests/unit/SearchActionsTest.ts b/tests/unit/SearchActionsTest.ts index 8511d0672c1d..ef657f36ce85 100644 --- a/tests/unit/SearchActionsTest.ts +++ b/tests/unit/SearchActionsTest.ts @@ -35,7 +35,6 @@ describe('queueExportSearchItemsToCSV', () => { it('sets optimistic Onyx data with state preparing and returns exportID', () => { const exportID = queueExportSearchItemsToCSV({ - query: EXPENSE_STATUS_ALL, jsonQuery: '{}', reportIDList: [], transactionIDList: [], diff --git a/tests/unit/SearchParserTest.ts b/tests/unit/SearchParserTest.ts index 8680fd85dbb3..3c83f8698a54 100644 --- a/tests/unit/SearchParserTest.ts +++ b/tests/unit/SearchParserTest.ts @@ -8,7 +8,6 @@ const tests = [ query: parserCommonTests.simple, expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -19,7 +18,6 @@ const tests = [ query: parserCommonTests.userFriendlyNames, expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -58,7 +56,6 @@ const tests = [ query: parserCommonTests.oldNames, expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -97,7 +94,6 @@ const tests = [ query: parserCommonTests.complex, expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -144,7 +140,6 @@ const tests = [ query: parserCommonTests.quotesIOS, expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -159,7 +154,6 @@ const tests = [ query: ',', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -174,7 +168,6 @@ const tests = [ query: 'currency:,', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -189,7 +182,6 @@ const tests = [ query: 'tag:,,travel,', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -204,7 +196,6 @@ const tests = [ query: 'category:', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -219,7 +210,6 @@ const tests = [ query: 'in:123333 currency:USD merchant:marriott', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -250,7 +240,6 @@ const tests = [ query: 'date>2024-01-01 date<2024-06-01 merchant:"McDonald\'s"', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -281,7 +270,6 @@ const tests = [ query: 'from:usera@user.com to:userb@user.com date>2024-01-01', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -312,7 +300,6 @@ const tests = [ query: 'amount>100 amount<200 from:usera@user.com tax-rate:1234 card:1234 report-id:12345 tag:ecx date>2023-01-01', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -383,7 +370,6 @@ const tests = [ query: 'amount>200 las vegas', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -406,18 +392,20 @@ const tests = [ query: 'status:all', expected: { type: 'expense', - status: '', sortBy: 'date', sortOrder: 'desc', view: 'table', - filters: null, + filters: { + operator: 'eq', + left: 'status', + right: 'all', + }, }, }, { query: 'amount>200 las vegas category:"Hotel : Marriott"', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -448,7 +436,6 @@ const tests = [ query: 'amount>200 las vegas category:"Hotel : Marriott" date:2024-01-01,2024-02-01 merchant:"Expensify, Inc." tag:hotel,travel,"meals & entertainment"', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -503,7 +490,6 @@ const tests = [ query: 'type:expense withdrawal-type:expensify-card', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -518,7 +504,6 @@ const tests = [ query: 'type:expense withdrawal-id:1234567890', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -533,7 +518,6 @@ const tests = [ query: 'type:expense withdrawal-status:pending', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -548,7 +532,6 @@ const tests = [ query: 'type:expense withdrawal-status:pending,cleared,failed', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -563,7 +546,6 @@ const tests = [ query: 'type:expense -withdrawal-status:failed', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -578,7 +560,6 @@ const tests = [ query: 'type:expense withdrawn:last-month', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -593,7 +574,6 @@ const tests = [ query: 'type:expense group-by:from', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_FROM, sortOrder: CONST.SEARCH.SORT_ORDER.ASC, view: 'table', @@ -605,7 +585,6 @@ const tests = [ query: 'type:expense group-by:card', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_CARD, sortOrder: CONST.SEARCH.SORT_ORDER.ASC, view: 'table', @@ -617,7 +596,6 @@ const tests = [ query: 'type:expense group-by:withdrawal-id', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_WITHDRAWN, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -629,7 +607,6 @@ const tests = [ query: 'type:expense group-by:category', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_CATEGORY, sortOrder: CONST.SEARCH.SORT_ORDER.ASC, view: 'table', @@ -641,7 +618,6 @@ const tests = [ query: 'type:expense group-by:tag', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_TAG, sortOrder: CONST.SEARCH.SORT_ORDER.ASC, view: 'table', @@ -653,7 +629,6 @@ const tests = [ query: 'type:expense group-by:merchant', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_MERCHANT, sortOrder: CONST.SEARCH.SORT_ORDER.ASC, view: 'table', @@ -665,7 +640,6 @@ const tests = [ query: 'type:expense group-by:month', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -677,7 +651,6 @@ const tests = [ query: 'type:expense group-by:week', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_WEEK, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -689,7 +662,6 @@ const tests = [ query: 'type:expense group-by:year', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_YEAR, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -701,7 +673,6 @@ const tests = [ query: 'type:expense group-by:quarter', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_QUARTER, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -713,7 +684,6 @@ const tests = [ query: 'type:chat is:read', expected: { type: CONST.SEARCH.DATA_TYPES.CHAT, - status: '', sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -728,7 +698,6 @@ const tests = [ query: 'type:chat is:unread', expected: { type: CONST.SEARCH.DATA_TYPES.CHAT, - status: '', sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -743,7 +712,6 @@ const tests = [ query: 'type:chat is:pinned', expected: { type: CONST.SEARCH.DATA_TYPES.CHAT, - status: '', sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -758,7 +726,6 @@ const tests = [ query: 'type:chat is:pinned,read,unread', expected: { type: CONST.SEARCH.DATA_TYPES.CHAT, - status: '', sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -773,7 +740,6 @@ const tests = [ query: 'type:chat has:attachment', expected: { type: CONST.SEARCH.DATA_TYPES.CHAT, - status: '', sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -788,7 +754,6 @@ const tests = [ query: 'type:chat has:link', expected: { type: CONST.SEARCH.DATA_TYPES.CHAT, - status: '', sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -803,7 +768,6 @@ const tests = [ query: 'type:chat has:link,attachment', expected: { type: CONST.SEARCH.DATA_TYPES.CHAT, - status: '', sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -818,7 +782,6 @@ const tests = [ query: 'type:chat is:READ', expected: { type: CONST.SEARCH.DATA_TYPES.CHAT, - status: '', sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -833,7 +796,6 @@ const tests = [ query: 'type:chat is:PINNED', expected: { type: CONST.SEARCH.DATA_TYPES.CHAT, - status: '', sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -885,7 +847,6 @@ const keywordTests = [ query: '" " " "', // Multiple whitespaces wrapped in quotes expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -900,7 +861,6 @@ const keywordTests = [ query: '"https://expensify.com" "https://new.expensify.com"', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -915,7 +875,6 @@ const keywordTests = [ query: '""https://expensify.com"" to ""https://new.expensify.com""', // Nested quotes with a colon expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -930,7 +889,6 @@ const keywordTests = [ query: '"""https://expensify.com" to "https://new.expensify.com"""', // Mismatched quotes expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -945,7 +903,6 @@ const keywordTests = [ query: 'date>2024-01-01 from:usera@user.com "https://expensify.com" "https://new.expensify.com"', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -976,7 +933,6 @@ const keywordTests = [ query: 'from:““Rag” Dog”,"Bag ”Dog“",email@gmail.com,1605423 to:"""Unruly"" “““Glad””” """Dog"""', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -999,7 +955,6 @@ const keywordTests = [ query: 'expense-type:per-diem', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1044,7 +999,6 @@ const keywordTests = [ query: 'columns:per-diem,drafts,draft,tax-rate,policy-name,withdrawal-id,bank-account', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1064,7 +1018,6 @@ const keywordTests = [ query: 'columns:long-report-id,exported-to,exchange-rate,reimbursable-total,non-reimbursable-total', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1082,7 +1035,6 @@ const keywordTests = [ query: 'columns:original-amount,tax,report-id', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1094,7 +1046,6 @@ const keywordTests = [ query: 'columns:group-from,group-expenses,group-total,group-card,group-feed,group-bank-account,group-withdrawn,group-withdrawal-id', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1115,7 +1066,6 @@ const keywordTests = [ query: 'columns:group-category,group-tag,group-merchant,group-month,group-week,group-year,group-quarter', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1135,7 +1085,6 @@ const keywordTests = [ query: 'columns:tax', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1147,7 +1096,6 @@ const keywordTests = [ query: 'merchant:tax', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1162,7 +1110,6 @@ const keywordTests = [ query: 'type:expense action:submit columns:group-bank-account,group-from', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1182,7 +1129,6 @@ const viewAndGroupByTests = [ query: 'type:expense view:bar', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_CATEGORY, sortOrder: 'asc', view: 'bar', @@ -1194,7 +1140,6 @@ const viewAndGroupByTests = [ query: 'type:expense view:table', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1205,11 +1150,14 @@ const viewAndGroupByTests = [ query: 'type:expense status:all', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', - filters: null, + filters: { + operator: 'eq', + left: 'status', + right: 'all', + }, }, }, // view:line defaults to groupBy:month, sortOrder:asc @@ -1217,7 +1165,6 @@ const viewAndGroupByTests = [ query: 'type:expense view:line', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH, sortOrder: 'asc', view: 'line', @@ -1231,7 +1178,6 @@ const viewAndGroupByTests = [ query: 'type:expense groupBy:week', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_WEEK, sortOrder: 'desc', view: 'table', @@ -1243,7 +1189,6 @@ const viewAndGroupByTests = [ query: 'type:expense groupBy:category', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_CATEGORY, sortOrder: 'asc', view: 'table', @@ -1257,7 +1202,6 @@ const viewAndGroupByTests = [ query: 'type:expense view:line groupBy:week', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_WEEK, sortOrder: 'asc', view: 'line', @@ -1271,7 +1215,6 @@ const viewAndGroupByTests = [ query: 'type:expense view:line groupBy:category', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_CATEGORY, sortOrder: 'asc', view: 'line', @@ -1283,7 +1226,6 @@ const viewAndGroupByTests = [ query: 'type:expense view:line groupBy:withdrawal-id', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_WITHDRAWN, sortOrder: 'desc', view: 'line', @@ -1297,7 +1239,6 @@ const viewAndGroupByTests = [ query: 'type:expense view:bar groupBy:week', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_WEEK, sortOrder: 'asc', view: 'bar', @@ -1311,7 +1252,6 @@ const viewAndGroupByTests = [ query: 'type:expense view:line sortOrder:desc', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH, sortOrder: 'desc', view: 'line', @@ -1325,7 +1265,6 @@ const viewAndGroupByTests = [ query: 'sortBy:groupCategory type:expense groupBy:category view:line', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_CATEGORY, sortOrder: 'asc', view: 'line', @@ -1337,7 +1276,6 @@ const viewAndGroupByTests = [ query: 'sortBy:groupmonth type:expense groupBy:month view:bar', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH, sortOrder: 'asc', view: 'bar', @@ -1349,7 +1287,6 @@ const viewAndGroupByTests = [ query: 'sortBy:groupCategory type:expense groupBy:category view:table', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_CATEGORY, sortOrder: 'asc', view: 'table', @@ -1361,7 +1298,6 @@ const viewAndGroupByTests = [ query: 'sortBy:groupmonth type:expense groupBy:month view:table', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH, sortOrder: 'desc', view: 'table', @@ -1377,7 +1313,6 @@ const limitTests = [ query: 'type:expense limit:10', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1390,7 +1325,6 @@ const limitTests = [ query: 'type:expense limit:50 merchant:Amazon', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1407,7 +1341,6 @@ const limitTests = [ query: 'type:expense LIMIT:25', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1420,7 +1353,6 @@ const limitTests = [ query: 'limit:100 category:travel,hotel', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', diff --git a/tests/unit/hooks/useAdvancedSearchFilters.test.ts b/tests/unit/hooks/useAdvancedSearchFilters.test.ts index 6ae232b9aad0..9cb7a784a344 100644 --- a/tests/unit/hooks/useAdvancedSearchFilters.test.ts +++ b/tests/unit/hooks/useAdvancedSearchFilters.test.ts @@ -100,7 +100,7 @@ describe('useAdvancedSearchFilters', () => { const policy = buildPolicy(1, {areCategoriesEnabled: false}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -117,7 +117,7 @@ describe('useAdvancedSearchFilters', () => { }; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}1`, categories); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -137,7 +137,7 @@ describe('useAdvancedSearchFilters', () => { }; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}1`, categories); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -151,7 +151,7 @@ describe('useAdvancedSearchFilters', () => { const policy = buildPolicy(1, {areTagsEnabled: false}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -164,7 +164,7 @@ describe('useAdvancedSearchFilters', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}1`, buildTagList('Engineering')); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -186,7 +186,7 @@ describe('useAdvancedSearchFilters', () => { }; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}1`, emptyTagList); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); // Tag filter is hidden because the tag list contains no actual tags await waitFor(() => { @@ -209,7 +209,7 @@ describe('useAdvancedSearchFilters', () => { }; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}1`, emptyTagList); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, ['1']), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: ['1'], isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -222,7 +222,7 @@ describe('useAdvancedSearchFilters', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}1`, buildTagList('Engineering')); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, ['1']), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: ['1'], isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -236,7 +236,7 @@ describe('useAdvancedSearchFilters', () => { const policy = buildPolicy(1, {tax: {trackingEnabled: false}}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -259,7 +259,7 @@ describe('useAdvancedSearchFilters', () => { }); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -280,7 +280,7 @@ describe('useAdvancedSearchFilters', () => { }); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -294,7 +294,7 @@ describe('useAdvancedSearchFilters', () => { const policy = buildPolicy(1, {type: CONST.POLICY.TYPE.CORPORATE, isAttendeeTrackingEnabled: false}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -308,7 +308,7 @@ describe('useAdvancedSearchFilters', () => { delete (policy as Record).isAttendeeTrackingEnabled; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -320,7 +320,7 @@ describe('useAdvancedSearchFilters', () => { const policy = buildPolicy(1, {type: CONST.POLICY.TYPE.CORPORATE, isAttendeeTrackingEnabled: true}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -334,7 +334,7 @@ describe('useAdvancedSearchFilters', () => { const policy = buildPolicy(1, {fieldList: {}}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -364,7 +364,7 @@ describe('useAdvancedSearchFilters', () => { }); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -394,7 +394,7 @@ describe('useAdvancedSearchFilters', () => { }); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -405,7 +405,7 @@ describe('useAdvancedSearchFilters', () => { describe('bank account filter visibility', () => { it('hides bank account filter when no bank accounts exist', async () => { - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -427,7 +427,7 @@ describe('useAdvancedSearchFilters', () => { }, }); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -449,7 +449,7 @@ describe('useAdvancedSearchFilters', () => { }, }); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -471,7 +471,7 @@ describe('useAdvancedSearchFilters', () => { }, }); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -493,7 +493,7 @@ describe('useAdvancedSearchFilters', () => { }, }); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -515,7 +515,7 @@ describe('useAdvancedSearchFilters', () => { }, }); - const {result} = renderHook(() => useAdvancedSearchFilters(CONST.SEARCH.DATA_TYPES.CHAT, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(CONST.SEARCH.DATA_TYPES.CHAT, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -550,7 +550,7 @@ describe('useAdvancedSearchFilters', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}1`, categories); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}1`, buildTagList('Engineering')); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); diff --git a/tests/unit/hooks/useAutocompleteSuggestions.test.ts b/tests/unit/hooks/useAutocompleteSuggestions.test.ts index df2242609325..e59a14878c5b 100644 --- a/tests/unit/hooks/useAutocompleteSuggestions.test.ts +++ b/tests/unit/hooks/useAutocompleteSuggestions.test.ts @@ -261,7 +261,7 @@ describe('useAutocompleteSuggestions', () => { it('returns status suggestions when autocomplete key is status', () => { parseForAutocomplete.mockReturnValue({ - autocomplete: {key: CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS, value: ''}, + autocomplete: {key: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, value: ''}, ranges: [{key: CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE, value: CONST.SEARCH.DATA_TYPES.EXPENSE, start: 0, length: 12}], }); diff --git a/tests/unit/hooks/useSearchBulkActionsDeleteTest.ts b/tests/unit/hooks/useSearchBulkActionsDeleteTest.ts index e02a8e0ccf85..ebd3ddc334ba 100644 --- a/tests/unit/hooks/useSearchBulkActionsDeleteTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsDeleteTest.ts @@ -238,7 +238,6 @@ const baseQueryJSON: SearchQueryJSON = { similarSearchHash: 12345, flatFilters: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, diff --git a/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts b/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts index efb52ff7befc..0a58c4af8a69 100644 --- a/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts @@ -81,7 +81,6 @@ const expenseReportQueryJSON: SearchQueryJSON = { similarSearchHash: 12345, flatFilters: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - status: CONST.SEARCH.STATUS.EXPENSE_REPORT.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, diff --git a/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts b/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts index 17632c9913c6..9dad10787219 100644 --- a/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts @@ -186,7 +186,6 @@ const baseQueryJSON: SearchQueryJSON = { similarSearchHash: 12345, flatFilters: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, @@ -217,7 +216,6 @@ const expenseReportQueryJSON: SearchQueryJSON = { ...baseQueryJSON, inputQuery: 'type:expense-report status:all', type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - status: CONST.SEARCH.STATUS.EXPENSE_REPORT.ALL, filters: {operator: CONST.SEARCH.SYNTAX_OPERATORS.AND, left: 'type', right: 'expense-report'}, }; diff --git a/tests/unit/hooks/useSearchBulkActionsExportTest.ts b/tests/unit/hooks/useSearchBulkActionsExportTest.ts index b2adbf51eb5b..efba81b7b7fd 100644 --- a/tests/unit/hooks/useSearchBulkActionsExportTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsExportTest.ts @@ -247,7 +247,6 @@ const expenseReportQueryJSON: SearchQueryJSON = { similarSearchHash: 12345, flatFilters: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - status: CONST.SEARCH.STATUS.EXPENSE_REPORT.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, @@ -315,7 +314,7 @@ function makeSearchResults(reports: Report[]): SearchResults { return { search: { type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - status: CONST.SEARCH.STATUS.EXPENSE_REPORT.ALL, + hash: 0, offset: 0, hasMoreResults: false, hasResults: true, diff --git a/tests/unit/hooks/useSearchBulkActionsTest.ts b/tests/unit/hooks/useSearchBulkActionsTest.ts index 42fd028878d7..2e6d24a58125 100644 --- a/tests/unit/hooks/useSearchBulkActionsTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsTest.ts @@ -187,7 +187,6 @@ const baseQueryJSON: SearchQueryJSON = { similarSearchHash: 12345, flatFilters: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, diff --git a/tests/unit/useSearchHighlightAndScrollTest.ts b/tests/unit/useSearchHighlightAndScrollTest.ts index 9a7c3e4680e8..4b2f04bc693f 100644 --- a/tests/unit/useSearchHighlightAndScrollTest.ts +++ b/tests/unit/useSearchHighlightAndScrollTest.ts @@ -42,7 +42,7 @@ describe('useSearchHighlightAndScroll', () => { hasMoreResults: false, hasResults: true, offset: 0, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, type: 'expense', isLoading: false, }, @@ -53,7 +53,6 @@ describe('useSearchHighlightAndScroll', () => { previousReportActions: {}, queryJSON: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', filters: {operator: 'and', left: 'tag', right: ''}, From 58cc820220ce2c8a9f34d310b60e39a286efd0e1 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 15:53:37 +0800 Subject: [PATCH 02/32] make it undefinedable --- .../FilterComponents/CategorySelector.tsx | 21 +++++++++--------- .../FilterComponents/ExportedToSelector.tsx | 4 ++-- .../Search/FilterComponents/TagSelector.tsx | 22 +++++++++---------- .../FilterComponents/TaxRateSelector.tsx | 2 +- .../Search/FilterComponents/index.tsx | 2 +- src/libs/PolicyUtils.ts | 4 ++-- src/libs/SearchQueryUtils.ts | 10 ++++----- tests/unit/PolicyUtilsTest.ts | 6 ++--- 8 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/components/Search/FilterComponents/CategorySelector.tsx b/src/components/Search/FilterComponents/CategorySelector.tsx index 0889c0723593..1e9430a0a3a0 100644 --- a/src/components/Search/FilterComponents/CategorySelector.tsx +++ b/src/components/Search/FilterComponents/CategorySelector.tsx @@ -13,7 +13,7 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; import MultiSelect from './MultiSelect'; type CategorySelectorProps = SearchFilterCommonProps & { - policyID: PolicyIDFilter; + policyID: PolicyIDFilter | undefined; }; function CategorySelector({value = [], policyID, selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: CategorySelectorProps) { @@ -45,22 +45,23 @@ function CategorySelector({value = [], policyID, selectionListTextInputStyle, se }, [availableNonPersonalPolicyCategoriesSelector], ); - const selectedPoliciesCategories: PolicyCategory[] = Object.keys(allPolicyCategories ?? {}) - .filter((key) => { - const isSelected = policyID.value?.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`)?.includes(key); - return policyID.isNegated ? !isSelected : isSelected; - }) - .map((key) => Object.values(allPolicyCategories?.[key] ?? {})) - .flat(); const categoryItems = [{text: translate('search.noCategory'), value: CONST.SEARCH.CATEGORY_EMPTY_VALUE as string}]; const uniqueCategoryNames = new Set(); - if (!policyID.value?.length) { + if (!policyID?.value?.length) { const categories = Object.values(allPolicyCategories ?? {}).flatMap((policyCategories) => Object.values(policyCategories ?? {})); for (const category of categories) { uniqueCategoryNames.add(category.name); } - } else if (selectedPoliciesCategories.length > 0) { + } else { + const selectedPoliciesCategories: PolicyCategory[] = Object.keys(allPolicyCategories ?? {}) + .filter((key) => { + const isSelected = policyID.value?.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`)?.includes(key); + return policyID.isNegated ? !isSelected : isSelected; + }) + .map((key) => Object.values(allPolicyCategories?.[key] ?? {})) + .flat(); + for (const category of selectedPoliciesCategories) { uniqueCategoryNames.add(category.name); } diff --git a/src/components/Search/FilterComponents/ExportedToSelector.tsx b/src/components/Search/FilterComponents/ExportedToSelector.tsx index f924581e99a0..3f3173c380f7 100644 --- a/src/components/Search/FilterComponents/ExportedToSelector.tsx +++ b/src/components/Search/FilterComponents/ExportedToSelector.tsx @@ -22,7 +22,7 @@ import type IconAsset from '@src/types/utils/IconAsset'; import MultiSelect from './MultiSelect'; type ExportedToSelectorProps = SearchFilterCommonProps & { - policyID: PolicyIDFilter; + policyID: PolicyIDFilter | undefined; }; const STANDARD_EXPORT_TEMPLATE_ID_TO_DISPLAY_LABEL: Record = { @@ -91,7 +91,7 @@ function ExportedToSelector({value = [], policyID, selectionListTextInputStyle, }); const usedPickerValueKeys = new Set(connectedIntegrationPickerItems.map((item) => item.value)); - const policiesToLoadTemplatesFrom = policyID.value?.length ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies ?? {}); + const policiesToLoadTemplatesFrom = policyID?.value?.length ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies ?? {}); const exportTemplatesFromPolicies = policiesToLoadTemplatesFrom.flatMap((policy) => getExportTemplates([], {}, translate, policy, false)); const exportTemplatesFromAccount = getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, undefined, true); const allExportTemplates = [...exportTemplatesFromAccount, ...exportTemplatesFromPolicies]; diff --git a/src/components/Search/FilterComponents/TagSelector.tsx b/src/components/Search/FilterComponents/TagSelector.tsx index 98f47c640737..4385be54d23d 100644 --- a/src/components/Search/FilterComponents/TagSelector.tsx +++ b/src/components/Search/FilterComponents/TagSelector.tsx @@ -14,29 +14,29 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; import MultiSelect from './MultiSelect'; type TagSelectorProps = SearchFilterCommonProps & { - policyID: PolicyIDFilter; + policyID: PolicyIDFilter | undefined; }; function TagSelector({value = [], policyID, selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: TagSelectorProps) { const {translate, localeCompare} = useLocalize(); const [allPolicyTagLists = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: passthroughPolicyTagListSelector}); - const selectedPoliciesTagLists = Object.keys(allPolicyTagLists ?? {}) - .filter((key) => { - const isSelected = policyID.value?.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`)?.includes(key); - return policyID.isNegated ? !isSelected : isSelected; - }) - ?.map((key) => getTagNamesFromTagsLists(allPolicyTagLists?.[key] ?? {})) - .flat(); - const tagItems = [{text: translate('search.noTag'), value: CONST.SEARCH.TAG_EMPTY_VALUE as string}]; const uniqueTagNames = new Set(); - if (!policyID.value?.length) { + if (!policyID?.value?.length) { const tagListsUnpacked = Object.values(allPolicyTagLists ?? {}).filter((item) => !!item); for (const tag of tagListsUnpacked.map(getTagNamesFromTagsLists).flat()) { uniqueTagNames.add(tag); } - } else if (selectedPoliciesTagLists.length > 0) { + } else { + const selectedPoliciesTagLists = Object.keys(allPolicyTagLists ?? {}) + .filter((key) => { + const isSelected = policyID.value?.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`)?.includes(key); + return policyID.isNegated ? !isSelected : isSelected; + }) + .map((key) => getTagNamesFromTagsLists(allPolicyTagLists?.[key] ?? {})) + .flat(); + for (const tag of selectedPoliciesTagLists) { uniqueTagNames.add(tag); } diff --git a/src/components/Search/FilterComponents/TaxRateSelector.tsx b/src/components/Search/FilterComponents/TaxRateSelector.tsx index 7b6681423cdd..f5f30d94764c 100644 --- a/src/components/Search/FilterComponents/TaxRateSelector.tsx +++ b/src/components/Search/FilterComponents/TaxRateSelector.tsx @@ -10,7 +10,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import MultiSelect from './MultiSelect'; type TaxRateSelectorProps = SearchFilterCommonProps & { - policyID: PolicyIDFilter; + policyID: PolicyIDFilter | undefined; }; function TaxRateSelector({value = [], policyID, selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: TaxRateSelectorProps) { diff --git a/src/components/Search/FilterComponents/index.tsx b/src/components/Search/FilterComponents/index.tsx index e4af48372e48..8feb08b2b591 100644 --- a/src/components/Search/FilterComponents/index.tsx +++ b/src/components/Search/FilterComponents/index.tsx @@ -141,7 +141,7 @@ function FilterComponents({filterKey, value, type, policyID, selectionListTextIn return ( | undefined, policyID: PolicyIDFilter): Set { +function getConnectedIntegrationNamesForPolicies(policies: OnyxCollection | undefined, policyID: PolicyIDFilter | undefined): Set { if (!policies) { return new Set(); } const connectedIntegrationNames = new Set(); - const hasWorkspaceFilter = !!policyID.value?.length; + const hasWorkspaceFilter = !!policyID?.value?.length; const policiesToCheck = hasWorkspaceFilter ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies); for (const policy of policiesToCheck) { diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 38484f7ffbae..3ccec0b62ebb 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1072,19 +1072,19 @@ function getAllPolicyValues( } function getAllPolicyValuesMap( - {value: policyID, isNegated}: PolicyIDFilter, + policyID: PolicyIDFilter | undefined, key: T, policyData: OnyxCollection, ): OnyxCollection { - if (!policyData || !policyID) { + if (!policyData || !policyID || !policyID.value) { return {}; } - if (isNegated) { + if (policyID.isNegated) { return Object.keys(policyData).reduce( (acc, curr) => { const id = curr.replace(key, ''); - if (!policyID.includes(id) && policyData[curr]) { + if (!policyID.value?.includes(id) && policyData[curr]) { acc[curr] = policyData[curr]; } return acc; @@ -1093,7 +1093,7 @@ function getAllPolicyValuesMap( ); } - return policyID.reduce( + return policyID.value.reduce( (acc, curr) => { if (policyData?.[`${key}${curr}`]) { acc[`${key}${curr}`] = policyData?.[`${key}${curr}`]; diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index 2646b2643b03..3f7dfdb231a0 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -2634,11 +2634,11 @@ describe('PolicyUtils', () => { describe('getConnectedIntegrationNamesForPolicies', () => { it('returns empty Set when policies is undefined', () => { - expect(getConnectedIntegrationNamesForPolicies(undefined, {value: undefined, isNegated: false})).toEqual(new Set()); + expect(getConnectedIntegrationNamesForPolicies(undefined, undefined)).toEqual(new Set()); }); it('returns empty Set when policies is empty object', () => { - expect(getConnectedIntegrationNamesForPolicies({}, {value: undefined, isNegated: false})).toEqual(new Set()); + expect(getConnectedIntegrationNamesForPolicies({}, undefined)).toEqual(new Set()); }); it('returns Set with connection name when policy has verified connection', () => { @@ -2653,7 +2653,7 @@ describe('PolicyUtils', () => { const policies: OnyxCollection = { [`${ONYXKEYS.COLLECTION.POLICY}1`]: policyWithXero, }; - expect(getConnectedIntegrationNamesForPolicies(policies, {value: undefined, isNegated: false})).toEqual(new Set([CONST.POLICY.CONNECTIONS.NAME.XERO])); + expect(getConnectedIntegrationNamesForPolicies(policies, undefined)).toEqual(new Set([CONST.POLICY.CONNECTIONS.NAME.XERO])); }); it('filters by policyIDs when provided', () => { From a607e7ed1f0e2281c3a2ae3de3883f8673fbec87 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 15:53:41 +0800 Subject: [PATCH 03/32] simplify --- .../Search/FilterComponents/WorkspaceSelector.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/Search/FilterComponents/WorkspaceSelector.tsx b/src/components/Search/FilterComponents/WorkspaceSelector.tsx index 69c5f99f9711..cab1d9715cc6 100644 --- a/src/components/Search/FilterComponents/WorkspaceSelector.tsx +++ b/src/components/Search/FilterComponents/WorkspaceSelector.tsx @@ -25,7 +25,7 @@ import type {MultiSelectItem} from './MultiSelect'; type WorkspaceSelectorProps = SearchFilterCommonProps; -function WorkspaceSelector({value, selectionListTextInputStyle, selectionListStyle, autoFocus, ready = true, footer, onChange}: WorkspaceSelectorProps) { +function WorkspaceSelector({value = [], selectionListTextInputStyle, selectionListStyle, autoFocus, ready = true, footer, onChange}: WorkspaceSelectorProps) { const {isOffline} = useNetwork(); const {translate} = useLocalize(); const theme = useTheme(); @@ -43,14 +43,12 @@ function WorkspaceSelector({value, selectionListTextInputStyle, selectionListSty icons: workspace.icons, })); - const policyID = value ?? []; - const updateSelectedItems = (item: ListItem) => { let newValue; if (item.isSelected) { - newValue = policyID.filter((i) => i !== item.keyForList); + newValue = value.filter((i) => i !== item.keyForList); } else { - newValue = [...policyID, item.keyForList]; + newValue = [...value, item.keyForList]; } onChange(newValue); }; @@ -58,7 +56,7 @@ function WorkspaceSelector({value, selectionListTextInputStyle, selectionListSty const listData: ListItem[] = workspaceOptions.map((item) => ({ text: item.text, keyForList: item.value, - isSelected: policyID.includes(item.value), + isSelected: value.includes(item.value), icons: item.icons, })); From 74c6b784d0c0d5291a6e2330c9e95db2d045633b Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 17:14:35 +0800 Subject: [PATCH 04/32] lint and test --- jest/setup.ts | 2 +- .../AdvancedFilters/CommonFilterContent.tsx | 5 +- .../AdvancedFilters/FilterList.tsx | 4 +- .../FilterComponents/CategorySelector.tsx | 7 +-- .../FilterComponents/ExportedToSelector.tsx | 5 +- .../Search/FilterComponents/TagSelector.tsx | 7 +-- .../FilterComponents/TaxRateSelector.tsx | 5 +- .../Search/FilterComponents/index.tsx | 5 +- .../SearchAdvancedFiltersPopup/index.tsx | 3 +- .../Search/hooks/useSearchSnapshot.ts | 1 - .../Search/hooks/useUpdateFilterQuery.tsx | 2 +- src/components/Search/index.tsx | 2 +- src/components/Search/types.ts | 6 ++ src/hooks/useAdvancedSearchFilters.ts | 4 +- src/hooks/useExportedToFilterOptions.ts | 4 +- src/hooks/useSearchBulkActions.ts | 1 - .../ExportSearchItemsToCSVParams.ts | 2 +- src/libs/PolicyUtils.ts | 4 +- src/libs/SearchQueryUtils.ts | 46 +++------------ src/libs/SearchUIUtils.ts | 43 ++++++++------ src/libs/actions/IOU/SearchUpdate.ts | 29 +++++----- src/pages/Search/EmptySearchView.tsx | 6 +- src/pages/home/YourSpendSection/queries.ts | 4 +- src/types/onyx/SearchResults.ts | 2 +- tests/actions/IOU/RequestMoneyTest.ts | 2 +- tests/actions/IOU/SearchUpdateTest.ts | 57 +++++++++++++------ .../HomePage/YourSpendSection/rowStateTest.ts | 3 +- .../YourSpendSection/useYourSpendDataTest.ts | 19 ++++--- tests/unit/PolicyUtilsTest.ts | 2 +- tests/unit/Search/SearchQueryUtilsTest.ts | 44 +------------- .../unit/Search/getSpendOverTimeStateTest.ts | 4 +- .../unit/Search/yourSpendQueryBuildersTest.ts | 12 ++-- tests/unit/SearchActionsTest.ts | 2 - tests/unit/SearchParserTest.ts | 4 -- tests/unit/useSearchHighlightAndScrollTest.ts | 1 - 35 files changed, 155 insertions(+), 194 deletions(-) diff --git a/jest/setup.ts b/jest/setup.ts index 81c4329ea98b..eeafcd179a20 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -96,7 +96,7 @@ if (!isVerbose) { // Make them global to override module-level console calls global.console = { ...console, - log: jest.fn(), + // log: jest.fn(), info: jest.fn(), debug: jest.fn(), warn: jest.fn(), diff --git a/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx index 525fb0224989..5a966a400dc8 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import type {SearchFilterCommonProps} from '@components/Search/types'; -import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; +import type {Filter, SearchFilterCommonProps} from '@components/Search/types'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import type {FilterComponentsProps} from '..'; import FilterComponents from '..'; @@ -8,7 +7,7 @@ import FilterComponents from '..'; type CommonFilterContentProps = SearchFilterCommonProps & { filterKey: FilterComponentsProps['filterKey']; type: SearchDataTypes | undefined; - policyID: PolicyIDFilter; + policyID: Filter; }; function CommonFilterContent({filterKey, value, type, ready, policyID, autoFocus, selectionListTextInputStyle, selectionListStyle, footer, onChange}: CommonFilterContentProps) { diff --git a/src/components/Search/FilterComponents/AdvancedFilters/FilterList.tsx b/src/components/Search/FilterComponents/AdvancedFilters/FilterList.tsx index 1d77f10648c1..2e425d6dac81 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/FilterList.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/FilterList.tsx @@ -13,11 +13,11 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import getButtonState from '@libs/getButtonState'; -import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import {FILTER_VIEW_MAP} from '@libs/SearchUIUtils'; import type {SearchFilter} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; +import type { Filter } from '@components/Search/types'; type ItemCallback = (filter: SearchFilter['key']) => void; type FilterItemCallbacks = { @@ -28,7 +28,7 @@ type FilterItemCallbacks = { type FilterListProps = FilterItemCallbacks & { type: SearchDataTypes | undefined; - policyID: PolicyIDFilter; + policyID: Filter; selectedFilter?: SearchFilter['key']; style?: StyleProp; contentContainerStyle?: StyleProp; diff --git a/src/components/Search/FilterComponents/CategorySelector.tsx b/src/components/Search/FilterComponents/CategorySelector.tsx index 1e9430a0a3a0..aaab4ac95624 100644 --- a/src/components/Search/FilterComponents/CategorySelector.tsx +++ b/src/components/Search/FilterComponents/CategorySelector.tsx @@ -1,11 +1,10 @@ import React from 'react'; import type {OnyxCollection} from 'react-native-onyx'; -import type {SearchFilterCommonProps} from '@components/Search/types'; +import type {Filter, SearchFilterCommonProps} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {getDecodedCategoryName} from '@libs/CategoryUtils'; import {sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils'; -import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyCategories, PolicyCategory} from '@src/types/onyx'; @@ -13,7 +12,7 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; import MultiSelect from './MultiSelect'; type CategorySelectorProps = SearchFilterCommonProps & { - policyID: PolicyIDFilter | undefined; + policyID: Filter | undefined; }; function CategorySelector({value = [], policyID, selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: CategorySelectorProps) { @@ -56,7 +55,7 @@ function CategorySelector({value = [], policyID, selectionListTextInputStyle, se } else { const selectedPoliciesCategories: PolicyCategory[] = Object.keys(allPolicyCategories ?? {}) .filter((key) => { - const isSelected = policyID.value?.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`)?.includes(key); + const isSelected = policyID.value?.map((id) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${id}`)?.includes(key); return policyID.isNegated ? !isSelected : isSelected; }) .map((key) => Object.values(allPolicyCategories?.[key] ?? {})) diff --git a/src/components/Search/FilterComponents/ExportedToSelector.tsx b/src/components/Search/FilterComponents/ExportedToSelector.tsx index 3f3173c380f7..f7f4ce185cf4 100644 --- a/src/components/Search/FilterComponents/ExportedToSelector.tsx +++ b/src/components/Search/FilterComponents/ExportedToSelector.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {View} from 'react-native'; import type {TupleToUnion} from 'type-fest'; import Icon from '@components/Icon'; -import type {SearchFilterCommonProps} from '@components/Search/types'; +import type {Filter, SearchFilterCommonProps} from '@components/Search/types'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -14,7 +14,6 @@ import {getExportTemplates} from '@libs/actions/Search'; import {getConnectedIntegrationNamesForPolicies} from '@libs/PolicyUtils'; import {getIntegrationIcon} from '@libs/ReportUtils'; import {getAllPolicyValues} from '@libs/SearchQueryUtils'; -import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -22,7 +21,7 @@ import type IconAsset from '@src/types/utils/IconAsset'; import MultiSelect from './MultiSelect'; type ExportedToSelectorProps = SearchFilterCommonProps & { - policyID: PolicyIDFilter | undefined; + policyID: Filter | undefined; }; const STANDARD_EXPORT_TEMPLATE_ID_TO_DISPLAY_LABEL: Record = { diff --git a/src/components/Search/FilterComponents/TagSelector.tsx b/src/components/Search/FilterComponents/TagSelector.tsx index 4385be54d23d..9b46c6be3ed5 100644 --- a/src/components/Search/FilterComponents/TagSelector.tsx +++ b/src/components/Search/FilterComponents/TagSelector.tsx @@ -1,11 +1,10 @@ import React from 'react'; import type {OnyxCollection} from 'react-native-onyx'; -import type {SearchFilterCommonProps} from '@components/Search/types'; +import type {Filter, SearchFilterCommonProps} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {getCleanedTagName, getTagNamesFromTagsLists} from '@libs/PolicyUtils'; import {sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils'; -import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import passthroughPolicyTagListSelector from '@src/selectors/PolicyTagList'; @@ -14,7 +13,7 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; import MultiSelect from './MultiSelect'; type TagSelectorProps = SearchFilterCommonProps & { - policyID: PolicyIDFilter | undefined; + policyID: Filter | undefined; }; function TagSelector({value = [], policyID, selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: TagSelectorProps) { @@ -31,7 +30,7 @@ function TagSelector({value = [], policyID, selectionListTextInputStyle, selecti } else { const selectedPoliciesTagLists = Object.keys(allPolicyTagLists ?? {}) .filter((key) => { - const isSelected = policyID.value?.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`)?.includes(key); + const isSelected = policyID.value?.map((id) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${id}`)?.includes(key); return policyID.isNegated ? !isSelected : isSelected; }) .map((key) => getTagNamesFromTagsLists(allPolicyTagLists?.[key] ?? {})) diff --git a/src/components/Search/FilterComponents/TaxRateSelector.tsx b/src/components/Search/FilterComponents/TaxRateSelector.tsx index f5f30d94764c..d412e37b76d5 100644 --- a/src/components/Search/FilterComponents/TaxRateSelector.tsx +++ b/src/components/Search/FilterComponents/TaxRateSelector.tsx @@ -1,16 +1,15 @@ import React from 'react'; -import type {SearchFilterCommonProps} from '@components/Search/types'; +import type {Filter, SearchFilterCommonProps} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {getAllTaxRates} from '@libs/PolicyUtils'; import {getAllPolicyValuesMap} from '@libs/SearchQueryUtils'; -import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import MultiSelect from './MultiSelect'; type TaxRateSelectorProps = SearchFilterCommonProps & { - policyID: PolicyIDFilter | undefined; + policyID: Filter | undefined; }; function TaxRateSelector({value = [], policyID, selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: TaxRateSelectorProps) { diff --git a/src/components/Search/FilterComponents/index.tsx b/src/components/Search/FilterComponents/index.tsx index 8feb08b2b591..802cd8baa425 100644 --- a/src/components/Search/FilterComponents/index.tsx +++ b/src/components/Search/FilterComponents/index.tsx @@ -1,11 +1,10 @@ import React from 'react'; -import type {SearchAmountFilterKeys, SearchDateFilterKeys, SearchFilterCommonProps} from '@components/Search/types'; +import type {Filter, SearchAmountFilterKeys, SearchDateFilterKeys, SearchFilterCommonProps} from '@components/Search/types'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import {FILTER_VIEW_MAP, getMultiSelectFilterOptions, getSingleSelectFilterOptions} from '@libs/SearchUIUtils'; import type {SearchFilter} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; @@ -30,7 +29,7 @@ type FilterKeys = Exclude & { filterKey: FilterKeys; type?: SearchDataTypes; - policyID: PolicyIDFilter | undefined; + policyID: Filter | undefined; }; type TextInputFilterComponentsProps = { diff --git a/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/index.tsx b/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/index.tsx index ee1d7472f23f..5e08661c30b0 100644 --- a/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/index.tsx +++ b/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/index.tsx @@ -9,7 +9,8 @@ import useOnyx from '@hooks/useOnyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {getFilterNegatableValue, type SearchFilter} from '@libs/SearchUIUtils'; +import {getFilterNegatableValue} from '@libs/SearchUIUtils'; +import type {SearchFilter} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import AmountFilterContentPopupWrapper from './AmountFilterContentPopupWrapper'; diff --git a/src/components/Search/hooks/useSearchSnapshot.ts b/src/components/Search/hooks/useSearchSnapshot.ts index 6fd0b5b39c7f..09c3bdbf1bcc 100644 --- a/src/components/Search/hooks/useSearchSnapshot.ts +++ b/src/components/Search/hooks/useSearchSnapshot.ts @@ -332,7 +332,6 @@ function useSearchSnapshot({queryJSON, searchResults, newSearchResultKeys, trans }, [ shouldComputeSections, type, - status, filteredData, localeCompare, translate, diff --git a/src/components/Search/hooks/useUpdateFilterQuery.tsx b/src/components/Search/hooks/useUpdateFilterQuery.tsx index 10d8b6da4c19..a84a9226b59a 100644 --- a/src/components/Search/hooks/useUpdateFilterQuery.tsx +++ b/src/components/Search/hooks/useUpdateFilterQuery.tsx @@ -21,7 +21,7 @@ function useUpdateFilterQuery(queryJSON: SearchQueryJSON | undefined) { if (updatedFilterFormValues.type !== currentValues.type) { updatedFilterFormValues.columns = []; - updatedFilterFormValues.status = CONST.SEARCH.STATUS.EXPENSE.ALL; + updatedFilterFormValues.status = undefined; updatedFilterFormValues.has = filterValidHasValues(updatedFilterFormValues.has, updatedFilterFormValues.type, translate); } diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 5049198ba4f9..21f83b35b9f6 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -241,7 +241,7 @@ function Search({ const isDataLoaded = shouldUseLiveData || isSearchDataLoaded(searchResults, queryJSON); const hasErrors = Object.keys(searchResults?.errors ?? {}).length > 0 && !isOffline; - +console.log('query json', queryJSON) const deferHeavySearchWork = useCallback( (useDoubleFrame = false) => { setShouldDeferHeavySearchWork(true); diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 482824411e01..9db6f017ed19 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -276,6 +276,11 @@ type QueryFilter = { value: string | number; }; +type Filter = { + value: string[] | undefined; + isNegated: boolean; +}; + // Report fields are dynamic keys, that policies can configure. They match: // reportField- : Normal report field // reportField- : Report field with a modifier, such as On, After, Before, Not, so that we can handle Dates and negation @@ -454,6 +459,7 @@ export type { SearchRowSelectionActionsValue, ASTNode, QueryFilter, + Filter, QueryFilters, SyntaxFilterKey, RawQueryFilter, diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts index d6df0d9092b1..3ffe3010a894 100644 --- a/src/hooks/useAdvancedSearchFilters.ts +++ b/src/hooks/useAdvancedSearchFilters.ts @@ -5,7 +5,6 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {isFilterableBankAccount} from '@libs/BankAccountUtils'; import {isPolicyFeatureEnabled} from '@libs/PolicyUtils'; import {getAllPolicyValues} from '@libs/SearchQueryUtils'; -import type {PolicyIDFilter} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CardList, Policy, PolicyCategories, PolicyTagLists} from '@src/types/onyx'; @@ -14,6 +13,7 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; import useLocalize from './useLocalize'; import useOnyx from './useOnyx'; import useWorkspaceList from './useWorkspaceList'; +import type { Filter } from '@components/Search/types'; /** * typeFiltersKeys is stored as an object keyed by the different search types. @@ -314,7 +314,7 @@ function shouldDisplayCardFilterSelector(cardList: OnyxEntry) { return shouldDisplayFilter(Object.keys(filterCardsHiddenFromSearch(cardList)).length, true); } -function useAdvancedSearchFilters(type: SearchDataTypes | undefined, policyID: PolicyIDFilter) { +function useAdvancedSearchFilters(type: SearchDataTypes | undefined, policyID: Filter) { const [shouldDisplayCardFilter] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {selector: shouldDisplayCardFilterSelector}); const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: advancedSearchPoliciesSelector}); const [policyDerived] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policyDerivedSelector}); diff --git a/src/hooks/useExportedToFilterOptions.ts b/src/hooks/useExportedToFilterOptions.ts index b35ed8ccabab..c5f5a7661876 100644 --- a/src/hooks/useExportedToFilterOptions.ts +++ b/src/hooks/useExportedToFilterOptions.ts @@ -3,7 +3,7 @@ import {useSearchQueryContext} from '@components/Search/SearchContext'; import {getStandardExportTemplateDisplayName} from '@libs/AccountingUtils'; import {getExportTemplates} from '@libs/actions/Search'; import {getConnectedIntegrationNamesForPolicies} from '@libs/PolicyUtils'; -import {getAllPolicyValues, getPolicyIDFromQuery} from '@libs/SearchQueryUtils'; +import {getAllPolicyValues, getFilterFromQuery} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ExportTemplate, Policy} from '@src/types/onyx'; @@ -41,7 +41,7 @@ function exportedToPoliciesSelector(policies: OnyxCollection): OnyxColle */ export default function useExportedToFilterOptions(): UseExportedToFilterDataResult { const {currentSearchQueryJSON} = useSearchQueryContext(); - const policyIDs = getPolicyIDFromQuery(currentSearchQueryJSON); + const policyIDs = getFilterFromQuery(currentSearchQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); const {translate} = useLocalize(); const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES); diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 517077e39744..329b980e01de 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -2134,7 +2134,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { return options; }, [ selectedTransactionsKeys, - status, hash, selectedTransactions, queryJSON?.type, diff --git a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts index 38c627fbe0af..b1ab748a25d2 100644 --- a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts +++ b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts @@ -1,4 +1,4 @@ -import type {SearchQueryString, SearchStatus} from '@components/Search/types'; +import type {SearchQueryString} from '@components/Search/types'; type ExportSearchItemsToCSVParams = { jsonQuery: SearchQueryString; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 00033d542c44..2b478a20593b 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -54,10 +54,10 @@ import {formatMemberForList} from './OptionsListUtils'; import type {MemberForList} from './OptionsListUtils'; import {getAccountIDsByLogins, getKnownAccountIDByLogin, getLoginsByAccountIDs, getPersonalDetailByEmail} from './PersonalDetailsUtils'; import {getAllPolicyValues} from './SearchQueryUtils'; -import type {PolicyIDFilter} from './SearchQueryUtils'; import {getAllSortedTransactions, getCategory, getTag, getTagArrayFromName} from './TransactionUtils'; import {generateAccountID} from './UserUtils'; import {isPublicDomain, isValidAccountRoute} from './ValidationUtils'; +import { Filter } from '@components/Search/types'; type MemberEmailsToAccountIDs = Record; @@ -2335,7 +2335,7 @@ function getValidConnectedIntegration(policy: Policy | undefined, connectionName * @param policies - Collection of policies to get connected integrations. * @param policyIDs - Policy IDs to filter by. When provided, only integrations from these policies are included. */ -function getConnectedIntegrationNamesForPolicies(policies: OnyxCollection | undefined, policyID: PolicyIDFilter | undefined): Set { +function getConnectedIntegrationNamesForPolicies(policies: OnyxCollection | undefined, policyID: Filter | undefined): Set { if (!policies) { return new Set(); } diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 3ccec0b62ebb..918f684ae83e 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -6,6 +6,7 @@ import type {ValueOf} from 'type-fest'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type { ASTNode, + Filter, QueryFilter, QueryFilters, RawQueryFilter, @@ -21,7 +22,6 @@ import type { SearchQueryString, SearchStatus, SearchWithdrawalType, - SingularSearchStatus, SyntaxFilterKey, UserFriendlyKey, UserFriendlyValue, @@ -62,7 +62,6 @@ import {getDisplayNameOrDefault, getPersonalDetailByEmail} from './PersonalDetai import {getCleanedTagName} from './PolicyUtils'; import {getReportName} from './ReportNameUtils'; import {parse as parseSearchQuery} from './SearchParser/searchParser'; -import {getStatusOptions} from './SearchUIUtils'; import StringUtils from './StringUtils'; import {hashText} from './UserUtils'; import {isValidDate} from './ValidationUtils'; @@ -428,35 +427,8 @@ function getFilters(queryJSON: SearchQueryJSON) { return filters; } -function getStatusFromQuery(queryJSON: SearchQueryJSON | undefined): SearchStatus { - const statusFilters = queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS)?.filters; - if (!statusFilters) { - return CONST.SEARCH.STATUS.EXPENSE.ALL; - } - - const isNegated = statusFilters.at(0)?.operator === CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO; - const values = statusFilters.map((filter) => filter.value) as SearchStatus; - - if (!isNegated) { - return values; - } - - const statusOptions = getStatusOptions(() => '', queryJSON.type); - return statusOptions.reduce((acc, curr) => { - if (!values.includes(curr.value)) { - acc.push(curr.value); - } - return acc; - }, [] as SingularSearchStatus[]); -} - -type PolicyIDFilter = { - value: string[] | undefined; - isNegated: boolean; -}; - -function getPolicyIDFromQuery(queryJSON: SearchQueryJSON | undefined): PolicyIDFilter { - const policyIDFilters = queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID)?.filters; +function getFilterFromQuery(queryJSON: SearchQueryJSON | undefined, filterKey: SearchAdvancedFiltersKey): Filter { + const policyIDFilters = queryJSON?.flatFilters.find((filter) => filter.key === filterKey)?.filters; const isNegated = policyIDFilters?.at(0)?.operator === CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO; const value = policyIDFilters?.map((filter) => filter.value.toString()); @@ -1047,7 +1019,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial( - {value: policyID, isNegated}: PolicyIDFilter, + {value: policyID, isNegated}: Filter, key: T, policyData: OnyxCollection, ): Array { @@ -1072,7 +1044,7 @@ function getAllPolicyValues( } function getAllPolicyValuesMap( - policyID: PolicyIDFilter | undefined, + policyID: Filter | undefined, key: T, policyData: OnyxCollection, ): OnyxCollection { @@ -1177,7 +1149,7 @@ function buildFilterFormValuesFromQuery( ) { const filters = queryJSON.flatFilters; const filtersForm = {} as Partial; - const policyID = getPolicyIDFromQuery(queryJSON); + const policyID = getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); // Pre-compute dynamic validation Sets once (avoids recreating per filter iteration) const validCurrencies = new Set(Object.keys(currencyList)); @@ -2463,11 +2435,11 @@ export { getParamsState, getRoutes, isSearchRootParams, - getStatusFromQuery, - getPolicyIDFromQuery, + getFilterFromQuery, isFilterNegatable, + getQueryHashes, }; export type {BuildUserReadableQueryStringParams}; -export type {SearchDateValues, PolicyIDFilter}; +export type {SearchDateValues}; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index cfcae957457e..97fcd42d25c0 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -8,7 +8,7 @@ import type {CurrencyListActionsContextType} from '@components/CurrencyListConte import type {ExpensifyIconName} from '@components/Icon/ExpensifyIconLoader'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type {MenuItemWithLink} from '@components/MenuItemList'; -import {FilterComponentsProps} from '@components/Search/FilterComponents'; +import type {FilterComponentsProps} from '@components/Search/FilterComponents'; import type {MultiSelectItem} from '@components/Search/FilterComponents/MultiSelect'; import type {SingleSelectItem} from '@components/Search/FilterComponents/SingleSelect'; import ChatListItem from '@components/Search/SearchList/ListItem/ChatListItem'; @@ -177,7 +177,7 @@ import { getDateFilterKeys, getDateRangeDisplayValueFromFormValue, getDateRangeForPreset, - getStatusFromQuery, + getFilterFromQuery, isFilterNegatable, isFilterSupported, isSearchDatePreset, @@ -2097,13 +2097,18 @@ function getTransactionsSections({ const isTrackedOptimisticItem = !!optimisticTransactionID && transactionItem.transactionID === optimisticTransactionID; if (currentQueryJSON && !isActionLoading && !isTrackedOptimisticItem) { if (currentQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) { - const status = getStatusFromQuery(currentQueryJSON); - if (Array.isArray(status)) { - shouldShow = status.some((expenseStatus) => { - return isValidExpenseStatus(expenseStatus) ? expenseStatusActionMapping[expenseStatus](report, transactionItem.reportID) : false; - }); - } else { - shouldShow = isValidExpenseStatus(status) ? expenseStatusActionMapping[status](report, transactionItem.reportID) : false; + const status = getFilterFromQuery(currentQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS); + if (status.value) { + if (status.isNegated) { + shouldShow = Object.keys(expenseStatusActionMapping).some((expenseStatus) => { + const isExcluded = status.value?.includes(expenseStatus); + return !isExcluded && expenseStatusActionMapping[expenseStatus](report, transactionItem.reportID); + }); + } else { + shouldShow = status.value.some((expenseStatus) => { + return isValidExpenseStatus(expenseStatus) ? expenseStatusActionMapping[expenseStatus](report, transactionItem.reportID) : false; + }); + } } } } @@ -2821,14 +2826,18 @@ function getReportSections({ const isActionLoading = isActionLoadingSet?.has(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportItem.reportID}`); if (currentQueryJSON && !isActionLoading) { if (currentQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) { - const status = getStatusFromQuery(currentQueryJSON); - - if (Array.isArray(status)) { - shouldShow = status.some((expenseStatus) => { - return isValidExpenseStatus(expenseStatus) ? expenseStatusActionMapping[expenseStatus](reportItem) : false; - }); - } else { - shouldShow = isValidExpenseStatus(status) ? expenseStatusActionMapping[status](reportItem) : false; + const status = getFilterFromQuery(currentQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS); + if (status.value) { + if (status.isNegated) { + shouldShow = Object.keys(expenseStatusActionMapping).some((expenseStatus) => { + const isExcluded = status.value?.includes(expenseStatus); + return !isExcluded && expenseStatusActionMapping[expenseStatus](reportItem); + }); + } else { + shouldShow = status.value.some((expenseStatus) => { + return isValidExpenseStatus(expenseStatus) ? expenseStatusActionMapping[expenseStatus](reportItem) : false; + }); + } } } } diff --git a/src/libs/actions/IOU/SearchUpdate.ts b/src/libs/actions/IOU/SearchUpdate.ts index 4416aaebf9f6..aed70a57c1b0 100644 --- a/src/libs/actions/IOU/SearchUpdate.ts +++ b/src/libs/actions/IOU/SearchUpdate.ts @@ -1,9 +1,8 @@ import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import type {SearchQueryJSON} from '@components/Search/types'; import {isExpenseReport, isOptimisticPersonalDetail} from '@libs/ReportUtils'; -import {buildSearchQueryJSON, buildSearchQueryString, getCurrentSearchQueryJSON, getPolicyIDFromQuery, getStatusFromQuery} from '@libs/SearchQueryUtils'; +import {buildSearchQueryJSON, buildSearchQueryString, getCurrentSearchQueryJSON, getFilterFromQuery} from '@libs/SearchQueryUtils'; import {getSuggestedSearches} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -54,20 +53,23 @@ function shouldOptimisticallyUpdateSearch( ) { return false; } - let shouldOptimisticallyUpdateByStatus; - const status = getStatusFromQuery(currentSearchQueryJSON); + let shouldOptimisticallyUpdateByStatus = true; + const status = getFilterFromQuery(currentSearchQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS); const transactionReportID = transaction?.reportID; - if (Array.isArray(status)) { - shouldOptimisticallyUpdateByStatus = status.some((val) => { - const expenseStatus = val as ValueOf; - return expenseReportStatusFilterMapping[expenseStatus](iouReport, transactionReportID); - }); - } else { - const expenseStatus = status as ValueOf; - shouldOptimisticallyUpdateByStatus = expenseReportStatusFilterMapping[expenseStatus](iouReport, transactionReportID); + if (status.value) { + if (status.isNegated) { + shouldOptimisticallyUpdateByStatus = Object.keys(expenseReportStatusFilterMapping).some((val) => { + const isExcluded = status.value?.includes(val); + return !isExcluded && expenseReportStatusFilterMapping[val](iouReport, transactionReportID); + }); + } else { + shouldOptimisticallyUpdateByStatus = status.value.some((val) => { + return expenseReportStatusFilterMapping[val](iouReport, transactionReportID); + }); + } } - const currentSearchPolicyIDs = getPolicyIDFromQuery(currentSearchQueryJSON); + const currentSearchPolicyIDs = getFilterFromQuery(currentSearchQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); if (currentSearchPolicyIDs.value?.length && iouReport?.policyID) { if (!currentSearchPolicyIDs.isNegated && !currentSearchPolicyIDs.value.includes(iouReport.policyID)) { return false; @@ -102,6 +104,7 @@ function shouldOptimisticallyUpdateSearch( const matchesSubmitQuery = submitQueryJSON?.similarSearchHash === currentSearchQueryJSON.similarSearchHash && expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.DRAFTS](iouReport); + console.log('query json hash', unapprovedCashSimilarSearchHash) const matchesApproveQuery = approveQueryJSON?.similarSearchHash === currentSearchQueryJSON.similarSearchHash && expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING](iouReport); diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index c5491bf60ca7..975b3bd3fe4b 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -28,7 +28,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {canSendInvoice, getDefaultChatEnabledPolicy, getGroupPoliciesWhereReportCanBeCreated} from '@libs/PolicyUtils'; import {generateReportID, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; -import {getAllPolicyValues, getPolicyIDFromQuery, isDefaultExpenseReportsQuery, isDefaultExpensesQuery} from '@libs/SearchQueryUtils'; +import {getAllPolicyValues, getFilterFromQuery, isDefaultExpenseReportsQuery, isDefaultExpensesQuery} from '@libs/SearchQueryUtils'; import type {SearchTypeMenuSection} from '@libs/SearchUIUtils'; import {TODO_SEARCH_KEYS} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; @@ -145,10 +145,10 @@ function EmptySearchViewContent({ const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy); - const filteredPolicyID = getPolicyIDFromQuery(queryJSON); + const filteredPolicyID = getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); let isFilteredWorkspaceAccessible = true; if (filteredPolicyID.value) { - const policyIDToCheck = getAllPolicyValues(filteredPolicyID, ONYXKEYS.COLLECTION.POLICY, allPolicies).at(0); + const policyIDToCheck = getAllPolicyValues(filteredPolicyID, ONYXKEYS.COLLECTION.POLICY, allPolicies).at(0)?.id; const filteredPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyIDToCheck}`]; isFilteredWorkspaceAccessible = !!filteredPolicy; } diff --git a/src/pages/home/YourSpendSection/queries.ts b/src/pages/home/YourSpendSection/queries.ts index 9f913c310e0d..dd241b7c6036 100644 --- a/src/pages/home/YourSpendSection/queries.ts +++ b/src/pages/home/YourSpendSection/queries.ts @@ -14,7 +14,7 @@ function get30DaysAgoDateString(): string { function buildAwaitingApprovalQuery(accountID: number, policyIDs: string[]): string { return buildQueryStringFromFilterFormValues({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING, + status: [CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], from: [String(accountID)], reimbursable: CONST.SEARCH.BOOLEAN.YES, // Limit to the user's workspaces so IOU and personal expenses aren't counted. @@ -25,7 +25,7 @@ function buildAwaitingApprovalQuery(accountID: number, policyIDs: string[]): str function buildRepaidLast30DaysQuery(accountID: number): string { return buildQueryStringFromFilterFormValues({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.PAID, + status: [CONST.SEARCH.STATUS.EXPENSE.PAID], from: [String(accountID)], reimbursable: CONST.SEARCH.BOOLEAN.YES, [FILTER_KEYS.DATE_AFTER]: get30DaysAgoDateString(), diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index fc725c36196d..cf0b188e2ec5 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -11,7 +11,7 @@ import type { TransactionListItemType, TransactionReportGroupListItemType, } from '@components/Search/SearchList/ListItem/types'; -import type {SearchGroupBy, SearchStatus} from '@components/Search/types'; +import type {SearchGroupBy} from '@components/Search/types'; import type CONST from '@src/CONST'; import type ONYXKEYS from '@src/ONYXKEYS'; import type PrefixedRecord from '@src/types/utils/PrefixedRecord'; diff --git a/tests/actions/IOU/RequestMoneyTest.ts b/tests/actions/IOU/RequestMoneyTest.ts index abbb177f77da..358239ce90c7 100644 --- a/tests/actions/IOU/RequestMoneyTest.ts +++ b/tests/actions/IOU/RequestMoneyTest.ts @@ -2,7 +2,7 @@ import {format} from 'date-fns'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import type {SearchQueryJSON, SearchStatus} from '@components/Search/types'; +import type {SearchQueryJSON} from '@components/Search/types'; import {clearAllRelatedReportActionErrors} from '@libs/actions/ClearReportActionErrors'; import {requestMoney, trackExpense} from '@libs/actions/IOU/TrackExpense'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; diff --git a/tests/actions/IOU/SearchUpdateTest.ts b/tests/actions/IOU/SearchUpdateTest.ts index adfd517ddc44..281167dff39b 100644 --- a/tests/actions/IOU/SearchUpdateTest.ts +++ b/tests/actions/IOU/SearchUpdateTest.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import type {SearchQueryJSON, SearchStatus} from '@components/Search/types'; +import type {SearchQueryJSON} from '@components/Search/types'; import '@libs/actions/IOU/MoneyRequest'; import {shouldOptimisticallyUpdateSearch} from '@libs/actions/IOU/SearchUpdate'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; @@ -16,6 +16,7 @@ import {createRandomReport} from '../../utils/collections/reports'; import createRandomTransaction from '../../utils/collections/transaction'; import {getGlobalFetchMock} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; +import { getQueryHashes } from '@libs/SearchQueryUtils'; const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ @@ -171,9 +172,9 @@ describe('actions/IOU', () => { ], }, ], - hash: 1920151829, - recentSearchHash: 2100977843, - similarSearchHash: 1855682507, + hash: 344995086, + recentSearchHash: 1106848141, + similarSearchHash: 1135147670, } as SearchQueryJSON; const iouReport: Report = {...createRandomReport(2, undefined), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN}; @@ -203,7 +204,7 @@ describe('actions/IOU', () => { }, right: { operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, - left: 'from', + left: 'to', right: '20671314', }, }, @@ -218,7 +219,7 @@ describe('actions/IOU', () => { ], }, { - key: CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM, + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.TO, filters: [ { operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, @@ -227,11 +228,10 @@ describe('actions/IOU', () => { ], }, ], - - hash: 1510971479, + hash: 1343227670, inputQuery: 'sortBy:date sortOrder:desc type:expense-report action:approve to:20671314', - recentSearchHash: 967911777, - similarSearchHash: 1539858783, + recentSearchHash: 1216776837, + similarSearchHash: 911924256, } as SearchQueryJSON; const iouReport: Report = {...createRandomReport(2, undefined), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN}; @@ -250,16 +250,37 @@ describe('actions/IOU', () => { reimbursable: true, }; const currentSearchQueryJSON = { - type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, + type: CONST.SEARCH.DATA_TYPES.EXPENSE, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + groupBy: CONST.SEARCH.GROUP_BY.FROM, filters: { - operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, - left: 'reimbursable', - - right: 'yes', + operator: CONST.SEARCH.SYNTAX_OPERATORS.AND, + left: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + left: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, + right: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], + }, + right: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + left: 'reimbursable', + right: 'yes', + }, }, flatFilters: [ + { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, + filters: [ + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + value: CONST.SEARCH.STATUS.EXPENSE.DRAFTS, + }, + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + value: CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING, + }, + ], + }, { key: CONST.SEARCH.SYNTAX_FILTER_KEYS.REIMBURSABLE, filters: [ @@ -270,10 +291,10 @@ describe('actions/IOU', () => { ], }, ], - hash: 71801560, + hash: 280939045, inputQuery: 'sortBy:date sortOrder:desc type:expense groupBy:from status:drafts,outstanding reimbursable:yes', - recentSearchHash: 1043581824, - similarSearchHash: 1832274510, + recentSearchHash: 2045056096, + similarSearchHash: 1931622284, } as SearchQueryJSON; const iouReport: Report = {...createRandomReport(2, undefined), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN}; diff --git a/tests/unit/HomePage/YourSpendSection/rowStateTest.ts b/tests/unit/HomePage/YourSpendSection/rowStateTest.ts index 48fe0fc25f21..074cd0ff4b0e 100644 --- a/tests/unit/HomePage/YourSpendSection/rowStateTest.ts +++ b/tests/unit/HomePage/YourSpendSection/rowStateTest.ts @@ -8,6 +8,7 @@ const makeSearchResults = (overrides: Partial = {}): SearchResult ({ search: { offset: 0, + hash: 0, type: 'expense', hasMoreResults: false, hasResults: true, @@ -16,7 +17,7 @@ const makeSearchResults = (overrides: Partial = {}): SearchResult }, data: {}, ...overrides, - }) as SearchResults; + }); const makeSearchResultsWithCount = (count: number): SearchResults => makeSearchResults({ diff --git a/tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts b/tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts index 54b1736eb351..1a900dbee8dc 100644 --- a/tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts +++ b/tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts @@ -137,13 +137,14 @@ function makeSearchResultsWithCount(count: number): SearchResults { search: { type: 'expense', offset: 0, + hash: 0, hasMoreResults: false, hasResults: count > 0, isLoading: false, count, }, data: {}, - } as SearchResults; + }; } /** Populates onyxData with a single-entry policies collection. */ @@ -586,17 +587,17 @@ describe('useYourSpendData — third-party cardRows', () => { mockedGetDisplayableThirdPartyCards.mockReturnValue(makeThirdPartyCards([{cardID: THIRD_PARTY_CARD_ID_1, lastFourPAN: THIRD_PARTY_LAST_FOUR_1}])); // First render: READY snapshot with count > 0 → row produced and total cached. setupCardSnapshot(THIRD_PARTY_CARD_ID_1, { - search: {type: 'expense', offset: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: 3, total: 1234, currency: 'USD'}, + search: {type: 'expense', offset: 0, hash: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: 3, total: 1234, currency: 'USD'}, data: {}, - } as SearchResults); + }); const {result, rerender} = renderHook(() => useYourSpendData()); expect(result.current.cardRows.at(0)?.total).toBe(1234); // Search screen wipes count/total/currency on the shared snapshot. setupCardSnapshot(THIRD_PARTY_CARD_ID_1, { - search: {type: 'expense', status: '', offset: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: undefined, total: undefined, currency: undefined}, + search: {type: 'expense', offset: 0, hash: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: undefined, total: undefined, currency: undefined}, data: {}, - } as unknown as SearchResults); + }); rerender(undefined); // Cached total/currency must survive the wipe so the row stays. expect(result.current.cardRows).toHaveLength(1); @@ -626,13 +627,13 @@ describe('useYourSpendData — third-party cardRows', () => { ]), ); setupCardSnapshot(THIRD_PARTY_CARD_ID_1, { - search: {type: 'expense', offset: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: 2, total: 500, currency: 'USD'}, + search: {type: 'expense', offset: 0, hash: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: 2, total: 500, currency: 'USD'}, data: {}, - } as SearchResults); + }); setupCardSnapshot(THIRD_PARTY_CARD_ID_2, { - search: {type: 'expense', offset: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: 3, total: 2200, currency: 'EUR'}, + search: {type: 'expense', offset: 0, hash: 0, hasMoreResults: false, hasResults: true, isLoading: false, count: 3, total: 2200, currency: 'EUR'}, data: {}, - } as SearchResults); + }); const {result} = renderHook(() => useYourSpendData()); expect(result.current.cardRows).toHaveLength(2); const [r1, r2] = result.current.cardRows; diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index 3f7dfdb231a0..0d7d4956ed09 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -2695,7 +2695,7 @@ describe('PolicyUtils', () => { [`${ONYXKEYS.COLLECTION.POLICY}1`]: policy1, [`${ONYXKEYS.COLLECTION.POLICY}2`]: policy2, }; - const result = getConnectedIntegrationNamesForPolicies(policies, {value: ['1'], isNegated: false}); + const result = getConnectedIntegrationNamesForPolicies(policies, undefined); expect(result).toContain(CONST.POLICY.CONNECTIONS.NAME.QBO); expect(result).toContain(CONST.POLICY.CONNECTIONS.NAME.XERO); expect(result.size).toBe(2); diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 89a63851b0a7..8a26c9c82f3e 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -20,13 +20,13 @@ import { getDateRangeDisplayValueFromFormValue, getDisplayQueryFiltersForKey, getFilterDisplayValue, + getFilterFromQuery, getKeywordQueryWithCurrentSearchContext, getLastRouteByName, getParamsState, getQueryWithUpdatedValues, getRangeBoundariesFromFormValue, getRoutes, - getStatusFromQuery, isSearchRootParams, serializeQueryJSONForBackend, shouldHighlight, @@ -373,7 +373,6 @@ describe('SearchQueryUtils', () => { test('simple filter value', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, policyID: ['12345'], amountLessThan: '100', }; @@ -386,7 +385,6 @@ describe('SearchQueryUtils', () => { test('receipt type filter value', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, receiptType: ['ereceipt', 'hotel'], }; @@ -398,7 +396,6 @@ describe('SearchQueryUtils', () => { test('negated receipt type filter value', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, receiptTypeNot: ['hotel'], }; @@ -420,7 +417,6 @@ describe('SearchQueryUtils', () => { test('with keywords', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, policyID: ['67890'], merchant: 'Amazon', description: 'Electronics', @@ -436,7 +432,6 @@ describe('SearchQueryUtils', () => { test('currencies and categories', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, category: ['services', 'consulting'], currency: ['USD', 'EUR'], }; @@ -449,7 +444,6 @@ describe('SearchQueryUtils', () => { test('has empty category values', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, category: ['equipment', 'consulting', 'none,Uncategorized'], }; @@ -461,7 +455,6 @@ describe('SearchQueryUtils', () => { test('serializes No Tag filter as missing tag query', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, tag: [CONST.SEARCH.TAG_EMPTY_VALUE], }; @@ -474,7 +467,6 @@ describe('SearchQueryUtils', () => { test('serializes real tag values as tag filters', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, tag: ['Engineering'], }; @@ -633,7 +625,6 @@ describe('SearchQueryUtils', () => { test('with withdrawal type filter', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, withdrawalType: CONST.SEARCH.WITHDRAWAL_TYPE.EXPENSIFY_CARD, }; @@ -645,7 +636,6 @@ describe('SearchQueryUtils', () => { test('with single withdrawal status filter', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, withdrawalStatus: [CONST.SEARCH.SETTLEMENT_STATUS.PENDING], }; @@ -657,7 +647,6 @@ describe('SearchQueryUtils', () => { test('with multi-value withdrawal status filter', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, withdrawalStatus: [CONST.SEARCH.SETTLEMENT_STATUS.PENDING, CONST.SEARCH.SETTLEMENT_STATUS.CLEARED, CONST.SEARCH.SETTLEMENT_STATUS.FAILED], }; @@ -669,7 +658,6 @@ describe('SearchQueryUtils', () => { test('with withdrawn filter', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, withdrawnOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, }; @@ -1103,7 +1091,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, category: ['Maintenance', 'none'], }); }); @@ -1128,7 +1115,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, action: 'submit', }); @@ -1144,7 +1130,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, action: undefined, }); }); @@ -1169,7 +1154,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, withdrawalStatus: [CONST.SEARCH.SETTLEMENT_STATUS.PENDING, CONST.SEARCH.SETTLEMENT_STATUS.CLEARED, CONST.SEARCH.SETTLEMENT_STATUS.FAILED], }); @@ -1185,7 +1169,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, withdrawalStatus: [CONST.SEARCH.SETTLEMENT_STATUS.PENDING, CONST.SEARCH.SETTLEMENT_STATUS.FAILED], }); }); @@ -1210,7 +1193,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, amountLessThan: '-12345', amountGreaterThan: '-67890', amountEqualTo: '-54321', @@ -1241,7 +1223,6 @@ describe('SearchQueryUtils', () => { // Both values should be preserved - name-only attendees should not be filtered out expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, attendee: ['12345', 'ZZ'], }); }); @@ -1437,7 +1418,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, tag: [CONST.SEARCH.TAG_EMPTY_VALUE], }); }); @@ -1453,7 +1433,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, has: [CONST.SEARCH.HAS_VALUES.RECEIPT], tag: [CONST.SEARCH.TAG_EMPTY_VALUE], }); @@ -2318,7 +2297,7 @@ describe('SearchQueryUtils', () => { const newQueryJSON = buildSearchQueryJSON(result); const keywordFilter = newQueryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD); expect(keywordFilter?.filters.at(0)?.value).toBe('status:done'); - expect(getStatusFromQuery(newQueryJSON)).toBe(CONST.SEARCH.STATUS.EXPENSE.ALL); + expect(getFilterFromQuery(newQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS).value).toBe(undefined); }); test('does not add quotes to non-keyword filter values', () => { @@ -3454,24 +3433,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({}); }); - it('should reset status to ALL when it has a non-ALL value', () => { - const form: Partial = { - status: CONST.SEARCH.STATUS.EXPENSE.DRAFTS, - }; - const result = getAdvancedFiltersToReset(form); - expect(result).toEqual({ - status: CONST.SEARCH.STATUS.EXPENSE.ALL, - }); - }); - - it('should not include status in reset when it is already ALL', () => { - const form: Partial = { - status: CONST.SEARCH.STATUS.EXPENSE.ALL, - }; - const result = getAdvancedFiltersToReset(form); - expect(result.status).toBeUndefined(); - }); - it('should reset type to EXPENSE when it has a non-EXPENSE value', () => { const form: Partial = { type: CONST.SEARCH.DATA_TYPES.CHAT, @@ -3496,6 +3457,7 @@ describe('SearchQueryUtils', () => { currency: ['USD', 'EUR'], dateAfter: '2024-01-01', keyword: 'hotel', + status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS], }; const result = getAdvancedFiltersToReset(form); expect(result).toEqual({ diff --git a/tests/unit/Search/getSpendOverTimeStateTest.ts b/tests/unit/Search/getSpendOverTimeStateTest.ts index f4c0b6e820bd..b8a0c2a82f05 100644 --- a/tests/unit/Search/getSpendOverTimeStateTest.ts +++ b/tests/unit/Search/getSpendOverTimeStateTest.ts @@ -13,10 +13,10 @@ const queryJSON: SearchQueryJSON = { const makeSearchResults = (overrides: Partial = {}): SearchResults => ({ - search: {offset: 0, type: queryJSON.type, hasMoreResults: false, hasResults: true, isLoading: false}, + search: {offset: 0, hash: 0, type: queryJSON.type, hasMoreResults: false, hasResults: true, isLoading: false}, data: {}, ...overrides, - }) as SearchResults; + }); const makeData = (count: number): GroupedItem[] => Array.from({length: count}, (_, i) => ({keyForList: String(i)})) as unknown as GroupedItem[]; diff --git a/tests/unit/Search/yourSpendQueryBuildersTest.ts b/tests/unit/Search/yourSpendQueryBuildersTest.ts index a12e011a7f63..6396ad5e878c 100644 --- a/tests/unit/Search/yourSpendQueryBuildersTest.ts +++ b/tests/unit/Search/yourSpendQueryBuildersTest.ts @@ -8,7 +8,7 @@ */ import {buildAwaitingApprovalQuery, buildRecentCardTransactionsQuery, buildRepaidLast30DaysQuery} from '@pages/home/YourSpendSection/queries'; import CONST from '@src/CONST'; -import {buildSearchQueryJSON, getPolicyIDFromQuery, getStatusFromQuery} from '@src/libs/SearchQueryUtils'; +import {buildSearchQueryJSON, getFilterFromQuery} from '@src/libs/SearchQueryUtils'; const ACCOUNT_ID = 12345; const CARD_ID = 67890; @@ -48,7 +48,7 @@ describe('buildAwaitingApprovalQuery', () => { it('produces status:outstanding', () => { const queryJSON = buildSearchQueryJSON(queryString); - expect(getStatusFromQuery(queryJSON)).toEqual([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]); + expect(getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS).value).toEqual([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]); }); it('resolves from to the numeric accountID (not literal [me])', () => { @@ -76,14 +76,14 @@ describe('buildAwaitingApprovalQuery', () => { it('emits the policyID filter for a single policy', () => { const scoped = buildAwaitingApprovalQuery(ACCOUNT_ID, ['policy_a']); const queryJSON = buildSearchQueryJSON(scoped); - expect(getPolicyIDFromQuery(queryJSON).value).toEqual(['policy_a']); + expect(getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID).value).toEqual(['policy_a']); expect(scoped).toContain(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:policy_a`); }); it('emits the policyID filter for multiple policies, preserving the provided order', () => { const scoped = buildAwaitingApprovalQuery(ACCOUNT_ID, ['policy_a', 'policy_b', 'policy_c']); const queryJSON = buildSearchQueryJSON(scoped); - expect(getPolicyIDFromQuery(queryJSON).value).toEqual(['policy_a', 'policy_b', 'policy_c']); + expect(getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID).value).toEqual(['policy_a', 'policy_b', 'policy_c']); expect(scoped).toContain(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:policy_a,policy_b,policy_c`); }); @@ -91,7 +91,7 @@ describe('buildAwaitingApprovalQuery', () => { const scoped = buildAwaitingApprovalQuery(ACCOUNT_ID, ['policy_a']); const queryJSON = buildSearchQueryJSON(scoped); expect(queryJSON?.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE); - expect(getStatusFromQuery(queryJSON)).toEqual([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]); + expect(getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS).value).toEqual([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]); const fromValues = getRawFiltersForKey(scoped, CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM).flatMap((f) => (Array.isArray(f.value) ? f.value : [f.value])); expect(fromValues).toContain(String(ACCOUNT_ID)); const reimbursableValues = getRawFiltersForKey(scoped, CONST.SEARCH.SYNTAX_FILTER_KEYS.REIMBURSABLE).flatMap((f) => (Array.isArray(f.value) ? f.value : [f.value])); @@ -127,7 +127,7 @@ describe('buildRepaidLast30DaysQuery', () => { it('produces status:paid', () => { const queryJSON = buildSearchQueryJSON(queryString); - expect(getStatusFromQuery(queryJSON)).toEqual([CONST.SEARCH.STATUS.EXPENSE.PAID]); + expect(getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS).value).toEqual([CONST.SEARCH.STATUS.EXPENSE.PAID]); }); it('resolves from to the numeric accountID', () => { diff --git a/tests/unit/SearchActionsTest.ts b/tests/unit/SearchActionsTest.ts index ef657f36ce85..0fa1f92cb35b 100644 --- a/tests/unit/SearchActionsTest.ts +++ b/tests/unit/SearchActionsTest.ts @@ -5,8 +5,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {AnyOnyxUpdate} from '@src/types/onyx/Request'; -const EXPENSE_STATUS_ALL = CONST.SEARCH.STATUS.EXPENSE.ALL; - jest.mock('@libs/API'); jest.mock('@libs/Network/enhanceParameters', () => ({ __esModule: true, diff --git a/tests/unit/SearchParserTest.ts b/tests/unit/SearchParserTest.ts index 3c83f8698a54..a9cbc3d01855 100644 --- a/tests/unit/SearchParserTest.ts +++ b/tests/unit/SearchParserTest.ts @@ -810,7 +810,6 @@ const tests = [ query: 'bankAccount:42', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -825,7 +824,6 @@ const tests = [ query: 'bankAccount:42,99', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -969,7 +967,6 @@ const keywordTests = [ query: 'receipt-type:ereceipt', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -984,7 +981,6 @@ const keywordTests = [ query: 'receipt-type:hotel,itemized', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', diff --git a/tests/unit/useSearchHighlightAndScrollTest.ts b/tests/unit/useSearchHighlightAndScrollTest.ts index 4b2f04bc693f..9428d3e8d328 100644 --- a/tests/unit/useSearchHighlightAndScrollTest.ts +++ b/tests/unit/useSearchHighlightAndScrollTest.ts @@ -4,7 +4,6 @@ import Onyx from 'react-native-onyx'; import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll'; import type {UseSearchHighlightAndScroll} from '@hooks/useSearchHighlightAndScroll'; import {search} from '@libs/actions/Search'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; jest.mock('@libs/actions/Search'); From 7687dcd20ac0558df78f4de15f77dcfaec649520 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 17:17:30 +0800 Subject: [PATCH 05/32] improvement --- src/components/Search/FilterComponents/CategorySelector.tsx | 3 ++- src/components/Search/FilterComponents/TagSelector.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Search/FilterComponents/CategorySelector.tsx b/src/components/Search/FilterComponents/CategorySelector.tsx index aaab4ac95624..67b38ed81ec5 100644 --- a/src/components/Search/FilterComponents/CategorySelector.tsx +++ b/src/components/Search/FilterComponents/CategorySelector.tsx @@ -53,9 +53,10 @@ function CategorySelector({value = [], policyID, selectionListTextInputStyle, se uniqueCategoryNames.add(category.name); } } else { + const selectedCategoryKeys = new Set(policyID.value?.map((id) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${id}`)); const selectedPoliciesCategories: PolicyCategory[] = Object.keys(allPolicyCategories ?? {}) .filter((key) => { - const isSelected = policyID.value?.map((id) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${id}`)?.includes(key); + const isSelected = selectedCategoryKeys.has(key); return policyID.isNegated ? !isSelected : isSelected; }) .map((key) => Object.values(allPolicyCategories?.[key] ?? {})) diff --git a/src/components/Search/FilterComponents/TagSelector.tsx b/src/components/Search/FilterComponents/TagSelector.tsx index 9b46c6be3ed5..8bde9ea452ba 100644 --- a/src/components/Search/FilterComponents/TagSelector.tsx +++ b/src/components/Search/FilterComponents/TagSelector.tsx @@ -28,9 +28,10 @@ function TagSelector({value = [], policyID, selectionListTextInputStyle, selecti uniqueTagNames.add(tag); } } else { + const selectedTagKeys = new Set(policyID.value?.map((id) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${id}`)); const selectedPoliciesTagLists = Object.keys(allPolicyTagLists ?? {}) .filter((key) => { - const isSelected = policyID.value?.map((id) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${id}`)?.includes(key); + const isSelected = selectedTagKeys.has(key); return policyID.isNegated ? !isSelected : isSelected; }) .map((key) => getTagNamesFromTagsLists(allPolicyTagLists?.[key] ?? {})) From 857b5d352e8174932279bbe89c90e687c0148d14 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 17:19:00 +0800 Subject: [PATCH 06/32] lint --- src/components/Search/index.tsx | 2 +- src/libs/actions/IOU/SearchUpdate.ts | 1 - tests/actions/IOU/SearchUpdateTest.ts | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 21f83b35b9f6..5049198ba4f9 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -241,7 +241,7 @@ function Search({ const isDataLoaded = shouldUseLiveData || isSearchDataLoaded(searchResults, queryJSON); const hasErrors = Object.keys(searchResults?.errors ?? {}).length > 0 && !isOffline; -console.log('query json', queryJSON) + const deferHeavySearchWork = useCallback( (useDoubleFrame = false) => { setShouldDeferHeavySearchWork(true); diff --git a/src/libs/actions/IOU/SearchUpdate.ts b/src/libs/actions/IOU/SearchUpdate.ts index aed70a57c1b0..ce5445c62025 100644 --- a/src/libs/actions/IOU/SearchUpdate.ts +++ b/src/libs/actions/IOU/SearchUpdate.ts @@ -104,7 +104,6 @@ function shouldOptimisticallyUpdateSearch( const matchesSubmitQuery = submitQueryJSON?.similarSearchHash === currentSearchQueryJSON.similarSearchHash && expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.DRAFTS](iouReport); - console.log('query json hash', unapprovedCashSimilarSearchHash) const matchesApproveQuery = approveQueryJSON?.similarSearchHash === currentSearchQueryJSON.similarSearchHash && expenseReportStatusFilterMapping[CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING](iouReport); diff --git a/tests/actions/IOU/SearchUpdateTest.ts b/tests/actions/IOU/SearchUpdateTest.ts index 281167dff39b..434608555117 100644 --- a/tests/actions/IOU/SearchUpdateTest.ts +++ b/tests/actions/IOU/SearchUpdateTest.ts @@ -16,7 +16,6 @@ import {createRandomReport} from '../../utils/collections/reports'; import createRandomTransaction from '../../utils/collections/transaction'; import {getGlobalFetchMock} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -import { getQueryHashes } from '@libs/SearchQueryUtils'; const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ From ff3db0bbb9f4222d42b5c674f8803e4192b4a804 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 17:19:05 +0800 Subject: [PATCH 07/32] prettier --- .../AdvancedFilters/FilterList.tsx | 2 +- src/hooks/useAdvancedSearchFilters.ts | 2 +- src/libs/PolicyUtils.ts | 2 +- .../HomePage/YourSpendSection/rowStateTest.ts | 27 +++++++++---------- .../unit/Search/getSpendOverTimeStateTest.ts | 11 ++++---- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/components/Search/FilterComponents/AdvancedFilters/FilterList.tsx b/src/components/Search/FilterComponents/AdvancedFilters/FilterList.tsx index 2e425d6dac81..b6596dbca691 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/FilterList.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/FilterList.tsx @@ -4,6 +4,7 @@ import type {StyleProp, ViewStyle} from 'react-native'; import Icon from '@components/Icon'; import {PressableWithFeedback} from '@components/Pressable'; import ScrollView from '@components/ScrollView'; +import type {Filter} from '@components/Search/types'; import SpacerView from '@components/SpacerView'; import Text from '@components/Text'; import useAdvancedSearchFilters from '@hooks/useAdvancedSearchFilters'; @@ -17,7 +18,6 @@ import {FILTER_VIEW_MAP} from '@libs/SearchUIUtils'; import type {SearchFilter} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; -import type { Filter } from '@components/Search/types'; type ItemCallback = (filter: SearchFilter['key']) => void; type FilterItemCallbacks = { diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts index 3ffe3010a894..5c4c8a5645fe 100644 --- a/src/hooks/useAdvancedSearchFilters.ts +++ b/src/hooks/useAdvancedSearchFilters.ts @@ -2,6 +2,7 @@ import {filterCardsHiddenFromSearch} from '@selectors/Card'; import passthroughPolicyTagListSelector from '@selectors/PolicyTagList'; import {emailSelector} from '@selectors/Session'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {Filter} from '@components/Search/types'; import {isFilterableBankAccount} from '@libs/BankAccountUtils'; import {isPolicyFeatureEnabled} from '@libs/PolicyUtils'; import {getAllPolicyValues} from '@libs/SearchQueryUtils'; @@ -13,7 +14,6 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; import useLocalize from './useLocalize'; import useOnyx from './useOnyx'; import useWorkspaceList from './useWorkspaceList'; -import type { Filter } from '@components/Search/types'; /** * typeFiltersKeys is stored as an object keyed by the different search types. diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 2b478a20593b..d5fb466e4c2a 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -2,6 +2,7 @@ import {Str} from 'expensify-common'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {TupleToUnion, ValueOf} from 'type-fest'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; +import {Filter} from '@components/Search/types'; import type {SelectorType} from '@components/SelectionScreen'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -57,7 +58,6 @@ import {getAllPolicyValues} from './SearchQueryUtils'; import {getAllSortedTransactions, getCategory, getTag, getTagArrayFromName} from './TransactionUtils'; import {generateAccountID} from './UserUtils'; import {isPublicDomain, isValidAccountRoute} from './ValidationUtils'; -import { Filter } from '@components/Search/types'; type MemberEmailsToAccountIDs = Record; diff --git a/tests/unit/HomePage/YourSpendSection/rowStateTest.ts b/tests/unit/HomePage/YourSpendSection/rowStateTest.ts index 074cd0ff4b0e..dafe1db7498d 100644 --- a/tests/unit/HomePage/YourSpendSection/rowStateTest.ts +++ b/tests/unit/HomePage/YourSpendSection/rowStateTest.ts @@ -4,20 +4,19 @@ import type SearchResults from '@src/types/onyx/SearchResults'; // Helpers -const makeSearchResults = (overrides: Partial = {}): SearchResults => - ({ - search: { - offset: 0, - hash: 0, - type: 'expense', - hasMoreResults: false, - hasResults: true, - isLoading: false, - count: 5, - }, - data: {}, - ...overrides, - }); +const makeSearchResults = (overrides: Partial = {}): SearchResults => ({ + search: { + offset: 0, + hash: 0, + type: 'expense', + hasMoreResults: false, + hasResults: true, + isLoading: false, + count: 5, + }, + data: {}, + ...overrides, +}); const makeSearchResultsWithCount = (count: number): SearchResults => makeSearchResults({ diff --git a/tests/unit/Search/getSpendOverTimeStateTest.ts b/tests/unit/Search/getSpendOverTimeStateTest.ts index b8a0c2a82f05..2572da17a3e8 100644 --- a/tests/unit/Search/getSpendOverTimeStateTest.ts +++ b/tests/unit/Search/getSpendOverTimeStateTest.ts @@ -11,12 +11,11 @@ const queryJSON: SearchQueryJSON = { sortOrder: CONST.SEARCH.SORT_ORDER.ASC, } as SearchQueryJSON; -const makeSearchResults = (overrides: Partial = {}): SearchResults => - ({ - search: {offset: 0, hash: 0, type: queryJSON.type, hasMoreResults: false, hasResults: true, isLoading: false}, - data: {}, - ...overrides, - }); +const makeSearchResults = (overrides: Partial = {}): SearchResults => ({ + search: {offset: 0, hash: 0, type: queryJSON.type, hasMoreResults: false, hasResults: true, isLoading: false}, + data: {}, + ...overrides, +}); const makeData = (count: number): GroupedItem[] => Array.from({length: count}, (_, i) => ({keyForList: String(i)})) as unknown as GroupedItem[]; From 5afd48b5c6943f0e7b8caf15760e596b3bc84634 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 17:20:01 +0800 Subject: [PATCH 08/32] remove unused export --- src/libs/SearchUIUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 97fcd42d25c0..51c5b8bde036 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -6414,7 +6414,6 @@ export { getTypeOptions, getSortByOptions, getSortOrderOptions, - getStatusOptions, getGroupBySections, getViewOptions, getCurrencyOptions, From f405bfaf71dee397e44f458a117d3cbb1b9b0b78 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 17:30:15 +0800 Subject: [PATCH 09/32] remove unused export --- src/libs/SearchQueryUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 918f684ae83e..dd7318c845f8 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -2437,7 +2437,6 @@ export { isSearchRootParams, getFilterFromQuery, isFilterNegatable, - getQueryHashes, }; export type {BuildUserReadableQueryStringParams}; From 6f543cd45db4c71a86c1b69ec3e4940ad5152e36 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 17:45:24 +0800 Subject: [PATCH 10/32] fix test --- tests/actions/IOUTest/SplitTest.ts | 2 +- tests/unit/Search/getSpendOverTimeStateTest.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/actions/IOUTest/SplitTest.ts b/tests/actions/IOUTest/SplitTest.ts index dc405b84dfe3..4dcecc358654 100644 --- a/tests/actions/IOUTest/SplitTest.ts +++ b/tests/actions/IOUTest/SplitTest.ts @@ -1992,7 +1992,7 @@ describe('updateSplitTransactionsFromSplitExpensesFlow', () => { }; // When splitting the expense - const hash = 1; + const hash = unapprovedCashHash; let allTransactions: OnyxCollection; let allReports: OnyxCollection; diff --git a/tests/unit/Search/getSpendOverTimeStateTest.ts b/tests/unit/Search/getSpendOverTimeStateTest.ts index 2572da17a3e8..d76aee17acae 100644 --- a/tests/unit/Search/getSpendOverTimeStateTest.ts +++ b/tests/unit/Search/getSpendOverTimeStateTest.ts @@ -4,6 +4,7 @@ import CONST from '@src/CONST'; import type SearchResults from '@src/types/onyx/SearchResults'; const queryJSON: SearchQueryJSON = { + hash: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, groupBy: CONST.SEARCH.GROUP_BY.MONTH, view: CONST.SEARCH.VIEW.LINE, From 122541bb819cc93ccdd5980f6ab267d82b6d9859 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 17:50:34 +0800 Subject: [PATCH 11/32] revert unintended change --- jest/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest/setup.ts b/jest/setup.ts index eeafcd179a20..81c4329ea98b 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -96,7 +96,7 @@ if (!isVerbose) { // Make them global to override module-level console calls global.console = { ...console, - // log: jest.fn(), + log: jest.fn(), info: jest.fn(), debug: jest.fn(), warn: jest.fn(), From 4396bb0da4f31ae07675686e423cb4364ef8a72d Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 17:55:22 +0800 Subject: [PATCH 12/32] make it undefinedable --- src/hooks/useAdvancedSearchFilters.ts | 2 +- src/libs/SearchQueryUtils.ts | 10 ++--- .../useAdvancedSearchFilters.perf-test.tsx | 2 +- .../hooks/useAdvancedSearchFilters.test.ts | 44 +++++++++---------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts index 5c4c8a5645fe..e2bb868f200d 100644 --- a/src/hooks/useAdvancedSearchFilters.ts +++ b/src/hooks/useAdvancedSearchFilters.ts @@ -314,7 +314,7 @@ function shouldDisplayCardFilterSelector(cardList: OnyxEntry) { return shouldDisplayFilter(Object.keys(filterCardsHiddenFromSearch(cardList)).length, true); } -function useAdvancedSearchFilters(type: SearchDataTypes | undefined, policyID: Filter) { +function useAdvancedSearchFilters(type: SearchDataTypes | undefined, policyID: Filter | undefined) { const [shouldDisplayCardFilter] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {selector: shouldDisplayCardFilterSelector}); const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: advancedSearchPoliciesSelector}); const [policyDerived] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policyDerivedSelector}); diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index dd7318c845f8..5f273851e112 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1019,19 +1019,19 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial( - {value: policyID, isNegated}: Filter, + policyID: Filter | undefined, key: T, policyData: OnyxCollection, ): Array { - if (!policyData || !policyID) { + if (!policyData || !policyID || !policyID.value) { return []; } - if (isNegated) { + if (policyID.isNegated) { return Object.keys(policyData).reduce( (acc, curr) => { const id = curr.replace(key, ''); - if (!policyID.includes(id) && policyData[curr]) { + if (!policyID.value?.includes(id) && policyData[curr]) { acc.push(policyData[curr]); } return acc; @@ -1040,7 +1040,7 @@ function getAllPolicyValues( ); } - return policyID.map((id) => policyData?.[`${key}${id}`]).filter((data) => !!data) as Array; + return policyID.value.map((id) => policyData?.[`${key}${id}`]).filter((data) => !!data) as Array; } function getAllPolicyValuesMap( diff --git a/tests/perf-test/useAdvancedSearchFilters.perf-test.tsx b/tests/perf-test/useAdvancedSearchFilters.perf-test.tsx index a07a852a145c..e8fd48a10b65 100644 --- a/tests/perf-test/useAdvancedSearchFilters.perf-test.tsx +++ b/tests/perf-test/useAdvancedSearchFilters.perf-test.tsx @@ -70,7 +70,7 @@ jest.mock('@react-navigation/native', () => { function TestComponent() { const currentType = 'expense'; - const filters = useAdvancedSearchFilters(currentType, {value: undefined, isNegated: false}); + const filters = useAdvancedSearchFilters(currentType, undefined); return ; } diff --git a/tests/unit/hooks/useAdvancedSearchFilters.test.ts b/tests/unit/hooks/useAdvancedSearchFilters.test.ts index 9cb7a784a344..4b502991c44b 100644 --- a/tests/unit/hooks/useAdvancedSearchFilters.test.ts +++ b/tests/unit/hooks/useAdvancedSearchFilters.test.ts @@ -100,7 +100,7 @@ describe('useAdvancedSearchFilters', () => { const policy = buildPolicy(1, {areCategoriesEnabled: false}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -117,7 +117,7 @@ describe('useAdvancedSearchFilters', () => { }; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}1`, categories); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -137,7 +137,7 @@ describe('useAdvancedSearchFilters', () => { }; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}1`, categories); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -151,7 +151,7 @@ describe('useAdvancedSearchFilters', () => { const policy = buildPolicy(1, {areTagsEnabled: false}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -164,7 +164,7 @@ describe('useAdvancedSearchFilters', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}1`, buildTagList('Engineering')); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -186,7 +186,7 @@ describe('useAdvancedSearchFilters', () => { }; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}1`, emptyTagList); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); // Tag filter is hidden because the tag list contains no actual tags await waitFor(() => { @@ -236,7 +236,7 @@ describe('useAdvancedSearchFilters', () => { const policy = buildPolicy(1, {tax: {trackingEnabled: false}}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -259,7 +259,7 @@ describe('useAdvancedSearchFilters', () => { }); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -280,7 +280,7 @@ describe('useAdvancedSearchFilters', () => { }); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -294,7 +294,7 @@ describe('useAdvancedSearchFilters', () => { const policy = buildPolicy(1, {type: CONST.POLICY.TYPE.CORPORATE, isAttendeeTrackingEnabled: false}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -308,7 +308,7 @@ describe('useAdvancedSearchFilters', () => { delete (policy as Record).isAttendeeTrackingEnabled; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -320,7 +320,7 @@ describe('useAdvancedSearchFilters', () => { const policy = buildPolicy(1, {type: CONST.POLICY.TYPE.CORPORATE, isAttendeeTrackingEnabled: true}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -334,7 +334,7 @@ describe('useAdvancedSearchFilters', () => { const policy = buildPolicy(1, {fieldList: {}}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -364,7 +364,7 @@ describe('useAdvancedSearchFilters', () => { }); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -394,7 +394,7 @@ describe('useAdvancedSearchFilters', () => { }); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, policy); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -405,7 +405,7 @@ describe('useAdvancedSearchFilters', () => { describe('bank account filter visibility', () => { it('hides bank account filter when no bank accounts exist', async () => { - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -427,7 +427,7 @@ describe('useAdvancedSearchFilters', () => { }, }); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -449,7 +449,7 @@ describe('useAdvancedSearchFilters', () => { }, }); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -471,7 +471,7 @@ describe('useAdvancedSearchFilters', () => { }, }); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -493,7 +493,7 @@ describe('useAdvancedSearchFilters', () => { }, }); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -515,7 +515,7 @@ describe('useAdvancedSearchFilters', () => { }, }); - const {result} = renderHook(() => useAdvancedSearchFilters(CONST.SEARCH.DATA_TYPES.CHAT, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(CONST.SEARCH.DATA_TYPES.CHAT, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); @@ -550,7 +550,7 @@ describe('useAdvancedSearchFilters', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}1`, categories); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}1`, buildTagList('Engineering')); - const {result} = renderHook(() => useAdvancedSearchFilters(undefined, {value: undefined, isNegated: false}), {wrapper}); + const {result} = renderHook(() => useAdvancedSearchFilters(undefined, undefined), {wrapper}); await waitFor(() => { const allKeys = result.current.flat(); From d21f3c5bc4559797c9c683cbfaecc6b6dc1d58a8 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 20:08:50 +0800 Subject: [PATCH 13/32] add more test --- tests/actions/IOU/SearchUpdateTest.ts | 134 +++++++++++++ tests/unit/Search/SearchQueryUtilsTest.ts | 234 ++++++++++++++++++++++ tests/unit/Search/SearchUIUtilsTest.ts | 110 +++++++++- tests/unit/SearchParserTest.ts | 127 ++++++++++++ 4 files changed, 603 insertions(+), 2 deletions(-) diff --git a/tests/actions/IOU/SearchUpdateTest.ts b/tests/actions/IOU/SearchUpdateTest.ts index 434608555117..050801069063 100644 --- a/tests/actions/IOU/SearchUpdateTest.ts +++ b/tests/actions/IOU/SearchUpdateTest.ts @@ -357,5 +357,139 @@ describe('actions/IOU', () => { }; expect(shouldOptimisticallyUpdateSearch(currentSearchQueryJSON, nonMatchingIOUReport, false, RORY_ACCOUNT_ID, transaction)).toBeFalsy(); }); + + it('when the current hash includes a non-negated status filter it should only return true if the iou report matches the status', () => { + const transaction = { + ...createRandomTransaction(1), + }; + const currentSearchQueryJSON: SearchQueryJSON = { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + view: CONST.SEARCH.VIEW.TABLE, + filters: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + left: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, + right: CONST.SEARCH.STATUS.EXPENSE.APPROVED, + }, + inputQuery: 'type:expense sortBy:date sortOrder:desc status:approved', + flatFilters: [ + { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, + filters: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, value: CONST.SEARCH.STATUS.EXPENSE.APPROVED}], + }, + ], + hash: 100000001, + recentSearchHash: 100000002, + similarSearchHash: 100000003, + }; + + // When the IOU report is approved (matches status:approved), it should return true + const approvedIOUReport: Report = { + ...createRandomReport(2, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + }; + expect(shouldOptimisticallyUpdateSearch(currentSearchQueryJSON, approvedIOUReport, false, RORY_ACCOUNT_ID, transaction)).toBeTruthy(); + + // When the IOU report is in draft (does not match status:approved), it should return false + const draftIOUReport: Report = { + ...createRandomReport(3, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + expect(shouldOptimisticallyUpdateSearch(currentSearchQueryJSON, draftIOUReport, false, RORY_ACCOUNT_ID, transaction)).toBeFalsy(); + }); + + it('when the current hash includes a negated status filter it should return true for iou reports that do not match the excluded status', () => { + const transaction = { + ...createRandomTransaction(1), + }; + const currentSearchQueryJSON: SearchQueryJSON = { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + view: CONST.SEARCH.VIEW.TABLE, + filters: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO, + left: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, + right: CONST.SEARCH.STATUS.EXPENSE.APPROVED, + }, + inputQuery: 'type:expense sortBy:date sortOrder:desc status!=approved', + flatFilters: [ + { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, + filters: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO, value: CONST.SEARCH.STATUS.EXPENSE.APPROVED}], + }, + ], + hash: 100000011, + recentSearchHash: 100000012, + similarSearchHash: 100000013, + }; + + // With status!=approved, a draft report (which is NOT approved) should return true... + const draftIOUReport: Report = { + ...createRandomReport(2, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + expect(shouldOptimisticallyUpdateSearch(currentSearchQueryJSON, draftIOUReport, false, RORY_ACCOUNT_ID, transaction)).toBeTruthy(); + + // ...and an outstanding report (also NOT approved) should return true + const outstandingIOUReport: Report = { + ...createRandomReport(3, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + }; + expect(shouldOptimisticallyUpdateSearch(currentSearchQueryJSON, outstandingIOUReport, false, RORY_ACCOUNT_ID, transaction)).toBeTruthy(); + }); + + it('when the current hash includes a negated policyID filter it should only return true if the iou report does not match the policyID filter', () => { + const transaction = { + ...createRandomTransaction(1), + }; + const policyID = '12345'; + const currentSearchQueryJSON: SearchQueryJSON = { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + view: CONST.SEARCH.VIEW.TABLE, + filters: {operator: CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO, left: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, right: policyID}, + inputQuery: `type:expense sortBy:date sortOrder:desc policyID!=${policyID}`, + flatFilters: [ + { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, + filters: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO, value: policyID}], + }, + ], + hash: 100000021, + recentSearchHash: 100000022, + similarSearchHash: 100000023, + }; + + // When the IOU report has a different policyID (not excluded), it should return true + const nonMatchingIOUReport: Report = { + ...createRandomReport(2, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + policyID: 'differentPolicyID', + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + expect(shouldOptimisticallyUpdateSearch(currentSearchQueryJSON, nonMatchingIOUReport, false, RORY_ACCOUNT_ID, transaction)).toBeTruthy(); + + // When the IOU report has the excluded policyID, it should return false + const matchingIOUReport: Report = { + ...createRandomReport(3, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + expect(shouldOptimisticallyUpdateSearch(currentSearchQueryJSON, matchingIOUReport, false, RORY_ACCOUNT_ID, transaction)).toBeFalsy(); + }); }); }); diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 8a26c9c82f3e..51253c31ba3b 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -16,6 +16,8 @@ import { buildSearchQueryString, buildUserReadableQueryString, getAdvancedFiltersToReset, + getAllPolicyValues, + getAllPolicyValuesMap, getCurrentSearchQueryJSON, getDateRangeDisplayValueFromFormValue, getDisplayQueryFiltersForKey, @@ -27,6 +29,8 @@ import { getQueryWithUpdatedValues, getRangeBoundariesFromFormValue, getRoutes, + isDefaultExpenseReportsQuery, + isDefaultExpensesQuery, isSearchRootParams, serializeQueryJSONForBackend, shouldHighlight, @@ -39,6 +43,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; import type * as OnyxTypes from '@src/types/onyx'; +import createRandomPolicy from '../../utils/collections/policies'; import {localeCompare, translateLocal} from '../../utils/TestHelper'; const mockGetRootState = jest.fn(); @@ -3496,4 +3501,233 @@ describe('SearchQueryUtils', () => { }); }); }); + + describe('getFilterFromQuery', () => { + test('returns all comma-separated values for a non-negated filter', () => { + const queryJSON = buildSearchQueryJSON('type:expense policyID:123,456'); + + const result = getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); + + expect(result.value).toEqual(['123', '456']); + expect(result.isNegated).toBe(false); + }); + + test('flags a negated filter as negated', () => { + const queryJSON = buildSearchQueryJSON('type:expense -policyID:123'); + + const result = getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); + + expect(result.value).toEqual(['123']); + expect(result.isNegated).toBe(true); + }); + + test('reads a filter with a single value', () => { + const queryJSON = buildSearchQueryJSON('type:expense policyID:123'); + + const result = getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); + + expect(result.value).toEqual(['123']); + expect(result.isNegated).toBe(false); + }); + + test('returns undefined value when the filter is not present in the query', () => { + const queryJSON = buildSearchQueryJSON('type:expense merchant:Amazon'); + + const result = getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); + + expect(result.value).toBeUndefined(); + expect(result.isNegated).toBe(false); + }); + + test('returns undefined value for an undefined queryJSON', () => { + const result = getFilterFromQuery(undefined, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); + + expect(result.value).toBeUndefined(); + expect(result.isNegated).toBe(false); + }); + }); + + describe('getAllPolicyValues', () => { + const policy1 = createRandomPolicy(1, undefined, 'Workspace 1'); + const policy2 = createRandomPolicy(2, undefined, 'Workspace 2'); + const policy3 = createRandomPolicy(3, undefined, 'Workspace 3'); + const policyData: OnyxCollection = { + [`${ONYXKEYS.COLLECTION.POLICY}1`]: policy1, + [`${ONYXKEYS.COLLECTION.POLICY}2`]: policy2, + [`${ONYXKEYS.COLLECTION.POLICY}3`]: policy3, + }; + + test('returns the matching policy values for a non-negated filter', () => { + const result = getAllPolicyValues({value: ['1', '2'], isNegated: false}, ONYXKEYS.COLLECTION.POLICY, policyData); + + expect(result).toEqual([policy1, policy2]); + }); + + test('returns every policy value except the excluded ones for a negated filter', () => { + const result = getAllPolicyValues({value: ['1'], isNegated: true}, ONYXKEYS.COLLECTION.POLICY, policyData); + + expect(result).toEqual([policy2, policy3]); + }); + + test('skips ids that do not exist in the policy data', () => { + const result = getAllPolicyValues({value: ['1', '999'], isNegated: false}, ONYXKEYS.COLLECTION.POLICY, policyData); + + expect(result).toEqual([policy1]); + }); + + test('returns an empty array when the filter is undefined', () => { + expect(getAllPolicyValues(undefined, ONYXKEYS.COLLECTION.POLICY, policyData)).toEqual([]); + }); + + test('returns an empty array when the filter has no value', () => { + expect(getAllPolicyValues({value: undefined, isNegated: false}, ONYXKEYS.COLLECTION.POLICY, policyData)).toEqual([]); + }); + + test('returns an empty array when the policy data is undefined', () => { + expect(getAllPolicyValues({value: ['1'], isNegated: false}, ONYXKEYS.COLLECTION.POLICY, undefined)).toEqual([]); + }); + }); + + describe('getAllPolicyValuesMap', () => { + const policy1 = createRandomPolicy(1, undefined, 'Workspace 1'); + const policy2 = createRandomPolicy(2, undefined, 'Workspace 2'); + const policy3 = createRandomPolicy(3, undefined, 'Workspace 3'); + const policyData: OnyxCollection = { + [`${ONYXKEYS.COLLECTION.POLICY}1`]: policy1, + [`${ONYXKEYS.COLLECTION.POLICY}2`]: policy2, + [`${ONYXKEYS.COLLECTION.POLICY}3`]: policy3, + }; + + test('returns a keyed map of the matching policy values for a non-negated filter', () => { + const result = getAllPolicyValuesMap({value: ['1', '2'], isNegated: false}, ONYXKEYS.COLLECTION.POLICY, policyData); + + expect(result).toEqual({ + [`${ONYXKEYS.COLLECTION.POLICY}1`]: policy1, + [`${ONYXKEYS.COLLECTION.POLICY}2`]: policy2, + }); + }); + + test('returns every policy value except the excluded ones for a negated filter', () => { + const result = getAllPolicyValuesMap({value: ['1'], isNegated: true}, ONYXKEYS.COLLECTION.POLICY, policyData); + + expect(result).toEqual({ + [`${ONYXKEYS.COLLECTION.POLICY}2`]: policy2, + [`${ONYXKEYS.COLLECTION.POLICY}3`]: policy3, + }); + }); + + test('skips ids that do not exist in the policy data', () => { + const result = getAllPolicyValuesMap({value: ['1', '999'], isNegated: false}, ONYXKEYS.COLLECTION.POLICY, policyData); + + expect(result).toEqual({ + [`${ONYXKEYS.COLLECTION.POLICY}1`]: policy1, + }); + }); + + test('returns an empty map when the filter is undefined', () => { + expect(getAllPolicyValuesMap(undefined, ONYXKEYS.COLLECTION.POLICY, policyData)).toEqual({}); + }); + + test('returns an empty map when the filter has no value', () => { + expect(getAllPolicyValuesMap({value: undefined, isNegated: false}, ONYXKEYS.COLLECTION.POLICY, policyData)).toEqual({}); + }); + + test('returns an empty map when the policy data is undefined', () => { + expect(getAllPolicyValuesMap({value: ['1'], isNegated: false}, ONYXKEYS.COLLECTION.POLICY, undefined)).toEqual({}); + }); + }); + + describe('isDefaultExpensesQuery', () => { + test('returns true for a bare expense query with no filters or groupBy', () => { + const queryJSON = buildSearchQueryJSON('type:expense'); + + if (!queryJSON) { + throw new Error('Failed to parse query string'); + } + + expect(isDefaultExpensesQuery(queryJSON)).toBe(true); + }); + + test('returns false when the query has filters', () => { + const queryJSON = buildSearchQueryJSON('type:expense merchant:Amazon'); + + if (!queryJSON) { + throw new Error('Failed to parse query string'); + } + + expect(isDefaultExpensesQuery(queryJSON)).toBe(false); + }); + + test('returns false when the query has a groupBy', () => { + const queryJSON = buildSearchQueryJSON('type:expense groupBy:category'); + + if (!queryJSON) { + throw new Error('Failed to parse query string'); + } + + expect(isDefaultExpensesQuery(queryJSON)).toBe(false); + }); + + test('returns false for a non-expense type', () => { + const queryJSON = buildSearchQueryJSON('type:invoice'); + + if (!queryJSON) { + throw new Error('Failed to parse query string'); + } + + expect(isDefaultExpensesQuery(queryJSON)).toBe(false); + }); + + test('returns false for an expense report type', () => { + const queryJSON = buildSearchQueryJSON('type:expense-report'); + + if (!queryJSON) { + throw new Error('Failed to parse query string'); + } + + expect(isDefaultExpensesQuery(queryJSON)).toBe(false); + }); + }); + + describe('isDefaultExpenseReportsQuery', () => { + test('returns true for a bare expense report query with no filters or groupBy', () => { + const queryJSON = buildSearchQueryJSON('type:expense-report'); + + if (!queryJSON) { + throw new Error('Failed to parse query string'); + } + + expect(isDefaultExpenseReportsQuery(queryJSON)).toBe(true); + }); + + test('returns false when the query has filters', () => { + const queryJSON = buildSearchQueryJSON('type:expense-report merchant:Amazon'); + + if (!queryJSON) { + throw new Error('Failed to parse query string'); + } + + expect(isDefaultExpenseReportsQuery(queryJSON)).toBe(false); + }); + + test('returns false when the query has a groupBy', () => { + const queryJSON = buildSearchQueryJSON('type:expense-report groupBy:category'); + + if (!queryJSON) { + throw new Error('Failed to parse query string'); + } + + expect(isDefaultExpenseReportsQuery(queryJSON)).toBe(false); + }); + + test('returns false for a plain expense type', () => { + const queryJSON = buildSearchQueryJSON('type:expense'); + + if (!queryJSON) { + throw new Error('Failed to parse query string'); + } + + expect(isDefaultExpenseReportsQuery(queryJSON)).toBe(false); + }); + }); }); diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 3f6f1825347b..3d4d782fc96f 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -5463,8 +5463,9 @@ describe('SearchUIUtils', () => { const TEST_QUERY_HASH = 99999; const expenseType: typeof CONST.SEARCH.DATA_TYPES.EXPENSE = CONST.SEARCH.DATA_TYPES.EXPENSE; - function makeExpenseQueryJSON(status: string | string[]) { + function makeExpenseQueryJSON(status: string | string[], isNegated = false) { const statusValues = Array.isArray(status) ? status : [status]; + const operator = isNegated ? CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO : CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO; return { type: expenseType, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, @@ -5474,7 +5475,7 @@ describe('SearchUIUtils', () => { flatFilters: [ { key: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, - filters: statusValues.map((value) => ({operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, value})), + filters: statusValues.map((value) => ({operator, value})), }, ], inputQuery: 'type:expense' as const, @@ -5572,6 +5573,31 @@ describe('SearchUIUtils', () => { expect(sections.some((s) => s.transactionID === filterTestTxID)).toBe(true); }); + it('should include transactions when negated status excludes a different status than the report state', () => { + // Report is OUTSTANDING but we negate DRAFTS, so OUTSTANDING is not excluded and the transaction is shown. + const data = makeFilterTestData({stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED}); + const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.DRAFTS, true)}); + expect(sections.some((s) => s.transactionID === filterTestTxID)).toBe(true); + }); + + it('should include transactions when negated status array excludes statuses other than the report state', () => { + // Report is OUTSTANDING and we negate DRAFTS + APPROVED, so OUTSTANDING is not excluded and the transaction is shown. + const data = makeFilterTestData({stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED}); + const [sections] = callGetTransactionsSections(data, { + queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.APPROVED], true), + }); + expect(sections.some((s) => s.transactionID === filterTestTxID)).toBe(true); + }); + + it('should exclude transactions when negated status excludes every status matching the report (including ALL)', () => { + // Report is OUTSTANDING and we negate both OUTSTANDING and ALL, so no non-excluded predicate matches and the transaction is hidden. + const data = makeFilterTestData({stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED}); + const [sections] = callGetTransactionsSections(data, { + queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING, CONST.SEARCH.STATUS.EXPENSE.ALL], true), + }); + expect(sections.some((s) => s.transactionID === filterTestTxID)).toBe(false); + }); + it('should include UNREPORTED transactions when there is no associated report', () => { const unreportedTxID = 'unreported-tx-1'; const data = { @@ -5811,6 +5837,31 @@ describe('SearchUIUtils', () => { expect(sections.some((s) => s.keyForList === rptFilterReportID)).toBe(false); }); + it('should include report when negated status excludes a different status than the report state', () => { + // Report is OUTSTANDING but we negate DRAFTS, so OUTSTANDING is not excluded and the report is shown. + const data = makeReportFilterTestData({stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, type: CONST.REPORT.TYPE.EXPENSE}); + const [sections] = callGetReportSections(data, {queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.DRAFTS, true)}); + expect(sections.some((s) => s.keyForList === rptFilterReportID)).toBe(true); + }); + + it('should include report when negated status array excludes statuses other than the report state', () => { + // Report is OUTSTANDING and we negate DRAFTS + APPROVED, so OUTSTANDING is not excluded and the report is shown. + const data = makeReportFilterTestData({stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, type: CONST.REPORT.TYPE.EXPENSE}); + const [sections] = callGetReportSections(data, { + queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.APPROVED], true), + }); + expect(sections.some((s) => s.keyForList === rptFilterReportID)).toBe(true); + }); + + it('should exclude report when negated status excludes every status matching the report (including ALL)', () => { + // Report is OUTSTANDING and we negate both OUTSTANDING and ALL, so no non-excluded predicate matches and the report is hidden. + const data = makeReportFilterTestData({stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, type: CONST.REPORT.TYPE.EXPENSE}); + const [sections] = callGetReportSections(data, { + queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING, CONST.SEARCH.STATUS.EXPENSE.ALL], true), + }); + expect(sections.some((s) => s.keyForList === rptFilterReportID)).toBe(false); + }); + it('should bypass status filter when isActionLoadingSet contains the report metadata key', () => { const data = makeReportFilterTestData({stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN, type: CONST.REPORT.TYPE.EXPENSE}); const loadingSet = new Set([`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${rptFilterReportID}`]); @@ -7603,6 +7654,61 @@ describe('SearchUIUtils', () => { }); }); + describe('Test isSearchDataLoaded', () => { + const queryJSON = buildSearchQueryJSON('type:expense'); + + function makeSearchResults(overrides: Partial = {}): OnyxTypes.SearchResults { + return { + data: {personalDetailsList: {}}, + search: { + hasMoreResults: false, + hasResults: true, + offset: 0, + hash: queryJSON?.hash ?? 0, + isLoading: false, + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + }, + ...overrides, + }; + } + + it('should return true when data is present and search type and hash match the query', () => { + expect(SearchUIUtils.isSearchDataLoaded(makeSearchResults(), queryJSON)).toBe(true); + }); + + it('should return true when data is absent but errors are present and type and hash match', () => { + const results = makeSearchResults({data: undefined, errors: {error: 'Something went wrong'}}); + expect(SearchUIUtils.isSearchDataLoaded(results, queryJSON)).toBe(true); + }); + + it('should return false when both data and errors are absent', () => { + const results = makeSearchResults({data: undefined, errors: undefined}); + expect(SearchUIUtils.isSearchDataLoaded(results, queryJSON)).toBe(false); + }); + + it('should return false when the search type does not match the query type', () => { + const results = makeSearchResults({ + search: {hasMoreResults: false, hasResults: true, offset: 0, hash: queryJSON?.hash ?? 0, isLoading: false, type: CONST.SEARCH.DATA_TYPES.CHAT}, + }); + expect(SearchUIUtils.isSearchDataLoaded(results, queryJSON)).toBe(false); + }); + + it('should return false when the search hash does not match the query hash', () => { + const results = makeSearchResults({ + search: {hasMoreResults: false, hasResults: true, offset: 0, hash: (queryJSON?.hash ?? 0) + 1, isLoading: false, type: CONST.SEARCH.DATA_TYPES.EXPENSE}, + }); + expect(SearchUIUtils.isSearchDataLoaded(results, queryJSON)).toBe(false); + }); + + it('should return false when searchResults is undefined', () => { + expect(SearchUIUtils.isSearchDataLoaded(undefined, queryJSON)).toBe(false); + }); + + it('should return false when queryJSON is undefined but searchResults has a concrete type and hash', () => { + expect(SearchUIUtils.isSearchDataLoaded(makeSearchResults(), undefined)).toBe(false); + }); + }); + describe('Test isSearchResultsEmpty', () => { it('should return true when all transactions have delete pending action', () => { const results: OnyxTypes.SearchResults = { diff --git a/tests/unit/SearchParserTest.ts b/tests/unit/SearchParserTest.ts index a9cbc3d01855..55cfb0897165 100644 --- a/tests/unit/SearchParserTest.ts +++ b/tests/unit/SearchParserTest.ts @@ -402,6 +402,133 @@ const tests = [ }, }, }, + { + query: '-status:all', + expected: { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + view: 'table', + filters: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO, + left: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, + right: 'all', + }, + }, + }, + { + query: 'status:drafts,outstanding', + expected: { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + view: 'table', + filters: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + left: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, + right: ['drafts', 'outstanding'], + }, + }, + }, + { + query: '-status:drafts,outstanding', + expected: { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + view: 'table', + filters: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO, + left: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, + right: ['drafts', 'outstanding'], + }, + }, + }, + { + query: 'policyID:123', + expected: { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + view: 'table', + filters: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + left: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, + right: '123', + }, + }, + }, + { + query: '-policyID:123', + expected: { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + view: 'table', + filters: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO, + left: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, + right: '123', + }, + }, + }, + { + query: 'policyID:123,456', + expected: { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + view: 'table', + filters: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + left: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, + right: ['123', '456'], + }, + }, + }, + { + query: '-policyID:123,456', + expected: { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + view: 'table', + filters: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO, + left: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, + right: ['123', '456'], + }, + }, + }, + { + // The "workspace" keyword is an alias that resolves to the policyID filter key + query: 'workspace:123', + expected: { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + view: 'table', + filters: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + left: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, + right: '123', + }, + }, + }, + { + query: '-workspace:123', + expected: { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + view: 'table', + filters: { + operator: CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO, + left: CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID, + right: '123', + }, + }, + }, { query: 'amount>200 las vegas category:"Hotel : Marriott"', expected: { From 075a7e05a643c4adeca5b912d95cbfb3a4da8013 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 1 Jul 2026 21:07:12 +0800 Subject: [PATCH 14/32] lint --- .../AdvancedFilters/CommonFilterContent.tsx | 5 +- .../SearchAdvancedFiltersContent.tsx | 4 +- .../FilterComponents/ExportedToSelector.tsx | 3 +- .../Search/hooks/useUpdateFilterQuery.tsx | 1 - src/components/Search/types.ts | 11 +++ src/hooks/useExportedToFilterOptions.ts | 3 +- src/libs/PolicyUtils.ts | 27 ------ src/libs/SearchQueryUtils.ts | 82 +++++++++++++------ src/libs/SearchUIUtils.ts | 5 +- src/types/utils/hasKey.ts | 5 ++ tests/unit/PolicyUtilsTest.ts | 71 ---------------- tests/unit/Search/SearchQueryUtilsTest.ts | 71 ++++++++++++++++ .../hooks/useExportedToFilterOptions.test.ts | 2 +- 13 files changed, 154 insertions(+), 136 deletions(-) create mode 100644 src/types/utils/hasKey.ts diff --git a/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx index 5a966a400dc8..104790f41269 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import type {Filter, SearchFilterCommonProps} from '@components/Search/types'; +import type {Filter, SearchFilterCommonProps, SearchTextFilterKeys} from '@components/Search/types'; +import type {SearchAdvancedFiltersForm} from '@src/types/form'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import type {FilterComponentsProps} from '..'; import FilterComponents from '..'; -type CommonFilterContentProps = SearchFilterCommonProps & { +type CommonFilterContentProps = SearchFilterCommonProps> & { filterKey: FilterComponentsProps['filterKey']; type: SearchDataTypes | undefined; policyID: Filter; diff --git a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx index 956529c00f5f..86df2afd92a0 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx @@ -29,9 +29,9 @@ type SearchAdvancedFiltersContentProps = { onChange: (values: Partial) => void; }; -function getFilterFormValue(filterKey: K, value: FilterComponentsProps['value']): Partial { +function getFilterFormValue(filterKey: K, value: SearchAdvancedFiltersForm[K] | undefined): Partial { const update: Partial = {}; - update[filterKey] = value as SearchAdvancedFiltersForm[K]; + update[filterKey] = value; return update; } diff --git a/src/components/Search/FilterComponents/ExportedToSelector.tsx b/src/components/Search/FilterComponents/ExportedToSelector.tsx index f7f4ce185cf4..f527757d1e38 100644 --- a/src/components/Search/FilterComponents/ExportedToSelector.tsx +++ b/src/components/Search/FilterComponents/ExportedToSelector.tsx @@ -11,9 +11,8 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {getSearchValueForConnection} from '@libs/AccountingUtils'; import {getExportTemplates} from '@libs/actions/Search'; -import {getConnectedIntegrationNamesForPolicies} from '@libs/PolicyUtils'; import {getIntegrationIcon} from '@libs/ReportUtils'; -import {getAllPolicyValues} from '@libs/SearchQueryUtils'; +import {getAllPolicyValues, getConnectedIntegrationNamesForPolicies} from '@libs/SearchQueryUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/components/Search/hooks/useUpdateFilterQuery.tsx b/src/components/Search/hooks/useUpdateFilterQuery.tsx index a84a9226b59a..d8ac73b261f9 100644 --- a/src/components/Search/hooks/useUpdateFilterQuery.tsx +++ b/src/components/Search/hooks/useUpdateFilterQuery.tsx @@ -4,7 +4,6 @@ import useOnyx from '@hooks/useOnyx'; import Navigation from '@libs/Navigation/Navigation'; import {buildFilterQueryWithSortDefaults} from '@libs/SearchQueryUtils'; import {filterValidHasValues} from '@libs/SearchUIUtils'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 9db6f017ed19..6e36bb41a54a 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -289,6 +289,16 @@ type ReportFieldNegatedKey = `${typeof CONST.SEARCH.REPORT_FIELD.NOT_PREFIX}${st type ReportFieldDateKey = `${typeof CONST.SEARCH.REPORT_FIELD.GLOBAL_PREFIX}${ValueOf}-${string}`; type ReportFieldKey = ReportFieldTextKey | ReportFieldDateKey | ReportFieldNegatedKey; +type SearchTextFilterKeys = + | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT + | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION + | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_ID + | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD + | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.TITLE + | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_ID + | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.LIMIT + | ReportFieldTextKey; + type SearchDateFilterKeys = | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.SUBMITTED @@ -480,6 +490,7 @@ export type { SearchWithdrawalStatus, UserFriendlyValue, SelectedReports, + SearchTextFilterKeys, BankAccountMenuItem, SearchCustomColumnIds, GroupedItem, diff --git a/src/hooks/useExportedToFilterOptions.ts b/src/hooks/useExportedToFilterOptions.ts index c5f5a7661876..0d992f2fafb0 100644 --- a/src/hooks/useExportedToFilterOptions.ts +++ b/src/hooks/useExportedToFilterOptions.ts @@ -2,8 +2,7 @@ import type {OnyxCollection} from 'react-native-onyx'; import {useSearchQueryContext} from '@components/Search/SearchContext'; import {getStandardExportTemplateDisplayName} from '@libs/AccountingUtils'; import {getExportTemplates} from '@libs/actions/Search'; -import {getConnectedIntegrationNamesForPolicies} from '@libs/PolicyUtils'; -import {getAllPolicyValues, getFilterFromQuery} from '@libs/SearchQueryUtils'; +import {getAllPolicyValues, getConnectedIntegrationNamesForPolicies, getFilterFromQuery} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ExportTemplate, Policy} from '@src/types/onyx'; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index d5fb466e4c2a..78649ca4b9e1 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -2,7 +2,6 @@ import {Str} from 'expensify-common'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {TupleToUnion, ValueOf} from 'type-fest'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; -import {Filter} from '@components/Search/types'; import type {SelectorType} from '@components/SelectionScreen'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -54,7 +53,6 @@ import {getIsOffline} from './NetworkState'; import {formatMemberForList} from './OptionsListUtils'; import type {MemberForList} from './OptionsListUtils'; import {getAccountIDsByLogins, getKnownAccountIDByLogin, getLoginsByAccountIDs, getPersonalDetailByEmail} from './PersonalDetailsUtils'; -import {getAllPolicyValues} from './SearchQueryUtils'; import {getAllSortedTransactions, getCategory, getTag, getTagArrayFromName} from './TransactionUtils'; import {generateAccountID} from './UserUtils'; import {isPublicDomain, isValidAccountRoute} from './ValidationUtils'; @@ -2330,30 +2328,6 @@ function getValidConnectedIntegration(policy: Policy | undefined, connectionName return connectionNames.find((integration) => !!policy?.connections?.[integration] && !isConnectionUnverified(policy, integration)); } -/** - * Returns a set of connected integration names for the given policies. - * @param policies - Collection of policies to get connected integrations. - * @param policyIDs - Policy IDs to filter by. When provided, only integrations from these policies are included. - */ -function getConnectedIntegrationNamesForPolicies(policies: OnyxCollection | undefined, policyID: Filter | undefined): Set { - if (!policies) { - return new Set(); - } - - const connectedIntegrationNames = new Set(); - const hasWorkspaceFilter = !!policyID?.value?.length; - const policiesToCheck = hasWorkspaceFilter ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies); - - for (const policy of policiesToCheck) { - const connectedIntegration = getValidConnectedIntegration(policy, getAccountingConnectionNames()); - if (connectedIntegration) { - connectedIntegrationNames.add(connectedIntegration); - } - } - - return connectedIntegrationNames; -} - function hasIntegrationAutoSync(policy: Policy | undefined, connectedIntegration?: ConnectionName) { if (!isAccountingConnectionName(connectedIntegration)) { return false; @@ -2737,7 +2711,6 @@ export { getCleanedTagName, getCommaSeparatedTagNameWithSanitizedColons, getConnectedIntegration, - getConnectedIntegrationNamesForPolicies, getConnectionExporters, findVendorByID, getMatchingVendorByID, diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 5f273851e112..357ddbd1b6b4 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -59,7 +59,7 @@ import navigationRef from './Navigation/navigationRef'; import type {SearchFullscreenNavigatorParamList} from './Navigation/types'; import {isRecord} from './ObjectUtils'; import {getDisplayNameOrDefault, getPersonalDetailByEmail} from './PersonalDetailsUtils'; -import {getCleanedTagName} from './PolicyUtils'; +import {getCleanedTagName, getValidConnectedIntegration} from './PolicyUtils'; import {getReportName} from './ReportNameUtils'; import {parse as parseSearchQuery} from './SearchParser/searchParser'; import StringUtils from './StringUtils'; @@ -1040,7 +1040,7 @@ function getAllPolicyValues( ); } - return policyID.value.map((id) => policyData?.[`${key}${id}`]).filter((data) => !!data) as Array; + return policyID.value.map((id) => policyData?.[`${key}${id}`]).filter((data): data is NonNullable => !!data); } function getAllPolicyValuesMap( @@ -1076,6 +1076,30 @@ function getAllPolicyValuesMap( ); } +/** + * Returns a set of connected integration names for the given policies. + * @param policies - Collection of policies to get connected integrations. + * @param policyIDs - Policy IDs to filter by. When provided, only integrations from these policies are included. + */ +function getConnectedIntegrationNamesForPolicies(policies: OnyxCollection | undefined, policyID: Filter | undefined): Set { + if (!policies) { + return new Set(); + } + + const connectedIntegrationNames = new Set(); + const hasWorkspaceFilter = !!policyID?.value?.length; + const policiesToCheck = hasWorkspaceFilter ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies); + + for (const policy of policiesToCheck) { + const connectedIntegration = getValidConnectedIntegration(policy, CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES); + if (connectedIntegration) { + connectedIntegrationNames.add(connectedIntegration); + } + } + + return connectedIntegrationNames; +} + function getEarlierDate(someDate: string | undefined, otherDate: string | undefined) { if (someDate && otherDate) { return someDate < otherDate ? someDate : otherDate; @@ -1162,27 +1186,26 @@ function buildFilterFormValuesFromQuery( const filterValues = filterList.map((item) => item.value.toString()); const isNegated = filterList.some((item) => item.operator === CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO); - const key = isNegated ? (`${filterKey}${CONST.SEARCH.NOT_MODIFIER}` as const) : filterKey; if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID) { - filtersForm[key as typeof filterKey] = filterValues; + filtersForm[addNegation(filterKey, isNegated)] = filterValues; } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS) { - filtersForm[key as typeof filterKey] = filterValues; + filtersForm[addNegation(filterKey, isNegated)] = filterValues; } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_ID || filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_ID) { - filtersForm[key as typeof filterKey] = filterValues.join(','); + filtersForm[addNegation(filterKey, isNegated)] = filterValues.join(','); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT || filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION || filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TITLE) { - filtersForm[key as typeof filterKey] = filterValues.join(','); + filtersForm[addNegation(filterKey, isNegated)] = filterValues.join(','); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.ACTION) { const actionValue = filterValues.join(','); - filtersForm[key as typeof filterKey] = + filtersForm[addNegation(filterKey, isNegated)] = actionValue && Object.values(CONST.SEARCH.ACTION_FILTERS).includes(actionValue as ValueOf) ? actionValue : undefined; } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPENSE_TYPE) { - filtersForm[key as typeof filterKey] = filterValues.filter((expenseType) => VALID_EXPENSE_TYPES.has(expenseType as ExpenseTypeValue)) as ExpenseTypeValues; + filtersForm[addNegation(filterKey, isNegated)] = filterValues.filter((expenseType) => VALID_EXPENSE_TYPES.has(expenseType as ExpenseTypeValue)) as ExpenseTypeValues; } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.RECEIPT_TYPE) { const receiptTypeValues = filterValues.filter((receiptType): receiptType is ReceiptTypeValue => VALID_RECEIPT_TYPES.has(receiptType)); @@ -1210,20 +1233,20 @@ function buildFilterFormValuesFromQuery( } } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.IS) { - filtersForm[key as typeof filterKey] = filterValues.filter((isType) => VALID_IS_TYPES.has(isType as IsFilterValue)) as IsFilterValues; + filtersForm[addNegation(filterKey, isNegated)] = filterValues.filter((isType) => VALID_IS_TYPES.has(isType as IsFilterValue)) as IsFilterValues; } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_TYPE) { - filtersForm[key as typeof filterKey] = filterValues.find((withdrawalType): withdrawalType is SearchWithdrawalType => + filtersForm[addNegation(filterKey, isNegated)] = filterValues.find((withdrawalType): withdrawalType is SearchWithdrawalType => VALID_WITHDRAWAL_TYPES.has(withdrawalType as ValueOf), ); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_STATUS) { - filtersForm[key as typeof filterKey] = filterValues.filter((withdrawalStatus) => VALID_WITHDRAWAL_STATUSES.has(withdrawalStatus)) as Array< + filtersForm[addNegation(filterKey, isNegated)] = filterValues.filter((withdrawalStatus) => VALID_WITHDRAWAL_STATUSES.has(withdrawalStatus)) as Array< ValueOf >; } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) { - filtersForm[key as typeof filterKey] = filterValues.filter((card) => cardList?.[card]); + filtersForm[addNegation(filterKey, isNegated)] = filterValues.filter((card) => cardList?.[card]); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.BANK_ACCOUNT) { // Drop unknown IDs and partially-setup accounts (SETUP/VERIFYING/PENDING). The filter looks backward at @@ -1232,7 +1255,7 @@ function buildFilterFormValuesFromQuery( // keeping its ID would leave the chip visible while the picker hides it. When bankAccountList is still // loading (undefined), keep the saved IDs as-is so useSearchFilterSync does not record an empty signature // and skip the re-sync once Onyx hydrates. - filtersForm[key as typeof filterKey] = bankAccountList + filtersForm[addNegation(filterKey, isNegated)] = bankAccountList ? filterValues.filter((bankAccountID) => { const bankAccount = bankAccountList[bankAccountID]; return !!bankAccount && !isBankAccountPartiallySetup(bankAccount.accountData?.state); @@ -1240,13 +1263,13 @@ function buildFilterFormValuesFromQuery( : filterValues; } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED) { - filtersForm[key as typeof filterKey] = filterValues.filter((feed) => feed); + filtersForm[addNegation(filterKey, isNegated)] = filterValues.filter((feed) => feed); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) { - filtersForm[key as typeof filterKey] = filterValues.filter((tax) => allTaxRateKeys.has(tax)); + filtersForm[addNegation(filterKey, isNegated)] = filterValues.filter((tax) => allTaxRateKeys.has(tax)); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.IN) { - filtersForm[key as typeof filterKey] = filterValues.filter((id) => reports?.[`${ONYXKEYS.COLLECTION.REPORT}${id}`]?.reportID); + filtersForm[addNegation(filterKey, isNegated)] = filterValues.filter((id) => reports?.[`${ONYXKEYS.COLLECTION.REPORT}${id}`]?.reportID); } if ( filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || @@ -1255,21 +1278,21 @@ function buildFilterFormValuesFromQuery( filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPORTER ) { const resolvedValues = filterValues.map((id) => (id === CONST.SEARCH.ME && currentUserAccountID ? currentUserAccountID.toString() : id)); - filtersForm[key as typeof filterKey] = resolvedValues.filter((id) => personalDetails?.[id]); + filtersForm[addNegation(filterKey, isNegated)] = resolvedValues.filter((id) => personalDetails?.[id]); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.ATTENDEE) { // Don't filter attendee values by personalDetails - they can be accountIDs OR display names for name-only attendees - filtersForm[key as typeof filterKey] = filterValues; + filtersForm[addNegation(filterKey, isNegated)] = filterValues; } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPORTED_TO) { - filtersForm[key as typeof filterKey] = filterValues.filter((value) => exportedToValues.has(value)); + filtersForm[addNegation(filterKey, isNegated)] = filterValues.filter((value) => exportedToValues.has(value)); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.PAYER) { - filtersForm[key as typeof filterKey] = filterValues.find((id) => personalDetails?.[id]); + filtersForm[addNegation(filterKey, isNegated)] = filterValues.find((id) => personalDetails?.[id]); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY || filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.PURCHASE_CURRENCY) { - filtersForm[key as typeof filterKey] = filterValues.filter((currency) => validCurrencies.has(currency)); + filtersForm[addNegation(filterKey, isNegated)] = filterValues.filter((currency) => validCurrencies.has(currency)); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.GROUP_CURRENCY) { filtersForm[filterKey] = filterValues.find((currency) => validCurrencies.has(currency)); @@ -1285,7 +1308,7 @@ function buildFilterFormValuesFromQuery( } } uniqueTags.add(CONST.SEARCH.TAG_EMPTY_VALUE); - filtersForm[key as typeof filterKey] = filterValues.filter((name) => uniqueTags.has(name)); + filtersForm[addNegation(filterKey, isNegated)] = filterValues.filter((name) => uniqueTags.has(name)); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY) { const uniqueCategories = new Set(); @@ -1297,10 +1320,12 @@ function buildFilterFormValuesFromQuery( } const hasEmptyCategoriesInFilter = filterValues.includes(CONST.SEARCH.CATEGORY_EMPTY_VALUE); // If empty categories are found, append the CATEGORY_EMPTY_VALUE to filtersForm. - filtersForm[key as typeof filterKey] = filterValues.filter((name) => uniqueCategories.has(name)).concat(hasEmptyCategoriesInFilter ? [CONST.SEARCH.CATEGORY_EMPTY_VALUE] : []); + filtersForm[addNegation(filterKey, isNegated)] = filterValues + .filter((name) => uniqueCategories.has(name)) + .concat(hasEmptyCategoriesInFilter ? [CONST.SEARCH.CATEGORY_EMPTY_VALUE] : []); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD) { - filtersForm[key as typeof filterKey] = filterValues + filtersForm[filterKey] = filterValues ?.map((filter) => { if (filter.includes(' ')) { return `"${filter}"`; @@ -1374,7 +1399,7 @@ function buildFilterFormValuesFromQuery( if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.BILLABLE || filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.REIMBURSABLE) { const validBooleanTypes = Object.values(CONST.SEARCH.BOOLEAN); - filtersForm[key as typeof filterKey] = validBooleanTypes.find((value) => filterValues.at(0) === value); + filtersForm[addNegation(filterKey, isNegated)] = validBooleanTypes.find((value) => filterValues.at(0) === value); } if (filterKey.startsWith(CONST.SEARCH.REPORT_FIELD.DEFAULT_PREFIX)) { @@ -2383,6 +2408,10 @@ function serializeQueryJSONForBackend(filterKey: T, isNegated: boolean): T | `${T}${typeof CONST.SEARCH.NOT_MODIFIER}` { + return isNegated ? `${filterKey}${CONST.SEARCH.NOT_MODIFIER}` : filterKey; +} + function removeNegation(filterKey: string) { return filterKey.replace(CONST.SEARCH.NOT_MODIFIER, ''); } @@ -2419,6 +2448,7 @@ export { shouldHighlight, getAllPolicyValues, getAllPolicyValuesMap, + getConnectedIntegrationNamesForPolicies, getUserFriendlyValue, getUserFriendlyKey, shouldResetSort, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 51c5b8bde036..6e42378a8165 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -92,6 +92,7 @@ import type { SearchWithdrawalIDGroup, SearchYearGroup, } from '@src/types/onyx/SearchResults'; +import hasKey from '@src/types/utils/hasKey'; import type IconAsset from '@src/types/utils/IconAsset'; import arraysEqual from '@src/utils/arraysEqual'; import {hasSynchronizationErrorMessage} from './actions/connections'; @@ -5449,8 +5450,8 @@ function getFilterNegatableValue( if (!isFilterNegatable(filterKey)) { return {isNegated: false, value: values?.[filterKey]}; } - const negatedFilterKey = `${filterKey}${CONST.SEARCH.NOT_MODIFIER}` as K; - const negatedValue = values?.[negatedFilterKey]; + const negatedFilterKey = `${filterKey}${CONST.SEARCH.NOT_MODIFIER}` as const; + const negatedValue = values && hasKey(values, negatedFilterKey) ? values[negatedFilterKey] : undefined; const value = negatedValue ?? values?.[filterKey]; return {isNegated: !!negatedValue, value}; } diff --git a/src/types/utils/hasKey.ts b/src/types/utils/hasKey.ts new file mode 100644 index 000000000000..e1896547bdcd --- /dev/null +++ b/src/types/utils/hasKey.ts @@ -0,0 +1,5 @@ +function hasKey(obj: T, key: PropertyKey): key is keyof T { + return key in obj; +} + +export default hasKey; diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index 0d7d4956ed09..eab9ae28de1a 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -17,7 +17,6 @@ import { getActivePoliciesWithExpenseChatAndPerDiemEnabledAndHasRates, getAllTaxRates, getAllTaxRatesNamesAndValues, - getConnectedIntegrationNamesForPolicies, getCustomUnitsForDuplication, getDefaultTimeTrackingRate, getEligibleBankAccountShareRecipients, @@ -2632,76 +2631,6 @@ describe('PolicyUtils', () => { }); }); - describe('getConnectedIntegrationNamesForPolicies', () => { - it('returns empty Set when policies is undefined', () => { - expect(getConnectedIntegrationNamesForPolicies(undefined, undefined)).toEqual(new Set()); - }); - - it('returns empty Set when policies is empty object', () => { - expect(getConnectedIntegrationNamesForPolicies({}, undefined)).toEqual(new Set()); - }); - - it('returns Set with connection name when policy has verified connection', () => { - const policyWithXero = createMock({ - ...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM), - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.XERO]: { - lastSync: {isConnected: true}, - }, - }), - }); - const policies: OnyxCollection = { - [`${ONYXKEYS.COLLECTION.POLICY}1`]: policyWithXero, - }; - expect(getConnectedIntegrationNamesForPolicies(policies, undefined)).toEqual(new Set([CONST.POLICY.CONNECTIONS.NAME.XERO])); - }); - - it('filters by policyIDs when provided', () => { - const policy1WithQBO = createMock({ - ...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM), - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.QBO]: {lastSync: {isConnected: true}}, - }), - }); - const policy2WithXero = createMock({ - ...createRandomPolicy(2, CONST.POLICY.TYPE.TEAM), - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.XERO]: {lastSync: {isConnected: true}}, - }), - }); - const policies: OnyxCollection = { - [`${ONYXKEYS.COLLECTION.POLICY}1`]: policy1WithQBO, - [`${ONYXKEYS.COLLECTION.POLICY}2`]: policy2WithXero, - }; - expect(getConnectedIntegrationNamesForPolicies(policies, {value: ['1'], isNegated: false})).toEqual(new Set([CONST.POLICY.CONNECTIONS.NAME.QBO])); - }); - - it('returns all connection names when policies have different connections', () => { - const policy1 = createMock({ - ...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM), - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.QBO]: {lastSync: {isConnected: true}}, - }), - }); - - const policy2 = createMock({ - ...createRandomPolicy(2, CONST.POLICY.TYPE.TEAM), - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.XERO]: {lastSync: {isConnected: true}}, - }), - }); - - const policies: OnyxCollection = { - [`${ONYXKEYS.COLLECTION.POLICY}1`]: policy1, - [`${ONYXKEYS.COLLECTION.POLICY}2`]: policy2, - }; - const result = getConnectedIntegrationNamesForPolicies(policies, undefined); - expect(result).toContain(CONST.POLICY.CONNECTIONS.NAME.QBO); - expect(result).toContain(CONST.POLICY.CONNECTIONS.NAME.XERO); - expect(result.size).toBe(2); - }); - }); - describe('hasDependentTags', () => { it('returns false when policy has no multiple tag lists', () => { const policy = createMock({hasMultipleTagLists: false}); diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 51253c31ba3b..4d672cad789f 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -18,6 +18,7 @@ import { getAdvancedFiltersToReset, getAllPolicyValues, getAllPolicyValuesMap, + getConnectedIntegrationNamesForPolicies, getCurrentSearchQueryJSON, getDateRangeDisplayValueFromFormValue, getDisplayQueryFiltersForKey, @@ -3730,4 +3731,74 @@ describe('SearchQueryUtils', () => { expect(isDefaultExpenseReportsQuery(queryJSON)).toBe(false); }); }); + + describe('getConnectedIntegrationNamesForPolicies', () => { + it('returns empty Set when policies is undefined', () => { + expect(getConnectedIntegrationNamesForPolicies(undefined, undefined)).toEqual(new Set()); + }); + + it('returns empty Set when policies is empty object', () => { + expect(getConnectedIntegrationNamesForPolicies({}, undefined)).toEqual(new Set()); + }); + + it('returns Set with connection name when policy has verified connection', () => { + const policyWithXero = createMock({ + ...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM), + connections: createMock({ + [CONST.POLICY.CONNECTIONS.NAME.XERO]: { + lastSync: {isConnected: true}, + }, + }), + }); + const policies: OnyxCollection = { + [`${ONYXKEYS.COLLECTION.POLICY}1`]: policyWithXero, + }; + expect(getConnectedIntegrationNamesForPolicies(policies, undefined)).toEqual(new Set([CONST.POLICY.CONNECTIONS.NAME.XERO])); + }); + + it('filters by policyIDs when provided', () => { + const policy1WithQBO = createMock({ + ...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM), + connections: createMock({ + [CONST.POLICY.CONNECTIONS.NAME.QBO]: {lastSync: {isConnected: true}}, + }), + }); + const policy2WithXero = createMock({ + ...createRandomPolicy(2, CONST.POLICY.TYPE.TEAM), + connections: createMock({ + [CONST.POLICY.CONNECTIONS.NAME.XERO]: {lastSync: {isConnected: true}}, + }), + }); + const policies: OnyxCollection = { + [`${ONYXKEYS.COLLECTION.POLICY}1`]: policy1WithQBO, + [`${ONYXKEYS.COLLECTION.POLICY}2`]: policy2WithXero, + }; + expect(getConnectedIntegrationNamesForPolicies(policies, {value: ['1'], isNegated: false})).toEqual(new Set([CONST.POLICY.CONNECTIONS.NAME.QBO])); + }); + + it('returns all connection names when policies have different connections', () => { + const policy1 = createMock({ + ...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM), + connections: createMock({ + [CONST.POLICY.CONNECTIONS.NAME.QBO]: {lastSync: {isConnected: true}}, + }), + }); + + const policy2 = createMock({ + ...createRandomPolicy(2, CONST.POLICY.TYPE.TEAM), + connections: createMock({ + [CONST.POLICY.CONNECTIONS.NAME.XERO]: {lastSync: {isConnected: true}}, + }), + }); + + const policies: OnyxCollection = { + [`${ONYXKEYS.COLLECTION.POLICY}1`]: policy1, + [`${ONYXKEYS.COLLECTION.POLICY}2`]: policy2, + }; + const result = getConnectedIntegrationNamesForPolicies(policies, undefined); + expect(result).toContain(CONST.POLICY.CONNECTIONS.NAME.QBO); + expect(result).toContain(CONST.POLICY.CONNECTIONS.NAME.XERO); + expect(result.size).toBe(2); + }); + }); }); diff --git a/tests/unit/hooks/useExportedToFilterOptions.test.ts b/tests/unit/hooks/useExportedToFilterOptions.test.ts index b2d55f510e51..72bc65dbc9c9 100644 --- a/tests/unit/hooks/useExportedToFilterOptions.test.ts +++ b/tests/unit/hooks/useExportedToFilterOptions.test.ts @@ -14,7 +14,7 @@ jest.mock('@libs/actions/Search', () => ({ getExportTemplates: (...args: unknown[]) => mockGetExportTemplates(...args), })); -jest.mock('@libs/PolicyUtils', () => ({ +jest.mock('@libs/SearchQueryUtils', () => ({ getConnectedIntegrationNamesForPolicies: () => mockGetConnectedIntegrationNamesForPolicies(), })); From ab98fc5abf2dac2112df2196c0598ae137035074 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 8 Jul 2026 11:51:48 +0800 Subject: [PATCH 15/32] fix test --- tests/ui/SearchPageTest.tsx | 1 - tests/unit/Search/ChatSearchViewTest.tsx | 2 -- .../Search/ExpenseGroupedSearchViewTest.tsx | 2 -- .../Search/ExpenseReportSearchViewTest.tsx | 2 -- tests/unit/Search/SearchQueryUtilsTest.ts | 2 -- tests/unit/Search/SearchUIUtilsTest.ts | 2 -- tests/unit/Search/TaskSearchViewTest.tsx | 2 -- tests/unit/SearchParserTest.ts | 2 -- .../hooks/useExportedToFilterOptions.test.ts | 21 +++++++++---------- 9 files changed, 10 insertions(+), 26 deletions(-) diff --git a/tests/ui/SearchPageTest.tsx b/tests/ui/SearchPageTest.tsx index 4ccdc141f2cf..c4da896bc446 100644 --- a/tests/ui/SearchPageTest.tsx +++ b/tests/ui/SearchPageTest.tsx @@ -193,7 +193,6 @@ describe('SearchPageNarrow', () => { errors: {error: 'Something went wrong'}, search: { type: CONST.SEARCH.DATA_TYPES.CHAT, - status: '', offset: 0, isLoading: false, hasMoreResults: false, diff --git a/tests/unit/Search/ChatSearchViewTest.tsx b/tests/unit/Search/ChatSearchViewTest.tsx index 8b6068c77666..649e8a126d39 100644 --- a/tests/unit/Search/ChatSearchViewTest.tsx +++ b/tests/unit/Search/ChatSearchViewTest.tsx @@ -116,14 +116,12 @@ const STABLE_QUERY_JSON: SearchQueryJSON = { similarSearchHash: 0, groupBy: undefined, type: CONST.SEARCH.DATA_TYPES.CHAT, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: 'desc', view: CONST.SEARCH.VIEW.TABLE, flatFilters: [], inputQuery: '', filters: {operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, left: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, right: ''}, - policyID: undefined, columns: undefined, limit: undefined, rawFilterList: undefined, diff --git a/tests/unit/Search/ExpenseGroupedSearchViewTest.tsx b/tests/unit/Search/ExpenseGroupedSearchViewTest.tsx index 23458e635e83..82831b99305f 100644 --- a/tests/unit/Search/ExpenseGroupedSearchViewTest.tsx +++ b/tests/unit/Search/ExpenseGroupedSearchViewTest.tsx @@ -126,14 +126,12 @@ const STABLE_QUERY_JSON: SearchQueryJSON = { similarSearchHash: 0, groupBy: CONST.SEARCH.GROUP_BY.CARD, type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: 'desc', view: CONST.SEARCH.VIEW.TABLE, flatFilters: [], inputQuery: '', filters: {operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, left: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, right: ''}, - policyID: undefined, columns: undefined, limit: undefined, rawFilterList: undefined, diff --git a/tests/unit/Search/ExpenseReportSearchViewTest.tsx b/tests/unit/Search/ExpenseReportSearchViewTest.tsx index ac20917619f6..2e40e0ba8679 100644 --- a/tests/unit/Search/ExpenseReportSearchViewTest.tsx +++ b/tests/unit/Search/ExpenseReportSearchViewTest.tsx @@ -116,14 +116,12 @@ const STABLE_QUERY_JSON: SearchQueryJSON = { similarSearchHash: 0, groupBy: undefined, type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: 'desc', view: CONST.SEARCH.VIEW.TABLE, flatFilters: [], inputQuery: '', filters: {operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, left: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, right: ''}, - policyID: undefined, columns: undefined, limit: undefined, rawFilterList: undefined, diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index b1346889629a..d4fb49ed994a 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -1228,7 +1228,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense-report', - status: CONST.SEARCH.STATUS.EXPENSE_REPORT.ALL, paidStatus: [CONST.SEARCH.PAID_STATUS.MARKED_AS_PAID, CONST.SEARCH.PAID_STATUS.WITHDRAWING, CONST.SEARCH.PAID_STATUS.CONFIRMED], }); @@ -1244,7 +1243,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense-report', - status: CONST.SEARCH.STATUS.EXPENSE_REPORT.ALL, paidStatus: [CONST.SEARCH.PAID_STATUS.MARKED_AS_PAID, CONST.SEARCH.PAID_STATUS.CONFIRMED], }); }); diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index bc0e66afb37c..d66a98363769 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -6349,7 +6349,6 @@ describe('SearchUIUtils', () => { const ascendingResult = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', [...emptyPostedTransactions], localeCompare, translateLocal, @@ -6359,7 +6358,6 @@ describe('SearchUIUtils', () => { ); const descendingResult = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', [...emptyPostedTransactions], localeCompare, translateLocal, diff --git a/tests/unit/Search/TaskSearchViewTest.tsx b/tests/unit/Search/TaskSearchViewTest.tsx index cda0ce9b9150..9950eb81df1d 100644 --- a/tests/unit/Search/TaskSearchViewTest.tsx +++ b/tests/unit/Search/TaskSearchViewTest.tsx @@ -116,14 +116,12 @@ const STABLE_QUERY_JSON: SearchQueryJSON = { similarSearchHash: 0, groupBy: undefined, type: CONST.SEARCH.DATA_TYPES.TASK, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: 'desc', view: CONST.SEARCH.VIEW.TABLE, flatFilters: [], inputQuery: '', filters: {operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, left: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, right: ''}, - policyID: undefined, columns: undefined, limit: undefined, rawFilterList: undefined, diff --git a/tests/unit/SearchParserTest.ts b/tests/unit/SearchParserTest.ts index ed74c5566c96..0536f2d8b990 100644 --- a/tests/unit/SearchParserTest.ts +++ b/tests/unit/SearchParserTest.ts @@ -690,7 +690,6 @@ const tests = [ query: 'type:expense-report paid-status:markedAsPaid', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', @@ -705,7 +704,6 @@ const tests = [ query: 'type:expense-report paid-status:markedAsPaid,withdrawing,confirmed', expected: { type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: 'table', diff --git a/tests/unit/hooks/useExportedToFilterOptions.test.ts b/tests/unit/hooks/useExportedToFilterOptions.test.ts index 116ea495f51b..6c5587d7b956 100644 --- a/tests/unit/hooks/useExportedToFilterOptions.test.ts +++ b/tests/unit/hooks/useExportedToFilterOptions.test.ts @@ -5,22 +5,23 @@ import useExportedToFilterOptions from '@hooks/useExportedToFilterOptions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ExportTemplate} from '@src/types/onyx'; +import type {ConnectionName} from '@src/types/onyx/Policy'; import Onyx from 'react-native-onyx'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; const mockGetExportTemplates = jest.fn(); -const mockGetConnectedIntegrationNamesForPolicies = jest.fn(() => new Set()); jest.mock('@libs/actions/Search', () => ({ // eslint-disable-next-line @typescript-eslint/no-unsafe-return getExportTemplates: (...args: unknown[]) => mockGetExportTemplates(...args), })); -jest.mock('@libs/SearchQueryUtils', () => ({ - getConnectedIntegrationNamesForPolicies: () => mockGetConnectedIntegrationNamesForPolicies(), -})); +/** Builds a policy with a verified connection so getConnectedIntegrationNamesForPolicies detects it. */ +function buildPolicyWithConnection(policyID: string, connectionName: ConnectionName) { + return {id: policyID, connections: {[connectionName]: {lastSync: {isConnected: true}}}} as const; +} describe('useExportedToFilterOptions', () => { const expenseLevelLabel = CONST.REPORT.EXPORT_OPTION_LABELS.EXPENSE_LEVEL_EXPORT; @@ -35,7 +36,6 @@ describe('useExportedToFilterOptions', () => { await waitForBatchedUpdates(); jest.clearAllMocks(); mockGetExportTemplates.mockReturnValue([]); - mockGetConnectedIntegrationNamesForPolicies.mockReturnValue(new Set()); }); it('returns empty options and templates when no policies and no export templates', () => { @@ -54,8 +54,8 @@ describe('useExportedToFilterOptions', () => { expect(result.current.connectedIntegrationNames).toEqual(new Set()); }); - it('includes connected integration display name in options when policy has connection', () => { - mockGetConnectedIntegrationNamesForPolicies.mockReturnValue(new Set([CONST.POLICY.CONNECTIONS.NAME.QBO])); + it('includes connected integration display name in options when policy has connection', async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, buildPolicyWithConnection('1', CONST.POLICY.CONNECTIONS.NAME.QBO)); const {result} = renderHook(() => useExportedToFilterOptions()); @@ -111,12 +111,11 @@ describe('useExportedToFilterOptions', () => { expect(result.current.combinedUniqueExportTemplates.at(0)?.templateName).toBe(sameName); }); - it('returns connectedIntegrationNames from getConnectedIntegrationNamesForPolicies', () => { - const connectedSet = new Set([CONST.POLICY.CONNECTIONS.NAME.XERO]); - mockGetConnectedIntegrationNamesForPolicies.mockReturnValue(connectedSet); + it('returns connectedIntegrationNames from getConnectedIntegrationNamesForPolicies', async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}1`, buildPolicyWithConnection('1', CONST.POLICY.CONNECTIONS.NAME.XERO)); const {result} = renderHook(() => useExportedToFilterOptions()); - expect(result.current.connectedIntegrationNames).toBe(connectedSet); + expect(result.current.connectedIntegrationNames).toEqual(new Set([CONST.POLICY.CONNECTIONS.NAME.XERO])); }); }); From 850354ec8c4053af3ac7aff84feac99d74f4e8e5 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 8 Jul 2026 12:08:20 +0800 Subject: [PATCH 16/32] fix test --- tests/ui/SearchPageTest.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ui/SearchPageTest.tsx b/tests/ui/SearchPageTest.tsx index c4da896bc446..839a5a40485a 100644 --- a/tests/ui/SearchPageTest.tsx +++ b/tests/ui/SearchPageTest.tsx @@ -194,6 +194,7 @@ describe('SearchPageNarrow', () => { search: { type: CONST.SEARCH.DATA_TYPES.CHAT, offset: 0, + hash: 929718687, isLoading: false, hasMoreResults: false, }, From b3a90372eee687aa179361517df0089cc337b1b2 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 8 Jul 2026 13:08:29 +0800 Subject: [PATCH 17/32] remove unnecessary wrapper --- .../AdvancedFilters/CommonFilterContent.tsx | 36 ------------------- .../SearchAdvancedFiltersContent.tsx | 3 +- .../CommonFilterContentPopupWrapper.tsx | 4 +-- .../CommonFilterContentPageWrapper.tsx | 4 +-- 4 files changed, 5 insertions(+), 42 deletions(-) delete mode 100644 src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx diff --git a/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx deleted file mode 100644 index f89505bfce71..000000000000 --- a/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type {Filter, SearchFilterCommonProps, SearchTextFilterKeys} from '@components/Search/types'; - -import type {SearchAdvancedFiltersForm} from '@src/types/form'; -import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; - -import React from 'react'; - -import type {FilterComponentsProps} from '..'; - -import FilterComponents from '..'; - -type CommonFilterContentProps = SearchFilterCommonProps> & { - filterKey: FilterComponentsProps['filterKey']; - type: SearchDataTypes | undefined; - policyID: Filter; -}; - -function CommonFilterContent({filterKey, value, type, ready, policyID, autoFocus, selectionListTextInputStyle, selectionListStyle, footer, onChange}: CommonFilterContentProps) { - return ( - - ); -} - -export default CommonFilterContent; -export type {CommonFilterContentProps}; diff --git a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx index e77bfc1e8181..bd347765d06e 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx @@ -8,7 +8,6 @@ import React from 'react'; import type {FilterComponentsProps} from '..'; import type {AmountFilterContentProps} from './AmountFilterContent'; -import type {CommonFilterContentProps} from './CommonFilterContent'; import type {DateFilterContentProps} from './DateFilterContent'; import type {ReportFieldFilterContentProps} from './ReportFieldFilterContent'; import type {TextInputFilterContentProps} from './TextInputFilterContent'; @@ -17,7 +16,7 @@ type TextInputFilterContentWrapperProps = Pick; type DateFilterContentWrapperProps = Pick; type ReportFieldFilterContentWrapperProps = Pick; -type CommonFilterContentWrapperProps = Omit; +type CommonFilterContentWrapperProps = Omit; type SearchAdvancedFiltersContentProps = { filterKey: SearchFilter['key']; values: Partial | undefined; diff --git a/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/CommonFilterContentPopupWrapper.tsx b/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/CommonFilterContentPopupWrapper.tsx index ae8522b25b2a..e92414a4bae4 100644 --- a/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/CommonFilterContentPopupWrapper.tsx +++ b/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/CommonFilterContentPopupWrapper.tsx @@ -1,4 +1,4 @@ -import CommonFilterContent from '@components/Search/FilterComponents/AdvancedFilters/CommonFilterContent'; +import FilterComponents from '@components/Search/FilterComponents'; import type {CommonFilterContentWrapperProps} from '@components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -8,7 +8,7 @@ import React from 'react'; function CommonFilterContentPopupWrapper({filterKey, value, type, policyID, onChange}: CommonFilterContentWrapperProps) { const styles = useThemeStyles(); return ( - (initialValue); return ( - Date: Wed, 8 Jul 2026 13:11:15 +0800 Subject: [PATCH 18/32] lint --- .../Search/FilterComponents/TypeSelector.tsx | 3 ++- .../Search/FilterComponents/WorkspaceSelector.tsx | 3 ++- src/components/Search/FilterComponents/index.tsx | 9 ++++++--- .../SearchPageHeader/useSearchFiltersBar.tsx | 2 +- src/libs/ObjectUtils.ts | 7 ++++++- src/libs/SearchUIUtils.ts | 14 ++++++++------ src/types/utils/hasKey.ts | 5 ----- tests/unit/Search/SearchQueryUtilsTest.ts | 2 +- 8 files changed, 26 insertions(+), 19 deletions(-) delete mode 100644 src/types/utils/hasKey.ts diff --git a/src/components/Search/FilterComponents/TypeSelector.tsx b/src/components/Search/FilterComponents/TypeSelector.tsx index 033443111b2a..81f7f51e59b1 100644 --- a/src/components/Search/FilterComponents/TypeSelector.tsx +++ b/src/components/Search/FilterComponents/TypeSelector.tsx @@ -9,6 +9,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {emailSelector} from '@src/selectors/Session'; import type {Policy} from '@src/types/onyx'; +import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import type {OnyxCollection} from 'react-native-onyx'; @@ -16,7 +17,7 @@ import React from 'react'; import SingleSelect from './SingleSelect'; -type TypeSelectorProps = SearchFilterCommonProps; +type TypeSelectorProps = SearchFilterCommonProps; /** * Extracts only the fields needed by getTypeOptions (canSendInvoice check). diff --git a/src/components/Search/FilterComponents/WorkspaceSelector.tsx b/src/components/Search/FilterComponents/WorkspaceSelector.tsx index c5d96aea5186..793c845b40d6 100644 --- a/src/components/Search/FilterComponents/WorkspaceSelector.tsx +++ b/src/components/Search/FilterComponents/WorkspaceSelector.tsx @@ -28,9 +28,10 @@ import {View} from 'react-native'; import type {MultiSelectItem} from './MultiSelect'; -type WorkspaceSelectorProps = SearchFilterCommonProps; import ListFilterView from './ListFilterViewWrapper'; +type WorkspaceSelectorProps = SearchFilterCommonProps; + function WorkspaceSelector({value = [], selectionListTextInputStyle, selectionListStyle, autoFocus, ready = true, footer, onChange}: WorkspaceSelectorProps) { const {isOffline} = useNetwork(); const {translate} = useLocalize(); diff --git a/src/components/Search/FilterComponents/index.tsx b/src/components/Search/FilterComponents/index.tsx index 738f551da285..617ac7b01e98 100644 --- a/src/components/Search/FilterComponents/index.tsx +++ b/src/components/Search/FilterComponents/index.tsx @@ -1,4 +1,4 @@ -import type {Filter, SearchAmountFilterKeys, SearchDateFilterKeys, SearchFilterCommonProps} from '@components/Search/types'; +import type {Filter, SearchAmountFilterKeys, SearchDateFilterKeys, SearchFilterCommonProps, SearchTextFilterKeys} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; @@ -26,7 +26,7 @@ import TypeSelector from './TypeSelector'; import UserSelector from './UserSelector'; import WorkspaceSelector from './WorkspaceSelector'; -type FilterKeys = Exclude; +type FilterKeys = Exclude; type FilterComponentsProps = SearchFilterCommonProps & { filterKey: FilterKeys; type?: SearchDataTypes; @@ -120,9 +120,12 @@ function FilterComponents({filterKey, value, type, policyID, selectionListTextIn ); } case CONST.SEARCH.SYNTAX_FILTER_KEYS.TYPE: { + const isTypeFilterValue = (v: FilterComponentsProps['value']): v is SearchDataTypes => { + return typeof v === 'string'; + }; return ( { return typeof value === 'object' && value !== null && !Array.isArray(value); } -export {shallowCompare, getObjectValues, filterObject, isRecord, getObjectKeys}; +// eslint-disable-next-line @typescript-eslint/no-restricted-types +function hasKey>(obj: T, key: PropertyKey): key is keyof T { + return key in obj; +} + +export {shallowCompare, getObjectValues, filterObject, isRecord, getObjectKeys, hasKey}; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index d2b92208d108..3a9da15d01ca 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -85,7 +85,6 @@ import type { SearchWithdrawalIDGroup, SearchYearGroup, } from '@src/types/onyx/SearchResults'; -import hasKey from '@src/types/utils/hasKey'; import type IconAsset from '@src/types/utils/IconAsset'; import arraysEqual from '@src/utils/arraysEqual'; @@ -113,6 +112,7 @@ import DateUtils from './DateUtils'; import interceptAnonymousUser from './interceptAnonymousUser'; import isSearchTopmostFullScreenRoute from './Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation from './Navigation/Navigation'; +import {hasKey} from './ObjectUtils'; import Parser from './Parser'; import {getLoginByAccountID, temporaryGetDisplayNameOrDefault} from './PersonalDetailsUtils'; import { @@ -5494,13 +5494,15 @@ function getFilterNegatableValue( isNegated: boolean; value: SearchAdvancedFiltersForm[K] | undefined; } { - if (!isFilterNegatable(filterKey)) { + const negatedFilterKey = `${filterKey}${CONST.SEARCH.NOT_MODIFIER}` as const; + if (!isFilterNegatable(filterKey) || !values || !hasKey(values, negatedFilterKey)) { return {isNegated: false, value: values?.[filterKey]}; } - const negatedFilterKey = `${filterKey}${CONST.SEARCH.NOT_MODIFIER}` as const; - const negatedValue = values && hasKey(values, negatedFilterKey) ? values[negatedFilterKey] : undefined; - const value = negatedValue ?? values?.[filterKey]; - return {isNegated: !!negatedValue, value}; + + // A negatable filter's negated key always shares the value type of its base key (e.g. `policyID` and `policyIDNot` are both `string[]`), + // so read it through a view of the form that reflects that relationship instead of the full value union. + const negatedValues = values as Partial>; + return {isNegated: true, value: negatedValues[negatedFilterKey]}; } function shouldShowFilter(skipFilters: Set | undefined, key: SearchAdvancedFiltersKey, value: ValueOf, type: SearchDataTypes) { diff --git a/src/types/utils/hasKey.ts b/src/types/utils/hasKey.ts deleted file mode 100644 index e1896547bdcd..000000000000 --- a/src/types/utils/hasKey.ts +++ /dev/null @@ -1,5 +0,0 @@ -function hasKey(obj: T, key: PropertyKey): key is keyof T { - return key in obj; -} - -export default hasKey; diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index d4fb49ed994a..3102480fc339 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -43,7 +43,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; import type * as OnyxTypes from '@src/types/onyx'; -import {Connections} from '@src/types/onyx/Policy'; +import type {Connections} from '@src/types/onyx/Policy'; /* eslint-disable @typescript-eslint/naming-convention */ // we need "dirty" object key names in these tests From 0fe57537e6d2bad84dc072861f59d36eea582f51 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 8 Jul 2026 14:21:14 +0800 Subject: [PATCH 19/32] lint --- src/libs/ObjectUtils.ts | 1 - src/libs/SearchUIUtils.ts | 7 ++----- tests/actions/IOU/RequestMoneyTest.ts | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/libs/ObjectUtils.ts b/src/libs/ObjectUtils.ts index c357a3c26415..ac73d2c7f4c2 100644 --- a/src/libs/ObjectUtils.ts +++ b/src/libs/ObjectUtils.ts @@ -42,7 +42,6 @@ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } -// eslint-disable-next-line @typescript-eslint/no-restricted-types function hasKey>(obj: T, key: PropertyKey): key is keyof T { return key in obj; } diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 3a9da15d01ca..83004b7e714b 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -5489,7 +5489,7 @@ function getDisplayValue( function getFilterNegatableValue( filterKey: K, - values: Partial | undefined, + values: (Partial & Partial>) | undefined, ): { isNegated: boolean; value: SearchAdvancedFiltersForm[K] | undefined; @@ -5499,10 +5499,7 @@ function getFilterNegatableValue( return {isNegated: false, value: values?.[filterKey]}; } - // A negatable filter's negated key always shares the value type of its base key (e.g. `policyID` and `policyIDNot` are both `string[]`), - // so read it through a view of the form that reflects that relationship instead of the full value union. - const negatedValues = values as Partial>; - return {isNegated: true, value: negatedValues[negatedFilterKey]}; + return {isNegated: true, value: values[negatedFilterKey]}; } function shouldShowFilter(skipFilters: Set | undefined, key: SearchAdvancedFiltersKey, value: ValueOf, type: SearchDataTypes) { diff --git a/tests/actions/IOU/RequestMoneyTest.ts b/tests/actions/IOU/RequestMoneyTest.ts index 81060af042f5..425e313a73be 100644 --- a/tests/actions/IOU/RequestMoneyTest.ts +++ b/tests/actions/IOU/RequestMoneyTest.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import type {SearchQueryJSON} from '@components/Search/types'; import {clearAllRelatedReportActionErrors} from '@libs/actions/ClearReportActionErrors'; From 945769ab500b3dab2955dcab7aa6e937c5add2d5 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 14:01:07 +0800 Subject: [PATCH 20/32] rename --- src/libs/SearchQueryUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 6cf4ec02fe03..767b2b364ac2 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -438,9 +438,9 @@ function getFilters(queryJSON: SearchQueryJSON) { } function getFilterFromQuery(queryJSON: SearchQueryJSON | undefined, filterKey: SearchAdvancedFiltersKey): Filter { - const policyIDFilters = queryJSON?.flatFilters.find((filter) => filter.key === filterKey)?.filters; - const isNegated = policyIDFilters?.at(0)?.operator === CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO; - const value = policyIDFilters?.map((filter) => filter.value.toString()); + const filters = queryJSON?.flatFilters.find((filter) => filter.key === filterKey)?.filters; + const isNegated = filters?.at(0)?.operator === CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO; + const value = filters?.map((filter) => filter.value.toString()); return {value, isNegated}; } From 9446de4645605af8d0d2b6f671edfbb9d106f419 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 14:02:26 +0800 Subject: [PATCH 21/32] check the length --- src/hooks/useExportedToFilterOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useExportedToFilterOptions.ts b/src/hooks/useExportedToFilterOptions.ts index c6998cf1769f..55421d02c5c5 100644 --- a/src/hooks/useExportedToFilterOptions.ts +++ b/src/hooks/useExportedToFilterOptions.ts @@ -52,7 +52,7 @@ export default function useExportedToFilterOptions(): UseExportedToFilterDataRes const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: exportedToPoliciesSelector}); // When search is scoped to workspaces, use only those policies otherwise use all. - const policiesToUse = policyIDs.value !== undefined ? getAllPolicyValues(policyIDs, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies ?? {}); + const policiesToUse = policyIDs.value?.length ? getAllPolicyValues(policyIDs, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies ?? {}); const policyLevelExportTemplates = policiesToUse.flatMap((policy) => getExportTemplates([], {}, translate, policy, false)); const accountLevelExportTemplates = getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, undefined, true); const combinedExportTemplates = [...accountLevelExportTemplates, ...policyLevelExportTemplates]; From f875d35ef5af6043cabae63db97db89842a181bf Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 14:03:15 +0800 Subject: [PATCH 22/32] combine into 1 if --- src/libs/SearchQueryUtils.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 767b2b364ac2..98488b2c0524 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1198,10 +1198,7 @@ function buildFilterFormValuesFromQuery( const isNegated = filterList.some((item) => item.operator === CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO); - if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID) { - filtersForm[addNegation(filterKey, isNegated)] = filterValues; - } - if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS) { + if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID || filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS) { filtersForm[addNegation(filterKey, isNegated)] = filterValues; } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_ID || filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_ID) { From e155d74053aacde9c29be29f51daa9bbc43b94fc Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 14:22:49 +0800 Subject: [PATCH 23/32] extract logic to s reusable function --- src/libs/SearchUIUtils.ts | 68 ++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index f6894b67d7fa..c246618a0103 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2073,6 +2073,28 @@ function hasVisibleViolations( return hasActionable && hasUserVisible; } +function shouldShowExpense(currentQueryJSON: SearchQueryJSON | undefined, isActionLoading: boolean, report: OnyxEntry, transactionItemReportID?: string) { + if (isActionLoading || currentQueryJSON?.type !== CONST.SEARCH.DATA_TYPES.EXPENSE) { + return true; + } + + const status = getFilterFromQuery(currentQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS); + if (!status.value) { + return true; + } + + if (status.isNegated) { + return Object.keys(expenseStatusActionMapping).some((expenseStatus) => { + const isExcluded = status.value?.includes(expenseStatus); + return !isExcluded && expenseStatusActionMapping[expenseStatus](report, transactionItemReportID); + }); + } + + return status.value.some((expenseStatus) => { + return isValidExpenseStatus(expenseStatus) ? expenseStatusActionMapping[expenseStatus](report, transactionItemReportID) : false; + }); +} + /** * @private * Organizes data into List Sections for display, for the TransactionListItemType of Search Results. @@ -2120,34 +2142,18 @@ function getTransactionsSections({ const transactionItem = data[key]; const report = getReportOrDraftReport(transactionItem.reportID) ?? data[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`]; - let shouldShow = true; - - const isActionLoading = isActionLoadingSet?.has(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${transactionItem.reportID}`); + const isActionLoading = !!isActionLoadingSet?.has(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${transactionItem.reportID}`); // Skip status filtering for the tracked optimistic item so it stays // visible before the server snapshot arrives. Scoped to the specific // transaction ID to avoid leaking unrelated pending items into wrong // status tabs (e.g. offline-queued expenses appearing in "approved"). const isTrackedOptimisticItem = !!optimisticTransactionID && transactionItem.transactionID === optimisticTransactionID; - if (currentQueryJSON && !isActionLoading && !isTrackedOptimisticItem) { - if (currentQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) { - const status = getFilterFromQuery(currentQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS); - if (status.value) { - if (status.isNegated) { - shouldShow = Object.keys(expenseStatusActionMapping).some((expenseStatus) => { - const isExcluded = status.value?.includes(expenseStatus); - return !isExcluded && expenseStatusActionMapping[expenseStatus](report, transactionItem.reportID); - }); - } else { - shouldShow = status.value.some((expenseStatus) => { - return isValidExpenseStatus(expenseStatus) ? expenseStatusActionMapping[expenseStatus](report, transactionItem.reportID) : false; - }); - } - } - } - } + let shouldShow = true; if (!transactionItem.transactionID) { shouldShow = false; + } else if (!isTrackedOptimisticItem) { + shouldShow = shouldShowExpense(currentQueryJSON, isActionLoading, report, transactionItem.reportID); } if (shouldShow) { @@ -2858,26 +2864,8 @@ function getReportSections({ const actions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportItem.reportID}`] ?? Object.values(data[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportItem.reportID}`] ?? {}); - let shouldShow = true; - - const isActionLoading = isActionLoadingSet?.has(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportItem.reportID}`); - if (currentQueryJSON && !isActionLoading) { - if (currentQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) { - const status = getFilterFromQuery(currentQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS); - if (status.value) { - if (status.isNegated) { - shouldShow = Object.keys(expenseStatusActionMapping).some((expenseStatus) => { - const isExcluded = status.value?.includes(expenseStatus); - return !isExcluded && expenseStatusActionMapping[expenseStatus](reportItem); - }); - } else { - shouldShow = status.value.some((expenseStatus) => { - return isValidExpenseStatus(expenseStatus) ? expenseStatusActionMapping[expenseStatus](reportItem) : false; - }); - } - } - } - } + const isActionLoading = !!isActionLoadingSet?.has(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportItem.reportID}`); + const shouldShow = shouldShowExpense(currentQueryJSON, isActionLoading, reportItem); if (shouldShow) { const reportPendingAction = From cb80450781e1679775cb30671807c9eac1e93e70 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 15:06:50 +0800 Subject: [PATCH 24/32] simplify --- .../FilterComponents/CategorySelector.tsx | 29 +++++-------------- .../FilterComponents/ExportedToSelector.tsx | 2 +- .../Search/FilterComponents/TagSelector.tsx | 25 +++------------- src/hooks/useAdvancedSearchFilters.ts | 4 +-- src/hooks/useExportedToFilterOptions.ts | 2 +- src/libs/SearchQueryUtils.ts | 9 +++--- tests/unit/Search/SearchQueryUtilsTest.ts | 8 ++--- 7 files changed, 23 insertions(+), 56 deletions(-) diff --git a/src/components/Search/FilterComponents/CategorySelector.tsx b/src/components/Search/FilterComponents/CategorySelector.tsx index 6285b451285d..766f71e544fa 100644 --- a/src/components/Search/FilterComponents/CategorySelector.tsx +++ b/src/components/Search/FilterComponents/CategorySelector.tsx @@ -4,11 +4,11 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {getDecodedCategoryName} from '@libs/CategoryUtils'; -import {sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils'; +import {getAllPolicyValues, sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyCategories, PolicyCategory} from '@src/types/onyx'; +import type {PolicyCategories} from '@src/types/onyx'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; import type {OnyxCollection} from 'react-native-onyx'; @@ -52,26 +52,11 @@ function CategorySelector({value = [], policyID, selectionListTextInputStyle, se ); const categoryItems = [{text: translate('search.noCategory'), value: CONST.SEARCH.CATEGORY_EMPTY_VALUE as string}]; - const uniqueCategoryNames = new Set(); - if (!policyID?.value?.length) { - const categories = Object.values(allPolicyCategories ?? {}).flatMap((policyCategories) => Object.values(policyCategories ?? {})); - for (const category of categories) { - uniqueCategoryNames.add(category.name); - } - } else { - const selectedCategoryKeys = new Set(policyID.value?.map((id) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${id}`)); - const selectedPoliciesCategories: PolicyCategory[] = Object.keys(allPolicyCategories ?? {}) - .filter((key) => { - const isSelected = selectedCategoryKeys.has(key); - return policyID.isNegated ? !isSelected : isSelected; - }) - .map((key) => Object.values(allPolicyCategories?.[key] ?? {})) - .flat(); - - for (const category of selectedPoliciesCategories) { - uniqueCategoryNames.add(category.name); - } - } + const uniqueCategoryNames = new Set( + getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_CATEGORIES, allPolicyCategories).flatMap((policyCategories) => + Object.values(policyCategories ?? {}).map((category) => category.name), + ), + ); categoryItems.push( ...Array.from(uniqueCategoryNames) .filter(Boolean) diff --git a/src/components/Search/FilterComponents/ExportedToSelector.tsx b/src/components/Search/FilterComponents/ExportedToSelector.tsx index 4649016c2041..3037c172ca16 100644 --- a/src/components/Search/FilterComponents/ExportedToSelector.tsx +++ b/src/components/Search/FilterComponents/ExportedToSelector.tsx @@ -97,7 +97,7 @@ function ExportedToSelector({value = [], policyID, selectionListTextInputStyle, }); const usedPickerValueKeys = new Set(connectedIntegrationPickerItems.map((item) => item.value)); - const policiesToLoadTemplatesFrom = policyID?.value?.length ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies ?? {}); + const policiesToLoadTemplatesFrom = getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, policies); const exportTemplatesFromPolicies = policiesToLoadTemplatesFrom.flatMap((policy) => getExportTemplates([], {}, translate, policy, false)); const exportTemplatesFromAccount = getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, undefined, true); const allExportTemplates = [...exportTemplatesFromAccount, ...exportTemplatesFromPolicies]; diff --git a/src/components/Search/FilterComponents/TagSelector.tsx b/src/components/Search/FilterComponents/TagSelector.tsx index 2c6a773a6576..26c569fde658 100644 --- a/src/components/Search/FilterComponents/TagSelector.tsx +++ b/src/components/Search/FilterComponents/TagSelector.tsx @@ -4,7 +4,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {getCleanedTagName, getTagNamesFromTagsLists} from '@libs/PolicyUtils'; -import {sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils'; +import {getAllPolicyValues, sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -27,26 +27,9 @@ function TagSelector({value = [], policyID, selectionListTextInputStyle, selecti const [allPolicyTagLists = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: passthroughPolicyTagListSelector}); const tagItems = [{text: translate('search.noTag'), value: CONST.SEARCH.TAG_EMPTY_VALUE as string}]; - const uniqueTagNames = new Set(); - if (!policyID?.value?.length) { - const tagListsUnpacked = Object.values(allPolicyTagLists ?? {}).filter((item) => !!item); - for (const tag of tagListsUnpacked.map(getTagNamesFromTagsLists).flat()) { - uniqueTagNames.add(tag); - } - } else { - const selectedTagKeys = new Set(policyID.value?.map((id) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${id}`)); - const selectedPoliciesTagLists = Object.keys(allPolicyTagLists ?? {}) - .filter((key) => { - const isSelected = selectedTagKeys.has(key); - return policyID.isNegated ? !isSelected : isSelected; - }) - .map((key) => getTagNamesFromTagsLists(allPolicyTagLists?.[key] ?? {})) - .flat(); - - for (const tag of selectedPoliciesTagLists) { - uniqueTagNames.add(tag); - } - } + const uniqueTagNames = new Set( + getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_TAGS, allPolicyTagLists).flatMap((policyTags) => getTagNamesFromTagsLists(policyTags ?? {})), + ); tagItems.push( ...Array.from(uniqueTagNames) .map((tagName) => ({text: getCleanedTagName(tagName), value: tagName})) diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts index ae571fa45376..3ff62d7f8ce8 100644 --- a/src/hooks/useAdvancedSearchFilters.ts +++ b/src/hooks/useAdvancedSearchFilters.ts @@ -327,9 +327,9 @@ function useAdvancedSearchFilters(type: SearchDataTypes | undefined, policyID: F const [allPolicyCategories = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, { selector: availablePolicyCategoriesSelector, }); - const selectedPolicyCategories = getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_CATEGORIES, allPolicyCategories); + const selectedPolicyCategories = policyID?.value?.length ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_CATEGORIES, allPolicyCategories) : []; const [allPolicyTagLists = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: passthroughPolicyTagListSelector}); - const selectedPolicyTagLists = getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_TAGS, allPolicyTagLists); + const selectedPolicyTagLists = policyID?.value?.length ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_TAGS, allPolicyTagLists) : []; const [hasTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: hasTagsSelector}); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); diff --git a/src/hooks/useExportedToFilterOptions.ts b/src/hooks/useExportedToFilterOptions.ts index 55421d02c5c5..fec7b85ba887 100644 --- a/src/hooks/useExportedToFilterOptions.ts +++ b/src/hooks/useExportedToFilterOptions.ts @@ -52,7 +52,7 @@ export default function useExportedToFilterOptions(): UseExportedToFilterDataRes const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: exportedToPoliciesSelector}); // When search is scoped to workspaces, use only those policies otherwise use all. - const policiesToUse = policyIDs.value?.length ? getAllPolicyValues(policyIDs, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies ?? {}); + const policiesToUse = getAllPolicyValues(policyIDs, ONYXKEYS.COLLECTION.POLICY, policies); const policyLevelExportTemplates = policiesToUse.flatMap((policy) => getExportTemplates([], {}, translate, policy, false)); const accountLevelExportTemplates = getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, undefined, true); const combinedExportTemplates = [...accountLevelExportTemplates, ...policyLevelExportTemplates]; diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 98488b2c0524..62bb3018b31d 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1035,7 +1035,7 @@ function getAllPolicyValues( policyData: OnyxCollection, ): Array { if (!policyData || !policyID || !policyID.value) { - return []; + return Object.values(policyData ?? {}).filter((data): data is NonNullable => !!data); } if (policyID.isNegated) { @@ -1098,8 +1098,7 @@ function getConnectedIntegrationNamesForPolicies(policies: OnyxCollection(); - const hasWorkspaceFilter = !!policyID?.value?.length; - const policiesToCheck = hasWorkspaceFilter ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, policies) : Object.values(policies); + const policiesToCheck = getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, policies); for (const policy of policiesToCheck) { const connectedIntegration = getValidConnectedIntegration(policy, CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES); @@ -1315,7 +1314,7 @@ function buildFilterFormValuesFromQuery( } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG) { const uniqueTags = new Set(); - const tagLists = policyID.value ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_TAGS, policyTags) : Object.values(policyTags ?? {}).filter((item) => !!item); + const tagLists = getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_TAGS, policyTags); for (const tagList of tagLists) { for (const policyTagList of Object.values(tagList ?? {})) { for (const tag of Object.values(policyTagList.tags ?? {})) { @@ -1328,7 +1327,7 @@ function buildFilterFormValuesFromQuery( } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY) { const uniqueCategories = new Set(); - const categoryLists = policyID.value ? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_CATEGORIES, policyCategories) : Object.values(policyCategories ?? {}); + const categoryLists = getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_CATEGORIES, policyCategories); for (const item of categoryLists) { for (const category of Object.values(item ?? {})) { uniqueCategories.add(category.name); diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 3102480fc339..de981e99ada0 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -3644,12 +3644,12 @@ describe('SearchQueryUtils', () => { expect(result).toEqual([policy1]); }); - test('returns an empty array when the filter is undefined', () => { - expect(getAllPolicyValues(undefined, ONYXKEYS.COLLECTION.POLICY, policyData)).toEqual([]); + test('returns every policy value when the filter is undefined', () => { + expect(getAllPolicyValues(undefined, ONYXKEYS.COLLECTION.POLICY, policyData)).toEqual([policy1, policy2, policy3]); }); - test('returns an empty array when the filter has no value', () => { - expect(getAllPolicyValues({value: undefined, isNegated: false}, ONYXKEYS.COLLECTION.POLICY, policyData)).toEqual([]); + test('returns every policy value when the filter has no value', () => { + expect(getAllPolicyValues({value: undefined, isNegated: false}, ONYXKEYS.COLLECTION.POLICY, policyData)).toEqual([policy1, policy2, policy3]); }); test('returns an empty array when the policy data is undefined', () => { From 6ca99da13e474dbdb56a8a37323a0952e6eb45c6 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 15:23:20 +0800 Subject: [PATCH 25/32] remove status ALL usages --- src/CONST/index.ts | 5 -- src/libs/CardNavigationUtils.ts | 2 +- src/libs/SearchQueryUtils.ts | 4 +- src/libs/SearchUIUtils.ts | 1 - src/libs/actions/IOU/SearchUpdate.ts | 1 - .../settings/Wallet/WalletPage/index.tsx | 1 - tests/actions/IOUTest/SplitSelfDMTest.ts | 2 +- tests/unit/Search/SearchUIUtilsTest.ts | 48 ++++++++++--------- tests/unit/Search/useSearchSnapshotTest.ts | 1 - tests/unit/hooks/useAllTransactions.test.ts | 7 --- .../hooks/useSearchBulkActionsDeleteTest.ts | 1 - tests/unit/hooks/useSearchSections.test.ts | 6 +-- 12 files changed, 31 insertions(+), 48 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 9af816b4bc73..866dec2120e5 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6779,7 +6779,6 @@ const CONST = { }, STATUS: { EXPENSE: { - ALL: '', UNREPORTED: 'unreported', DRAFTS: 'drafts', OUTSTANDING: 'outstanding', @@ -6789,7 +6788,6 @@ const CONST = { DELETED: 'deleted', }, EXPENSE_REPORT: { - ALL: '', DRAFTS: 'drafts', OUTSTANDING: 'outstanding', APPROVED: 'approved', @@ -6797,18 +6795,15 @@ const CONST = { PAID: 'paid', }, INVOICE: { - ALL: '', OUTSTANDING: 'outstanding', PAID: 'paid', }, TRIP: { - ALL: '', CURRENT: 'current', PAST: 'past', }, CHAT: {}, TASK: { - ALL: '', OUTSTANDING: 'outstanding', COMPLETED: 'completed', }, diff --git a/src/libs/CardNavigationUtils.ts b/src/libs/CardNavigationUtils.ts index 9020a061eca5..836f55b3b323 100644 --- a/src/libs/CardNavigationUtils.ts +++ b/src/libs/CardNavigationUtils.ts @@ -7,7 +7,7 @@ import {buildCannedSearchQuery} from './SearchQueryUtils'; function navigateToCardTransactions(cardID: string) { Navigation.navigate( ROUTES.SEARCH_ROOT.getRoute({ - query: buildCannedSearchQuery({type: CONST.SEARCH.DATA_TYPES.EXPENSE, status: CONST.SEARCH.STATUS.EXPENSE.ALL, cardID}), + query: buildCannedSearchQuery({type: CONST.SEARCH.DATA_TYPES.EXPENSE, cardID}), }), ); } diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 62bb3018b31d..7813d9cae785 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1956,18 +1956,16 @@ function buildUserReadableQueryString({ */ function buildCannedSearchQuery({ type = CONST.SEARCH.DATA_TYPES.EXPENSE, - status, policyID, cardID, groupBy, }: { type?: SearchDataTypes; - status?: SearchStatus; policyID?: string; cardID?: string; groupBy?: string; } = {}): SearchQueryString { - let queryString = status ? `type:${type} status:${Array.isArray(status) ? status.join(',') : status}` : `type:${type}`; + let queryString = `type:${type}`; if (groupBy) { queryString += ` group-by:${groupBy}`; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index c246618a0103..d0094dc9cdc2 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -414,7 +414,6 @@ const expenseStatusActionMapping: Record = { [CONST.SEARCH.STATUS.EXPENSE.DONE]: (expenseReport) => expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport.statusNum === CONST.REPORT.STATUS_NUM.CLOSED, [CONST.SEARCH.STATUS.EXPENSE.UNREPORTED]: (expenseReport, transactionReportID) => !expenseReport && transactionReportID !== CONST.REPORT.TRASH_REPORT_ID, [CONST.SEARCH.STATUS.EXPENSE.DELETED]: (_expenseReport, transactionReportID) => transactionReportID === CONST.REPORT.TRASH_REPORT_ID, - [CONST.SEARCH.STATUS.EXPENSE.ALL]: () => true, }; const nonSortableColumns = new Set([ diff --git a/src/libs/actions/IOU/SearchUpdate.ts b/src/libs/actions/IOU/SearchUpdate.ts index cd03b330e803..75a8ab695134 100644 --- a/src/libs/actions/IOU/SearchUpdate.ts +++ b/src/libs/actions/IOU/SearchUpdate.ts @@ -29,7 +29,6 @@ const expenseReportStatusFilterMapping: Record expenseReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && expenseReport?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED, [CONST.SEARCH.STATUS.EXPENSE.UNREPORTED]: (expenseReport, transactionReportID) => !expenseReport && transactionReportID !== CONST.REPORT.TRASH_REPORT_ID, [CONST.SEARCH.STATUS.EXPENSE.DELETED]: (_expenseReport, transactionReportID) => transactionReportID === CONST.REPORT.TRASH_REPORT_ID, - [CONST.SEARCH.STATUS.EXPENSE.ALL]: () => true, }; type GetSearchOnyxUpdateParams = { diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index a7083cfd427b..8e8703cd21d4 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -625,7 +625,6 @@ function WalletPage() { ROUTES.SEARCH_ROOT.getRoute({ query: buildCannedSearchQuery({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, cardID: String(paymentMethod.methodID), }), }), diff --git a/tests/actions/IOUTest/SplitSelfDMTest.ts b/tests/actions/IOUTest/SplitSelfDMTest.ts index 235b9549e353..6e5e9b203570 100644 --- a/tests/actions/IOUTest/SplitSelfDMTest.ts +++ b/tests/actions/IOUTest/SplitSelfDMTest.ts @@ -327,7 +327,7 @@ describe('updateSplitTransactionsFromSplitExpensesFlow - selfDM', () => { const originalTransactionSnapshotKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransaction.transactionID}`; await Onyx.merge(snapshotKey, { data: {[originalTransactionSnapshotKey]: originalTransaction}, - search: {type: CONST.SEARCH.DATA_TYPES.EXPENSE, status: CONST.SEARCH.STATUS.EXPENSE.ALL, isLoading: false}, + search: {type: CONST.SEARCH.DATA_TYPES.EXPENSE, isLoading: false}, } as unknown as SearchResults); await waitForBatchedUpdates(); diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 711aad21217d..a48aad01cf60 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -5638,21 +5638,23 @@ describe('SearchUIUtils', () => { const TEST_QUERY_HASH = 99999; const expenseType: typeof CONST.SEARCH.DATA_TYPES.EXPENSE = CONST.SEARCH.DATA_TYPES.EXPENSE; - function makeExpenseQueryJSON(status: string | string[], isNegated = false) { - const statusValues = Array.isArray(status) ? status : [status]; + function makeExpenseQueryJSON(status: string[] | undefined, isNegated = false) { const operator = isNegated ? CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO : CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO; + const flatFilters = status + ? [ + { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, + filters: status.map((value) => ({operator, value})), + }, + ] + : undefined; return { type: expenseType, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, view: CONST.SEARCH.VIEW.TABLE, hash: TEST_QUERY_HASH, - flatFilters: [ - { - key: CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS, - filters: statusValues.map((value) => ({operator, value})), - }, - ], + flatFilters, inputQuery: 'type:expense' as const, recentSearchHash: TEST_QUERY_HASH, similarSearchHash: TEST_QUERY_HASH, @@ -5705,13 +5707,13 @@ describe('SearchUIUtils', () => { it('should include transactions when queryJSON status matches report state (DRAFTS)', () => { const data = makeFilterTestData({stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN}); - const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.DRAFTS)}); + const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.DRAFTS])}); expect(sections.some((s) => s.transactionID === filterTestTxID)).toBe(true); }); it('should exclude transactions when queryJSON status does not match report state', () => { const data = makeFilterTestData({stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN}); - const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING)}); + const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING])}); expect(sections.some((s) => s.transactionID === filterTestTxID)).toBe(false); }); @@ -5733,7 +5735,7 @@ describe('SearchUIUtils', () => { it('should exclude transactions when queryJSON status is an invalid string', () => { const data = makeFilterTestData({stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN}); - const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON('not_a_valid_status')}); + const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON(['not_a_valid_status'])}); expect(sections.some((s) => s.transactionID === filterTestTxID)).toBe(false); }); @@ -5745,14 +5747,14 @@ describe('SearchUIUtils', () => { it('should include transactions when queryJSON status is ALL', () => { const data = makeFilterTestData({stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED}); - const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.ALL)}); + const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON(undefined)}); expect(sections.some((s) => s.transactionID === filterTestTxID)).toBe(true); }); it('should include transactions when negated status excludes a different status than the report state', () => { // Report is OUTSTANDING but we negate DRAFTS, so OUTSTANDING is not excluded and the transaction is shown. const data = makeFilterTestData({stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED}); - const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.DRAFTS, true)}); + const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.DRAFTS], true)}); expect(sections.some((s) => s.transactionID === filterTestTxID)).toBe(true); }); @@ -5769,7 +5771,7 @@ describe('SearchUIUtils', () => { // Report is OUTSTANDING and we negate both OUTSTANDING and ALL, so no non-excluded predicate matches and the transaction is hidden. const data = makeFilterTestData({stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED}); const [sections] = callGetTransactionsSections(data, { - queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING, CONST.SEARCH.STATUS.EXPENSE.ALL], true), + queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], true), }); expect(sections.some((s) => s.transactionID === filterTestTxID)).toBe(false); }); @@ -5784,7 +5786,7 @@ describe('SearchUIUtils', () => { reportID: 'nonexistent-report', }, }; - const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.UNREPORTED)}); + const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.UNREPORTED])}); expect(sections.some((s) => s.transactionID === unreportedTxID)).toBe(true); }); @@ -5798,7 +5800,7 @@ describe('SearchUIUtils', () => { for (const {status, stateNum, statusNum} of statusToReportState) { const data = makeFilterTestData({stateNum, statusNum}); - const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON(status)}); + const [sections] = callGetTransactionsSections(data, {queryJSON: makeExpenseQueryJSON([status])}); expect(sections.some((s) => s.transactionID === filterTestTxID)).toBe(true); } }); @@ -5807,7 +5809,7 @@ describe('SearchUIUtils', () => { const data = makeFilterTestData({stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN}); const loadingSet = new Set([`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${filterTestReportID}`]); const [sections] = callGetTransactionsSections(data, { - queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING), + queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]), isActionLoadingSet: loadingSet, }); expect(sections.some((s) => s.transactionID === filterTestTxID)).toBe(true); @@ -5988,13 +5990,13 @@ describe('SearchUIUtils', () => { it('should include report when queryJSON status matches report state (DRAFTS)', () => { const data = makeReportFilterTestData({stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN, type: CONST.REPORT.TYPE.EXPENSE}); - const [sections] = callGetReportSections(data, {queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.DRAFTS)}); + const [sections] = callGetReportSections(data, {queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.DRAFTS])}); expect(sections.some((s) => s.keyForList === rptFilterReportID)).toBe(true); }); it('should exclude report when queryJSON status does not match', () => { const data = makeReportFilterTestData({stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN, type: CONST.REPORT.TYPE.EXPENSE}); - const [sections] = callGetReportSections(data, {queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING)}); + const [sections] = callGetReportSections(data, {queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING])}); expect(sections.some((s) => s.keyForList === rptFilterReportID)).toBe(false); }); @@ -6017,7 +6019,7 @@ describe('SearchUIUtils', () => { it('should include report when negated status excludes a different status than the report state', () => { // Report is OUTSTANDING but we negate DRAFTS, so OUTSTANDING is not excluded and the report is shown. const data = makeReportFilterTestData({stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, type: CONST.REPORT.TYPE.EXPENSE}); - const [sections] = callGetReportSections(data, {queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.DRAFTS, true)}); + const [sections] = callGetReportSections(data, {queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.DRAFTS], true)}); expect(sections.some((s) => s.keyForList === rptFilterReportID)).toBe(true); }); @@ -6034,7 +6036,7 @@ describe('SearchUIUtils', () => { // Report is OUTSTANDING and we negate both OUTSTANDING and ALL, so no non-excluded predicate matches and the report is hidden. const data = makeReportFilterTestData({stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, type: CONST.REPORT.TYPE.EXPENSE}); const [sections] = callGetReportSections(data, { - queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING, CONST.SEARCH.STATUS.EXPENSE.ALL], true), + queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], true), }); expect(sections.some((s) => s.keyForList === rptFilterReportID)).toBe(false); }); @@ -6043,7 +6045,7 @@ describe('SearchUIUtils', () => { const data = makeReportFilterTestData({stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN, type: CONST.REPORT.TYPE.EXPENSE}); const loadingSet = new Set([`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${rptFilterReportID}`]); const [sections] = callGetReportSections(data, { - queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING), + queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING]), isActionLoadingSet: loadingSet, }); expect(sections.some((s) => s.keyForList === rptFilterReportID)).toBe(true); @@ -6051,7 +6053,7 @@ describe('SearchUIUtils', () => { it('should drop transactions when their parent report is filtered out', () => { const data = makeReportFilterTestData({stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN, type: CONST.REPORT.TYPE.EXPENSE}); - const [sections] = callGetReportSections(data, {queryJSON: makeExpenseQueryJSON(CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING)}); + const [sections] = callGetReportSections(data, {queryJSON: makeExpenseQueryJSON([CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING])}); expect(sections.some((s) => s.keyForList === rptFilterReportID)).toBe(false); const allTxIDs = sections.flatMap((s) => s.transactions?.map((t) => t.transactionID) ?? []); expect(allTxIDs).not.toContain(rptFilterTxID); diff --git a/tests/unit/Search/useSearchSnapshotTest.ts b/tests/unit/Search/useSearchSnapshotTest.ts index b767a84419eb..a2c0b2531f1f 100644 --- a/tests/unit/Search/useSearchSnapshotTest.ts +++ b/tests/unit/Search/useSearchSnapshotTest.ts @@ -114,7 +114,6 @@ function makeQueryJSON(overrides: Partial = {}): SearchQueryJSO const base = { hash: HASH, type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, ...overrides, diff --git a/tests/unit/hooks/useAllTransactions.test.ts b/tests/unit/hooks/useAllTransactions.test.ts index 06c01f9c9109..36c3980b3198 100644 --- a/tests/unit/hooks/useAllTransactions.test.ts +++ b/tests/unit/hooks/useAllTransactions.test.ts @@ -61,7 +61,6 @@ describe('useAllTransactions', () => { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, hasMoreResults: false, hasResults: false, isLoading: false, @@ -90,7 +89,6 @@ describe('useAllTransactions', () => { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, hasMoreResults: false, hasResults: true, isLoading: false, @@ -126,7 +124,6 @@ describe('useAllTransactions', () => { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, hasMoreResults: false, hasResults: true, isLoading: false, @@ -155,7 +152,6 @@ describe('useAllTransactions', () => { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, hasMoreResults: false, hasResults: true, isLoading: false, @@ -184,7 +180,6 @@ describe('useAllTransactions', () => { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, hasMoreResults: false, hasResults: false, isLoading: false, @@ -209,7 +204,6 @@ describe('useAllTransactions', () => { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, hasMoreResults: false, hasResults: true, isLoading: false, @@ -274,7 +268,6 @@ describe('useAllTransactions', () => { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, hasMoreResults: false, hasResults: true, isLoading: false, diff --git a/tests/unit/hooks/useSearchBulkActionsDeleteTest.ts b/tests/unit/hooks/useSearchBulkActionsDeleteTest.ts index 73aac107213a..c60898ce4a21 100644 --- a/tests/unit/hooks/useSearchBulkActionsDeleteTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsDeleteTest.ts @@ -351,7 +351,6 @@ describe('useSearchBulkActions - delete unreported expenses', () => { mockCurrentSearchResults = { search: { type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, offset: 0, hasMoreResults: false, hasResults: true, diff --git a/tests/unit/hooks/useSearchSections.test.ts b/tests/unit/hooks/useSearchSections.test.ts index a8d82c2d7360..ce04f7d294c9 100644 --- a/tests/unit/hooks/useSearchSections.test.ts +++ b/tests/unit/hooks/useSearchSections.test.ts @@ -63,7 +63,7 @@ describe('useSearchSections', () => { mockGetSortedSections.mockReturnValue([{reportID: '1'}, {reportID: '2'}]); mockGetSections.mockReturnValue([[{reportID: '1'}, {reportID: '2'}]]); onyxData[ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY] = { - queryJSON: {hash: '123', type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, status: CONST.SEARCH.STATUS.EXPENSE.ALL}, + queryJSON: {hash: '123', type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT}, }; onyxData[`${ONYXKEYS.COLLECTION.SNAPSHOT}123`] = { data: {reports: {}}, @@ -80,7 +80,7 @@ describe('useSearchSections', () => { mockGetSortedSections.mockReturnValue([{reportID: '1'}, {reportID: '2'}, {reportID: '3'}]); mockGetSections.mockReturnValue([[{reportID: '1'}, {reportID: '2'}, {reportID: '3'}]]); onyxData[ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY] = { - queryJSON: {hash: '123', type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, status: CONST.SEARCH.STATUS.EXPENSE.ALL}, + queryJSON: {hash: '123', type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT}, }; onyxData[`${ONYXKEYS.COLLECTION.SNAPSHOT}123`] = { data: {reports: {}}, @@ -97,7 +97,7 @@ describe('useSearchSections', () => { it('returns empty allReports when search results are not yet loaded', () => { onyxData[ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY] = { - queryJSON: {hash: '123', type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, status: CONST.SEARCH.STATUS.EXPENSE.ALL}, + queryJSON: {hash: '123', type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT}, }; // No snapshot data — simulates deep-link before search has run From f9210612eb151e68ef577b9131c42b402aab0150 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 15:33:34 +0800 Subject: [PATCH 26/32] make it more reusable --- src/libs/SearchUIUtils.ts | 13 +++++-------- src/libs/actions/IOU/SearchUpdate.ts | 18 ++---------------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index d0094dc9cdc2..e99bc17acb54 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2072,11 +2072,7 @@ function hasVisibleViolations( return hasActionable && hasUserVisible; } -function shouldShowExpense(currentQueryJSON: SearchQueryJSON | undefined, isActionLoading: boolean, report: OnyxEntry, transactionItemReportID?: string) { - if (isActionLoading || currentQueryJSON?.type !== CONST.SEARCH.DATA_TYPES.EXPENSE) { - return true; - } - +function isEligibleForStatus(currentQueryJSON: SearchQueryJSON | undefined, report: OnyxEntry, transactionItemReportID?: string) { const status = getFilterFromQuery(currentQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS); if (!status.value) { return true; @@ -2151,8 +2147,8 @@ function getTransactionsSections({ if (!transactionItem.transactionID) { shouldShow = false; - } else if (!isTrackedOptimisticItem) { - shouldShow = shouldShowExpense(currentQueryJSON, isActionLoading, report, transactionItem.reportID); + } else if (!isActionLoading && currentQueryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE && !isTrackedOptimisticItem) { + shouldShow = isEligibleForStatus(currentQueryJSON, report, transactionItem.reportID); } if (shouldShow) { @@ -2864,7 +2860,7 @@ function getReportSections({ reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportItem.reportID}`] ?? Object.values(data[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportItem.reportID}`] ?? {}); const isActionLoading = !!isActionLoadingSet?.has(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportItem.reportID}`); - const shouldShow = shouldShowExpense(currentQueryJSON, isActionLoading, reportItem); + const shouldShow = !isActionLoading && currentQueryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE ? isEligibleForStatus(currentQueryJSON, reportItem) : true; if (shouldShow) { const reportPendingAction = @@ -6564,6 +6560,7 @@ export { hasFlexColumn, isTransactionSearchType, splitGroupsIntoPairs, + isEligibleForStatus, SKIPPED_SEARCH_FILTERS, }; export type {SavedSearchMenuItem, SearchTypeMenuSection, SearchTypeMenuItem, SearchDateModifier, SearchDateModifierLower, SearchKey, GroupBySection, SearchFilter}; diff --git a/src/libs/actions/IOU/SearchUpdate.ts b/src/libs/actions/IOU/SearchUpdate.ts index 75a8ab695134..2f20191c58a7 100644 --- a/src/libs/actions/IOU/SearchUpdate.ts +++ b/src/libs/actions/IOU/SearchUpdate.ts @@ -2,7 +2,7 @@ import type {SearchQueryJSON} from '@components/Search/types'; import {isExpenseReport, isOptimisticPersonalDetail} from '@libs/ReportUtils'; import {buildSearchQueryJSON, buildSearchQueryString, getCurrentSearchQueryJSON, getFilterFromQuery} from '@libs/SearchQueryUtils'; -import {getSuggestedSearches} from '@libs/SearchUIUtils'; +import {getSuggestedSearches, isEligibleForStatus} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -57,21 +57,6 @@ function shouldOptimisticallyUpdateSearch( ) { return false; } - let shouldOptimisticallyUpdateByStatus = true; - const status = getFilterFromQuery(currentSearchQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS); - const transactionReportID = transaction?.reportID; - if (status.value) { - if (status.isNegated) { - shouldOptimisticallyUpdateByStatus = Object.keys(expenseReportStatusFilterMapping).some((val) => { - const isExcluded = status.value?.includes(val); - return !isExcluded && expenseReportStatusFilterMapping[val](iouReport, transactionReportID); - }); - } else { - shouldOptimisticallyUpdateByStatus = status.value.some((val) => { - return expenseReportStatusFilterMapping[val](iouReport, transactionReportID); - }); - } - } const currentSearchPolicyIDs = getFilterFromQuery(currentSearchQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); if (currentSearchPolicyIDs.value?.length && iouReport?.policyID) { @@ -84,6 +69,7 @@ function shouldOptimisticallyUpdateSearch( } } + const shouldOptimisticallyUpdateByStatus = isEligibleForStatus(currentSearchQueryJSON, iouReport, transaction?.reportID); if (!shouldOptimisticallyUpdateByStatus) { return false; } From 7ec66599486ee1b1095d3a63d57bbf59ebb30e9d Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 17:39:03 +0800 Subject: [PATCH 27/32] preserve the query hashes --- src/libs/SearchQueryUtils.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 7813d9cae785..74781681ef8c 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -544,8 +544,17 @@ function wasViewExplicitlySet(queryJSON?: SearchQueryJSON | Readonly(orderedQuery); // Certain filters shouldn't affect whether two searchers are similar or not, since they dont @@ -566,6 +575,11 @@ function getQueryHashes(query: SearchQueryJSON) { .sort((a, b) => customCollator.compare(a.filterString, b.filterString)); for (const {filterString, filterKey} of flatFilters) { + // Skip the status and policyID filter because we already handle it above + if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS || filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID) { + continue; + } + if (!similarSearchIgnoredFilters.has(filterKey)) { filterSet.add(filterKey); } From da45e905b51a82904a3411438b8a2fb077322445 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 17:56:42 +0800 Subject: [PATCH 28/32] fix typecheck --- src/hooks/useAutocompleteSuggestions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useAutocompleteSuggestions.ts b/src/hooks/useAutocompleteSuggestions.ts index eba6388bbc80..a1e60b888d2d 100644 --- a/src/hooks/useAutocompleteSuggestions.ts +++ b/src/hooks/useAutocompleteSuggestions.ts @@ -360,7 +360,7 @@ function useAutocompleteSuggestions({ default: suggestedStatuses = DEFAULT_STATUS_VALUES; } - return suggestedStatuses.filter((value) => value !== '').map((value) => getUserFriendlyValue(value)); + return suggestedStatuses.map(getUserFriendlyValue); })(); const filteredStatuses = statusAutocompleteList .filter((status) => status.includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.has(status)) From 04e1251dc48a9da8392fcc254cb547255d3d58b7 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 18:21:09 +0800 Subject: [PATCH 29/32] fix test --- tests/actions/IOU/SearchUpdateTest.ts | 18 +++++++++--------- tests/ui/SearchPageTest.tsx | 2 +- tests/unit/Search/SearchUIUtilsTest.ts | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/actions/IOU/SearchUpdateTest.ts b/tests/actions/IOU/SearchUpdateTest.ts index c5f22275fbfa..4fb39e74d209 100644 --- a/tests/actions/IOU/SearchUpdateTest.ts +++ b/tests/actions/IOU/SearchUpdateTest.ts @@ -176,9 +176,9 @@ describe('actions/IOU', () => { ], }, ], - hash: 344995086, - recentSearchHash: 1106848141, - similarSearchHash: 1135147670, + hash: 939629734, + recentSearchHash: 1023339253, + similarSearchHash: 1855682507, } as SearchQueryJSON; const iouReport: Report = {...createRandomReport(2, undefined), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN}; @@ -232,10 +232,10 @@ describe('actions/IOU', () => { ], }, ], - hash: 1343227670, + hash: 1685631874, inputQuery: 'sortBy:date sortOrder:desc type:expense-report action:approve to:20671314', - recentSearchHash: 1216776837, - similarSearchHash: 911924256, + recentSearchHash: 244251677, + similarSearchHash: 1539858783, } as SearchQueryJSON; const iouReport: Report = {...createRandomReport(2, undefined), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN}; @@ -295,10 +295,10 @@ describe('actions/IOU', () => { ], }, ], - hash: 280939045, + hash: 1967417738, inputQuery: 'sortBy:date sortOrder:desc type:expense groupBy:from status:drafts,outstanding reimbursable:yes', - recentSearchHash: 2045056096, - similarSearchHash: 1931622284, + recentSearchHash: 1043581824, + similarSearchHash: 1832274510, } as SearchQueryJSON; const iouReport: Report = {...createRandomReport(2, undefined), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN}; diff --git a/tests/ui/SearchPageTest.tsx b/tests/ui/SearchPageTest.tsx index 839a5a40485a..80f7f450d0de 100644 --- a/tests/ui/SearchPageTest.tsx +++ b/tests/ui/SearchPageTest.tsx @@ -194,7 +194,7 @@ describe('SearchPageNarrow', () => { search: { type: CONST.SEARCH.DATA_TYPES.CHAT, offset: 0, - hash: 929718687, + hash: failedQueryJSON?.hash, isLoading: false, hasMoreResults: false, }, diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index a48aad01cf60..c44c6457140f 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -5647,7 +5647,7 @@ describe('SearchUIUtils', () => { filters: status.map((value) => ({operator, value})), }, ] - : undefined; + : []; return { type: expenseType, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, From 5c0c385d5627830c1b2fe4df8f99973e1d38f95c Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 19:51:41 +0800 Subject: [PATCH 30/32] remove unused import --- src/libs/SearchQueryUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 74781681ef8c..bfb2dc086470 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -15,7 +15,6 @@ import type { SearchFilterKey, SearchQueryJSON, SearchQueryString, - SearchStatus, SearchWithdrawalType, SyntaxFilterKey, UserFriendlyKey, From fedacb2c3c9c14cc39cc8b316c1bfe1c48ad5b52 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 20:07:42 +0800 Subject: [PATCH 31/32] remove unused export --- src/components/Search/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 34db3b937d1b..0718fae07541 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -454,7 +454,6 @@ export type { SearchDateKey, SearchAmountFilterKeys, SearchAmountValues, - SearchStatus, SearchQueryJSON, SearchQueryString, ReportFieldKey, From 0e4310515d4057ddf8b7eeaeafa772d4da35609c Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 9 Jul 2026 20:47:30 +0800 Subject: [PATCH 32/32] lint --- src/components/Search/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 0718fae07541..489594ab7e5a 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -155,7 +155,6 @@ type InvoiceSearchStatus = ValueOf; type TripSearchStatus = ValueOf; type TaskSearchStatus = ValueOf; type SingularSearchStatus = ExpenseSearchStatus | ExpenseReportSearchStatus | InvoiceSearchStatus | TripSearchStatus | TaskSearchStatus; -type SearchStatus = SingularSearchStatus | SingularSearchStatus[]; type SearchGroupBy = ValueOf; type SearchView = ValueOf; // PieChart is not implemented so we exclude it here to prevent TypeScript errors in `SearchChartView.tsx`.