diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 746a7040104f..ac782196b13f 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6837,7 +6837,6 @@ const CONST = { }, STATUS: { EXPENSE: { - ALL: '', UNREPORTED: 'unreported', DRAFTS: 'drafts', OUTSTANDING: 'outstanding', @@ -6847,7 +6846,6 @@ const CONST = { DELETED: 'deleted', }, EXPENSE_REPORT: { - ALL: '', DRAFTS: 'drafts', OUTSTANDING: 'outstanding', APPROVED: 'approved', @@ -6855,18 +6853,15 @@ const CONST = { PAID: 'paid', }, INVOICE: { - ALL: '', OUTSTANDING: 'outstanding', PAID: 'paid', }, TRIP: { - ALL: '', CURRENT: 'current', PAST: 'past', }, CHAT: {}, TASK: { - ALL: '', OUTSTANDING: 'outstanding', COMPLETED: 'completed', }, @@ -6956,7 +6951,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 deleted file mode 100644 index 0ea07b60feb9..000000000000 --- a/src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type {SearchFilterCommonProps} from '@components/Search/types'; - -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; - policyIDs: string[] | undefined; - policyIDQuery: string[] | undefined; -}; - -function CommonFilterContent({ - filterKey, - value, - type, - ready, - policyIDs, - policyIDQuery, - autoFocus, - selectionListTextInputStyle, - selectionListStyle, - footer, - onChange, -}: CommonFilterContentProps) { - return ( - - ); -} - -export default CommonFilterContent; -export type {CommonFilterContentProps}; diff --git a/src/components/Search/FilterComponents/AdvancedFilters/FilterList.tsx b/src/components/Search/FilterComponents/AdvancedFilters/FilterList.tsx index 5560f8a9d0ad..0c0aef3d8a6a 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/FilterList.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/FilterList.tsx @@ -1,6 +1,7 @@ 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'; @@ -33,7 +34,7 @@ type FilterItemCallbacks = { type FilterListProps = FilterItemCallbacks & { type: SearchDataTypes | undefined; - policyID: string[] | undefined; + policyID: Filter; selectedFilter?: SearchFilter['key']; style?: StyleProp; contentContainerStyle?: StyleProp; diff --git a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx index 2e1994618438..bd347765d06e 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx @@ -1,4 +1,4 @@ -import {isAmountFilterKey, isDateFilterKey, isTextFilterKey} from '@libs/SearchUIUtils'; +import {getFilterNegatableValue, isAmountFilterKey, isDateFilterKey, isTextFilterKey} from '@libs/SearchUIUtils'; import type {SearchFilter} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; @@ -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,11 +16,10 @@ type TextInputFilterContentWrapperProps = Pick; type DateFilterContentWrapperProps = Pick; type ReportFieldFilterContentWrapperProps = Pick; -type CommonFilterContentWrapperProps = Omit; +type CommonFilterContentWrapperProps = Omit; type SearchAdvancedFiltersContentProps = { filterKey: SearchFilter['key']; values: Partial | undefined; - policyIDQuery: string[] | undefined; ready?: boolean; components: { Text: React.ComponentType; @@ -39,7 +37,7 @@ function getFilterFormValue(filter 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 (isTextFilterKey(filterKey)) { @@ -112,8 +110,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/CategorySelector.tsx b/src/components/Search/FilterComponents/CategorySelector.tsx index e55af2450d7d..766f71e544fa 100644 --- a/src/components/Search/FilterComponents/CategorySelector.tsx +++ b/src/components/Search/FilterComponents/CategorySelector.tsx @@ -1,14 +1,14 @@ -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 {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'; @@ -18,10 +18,10 @@ import React from 'react'; import MultiSelect from './MultiSelect'; type CategorySelectorProps = SearchFilterCommonProps & { - policyIDs: string[] | undefined; + policyID: Filter | undefined; }; -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); @@ -50,23 +50,13 @@ function CategorySelector({value = [], policyIDs = [], selectionListTextInputSty }, [availableNonPersonalPolicyCategoriesSelector], ); - const selectedPoliciesCategories: PolicyCategory[] = Object.keys(allPolicyCategories ?? {}) - .filter((key) => policyIDs.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`)?.includes(key)) - .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) { - const categories = Object.values(allPolicyCategories ?? {}).flatMap((policyCategories) => Object.values(policyCategories ?? {})); - for (const category of categories) { - uniqueCategoryNames.add(category.name); - } - } else if (selectedPoliciesCategories.length > 0) { - 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 2f125c9e0824..3037c172ca16 100644 --- a/src/components/Search/FilterComponents/ExportedToSelector.tsx +++ b/src/components/Search/FilterComponents/ExportedToSelector.tsx @@ -1,5 +1,5 @@ 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'; @@ -10,8 +10,8 @@ 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, getConnectedIntegrationNamesForPolicies} from '@libs/SearchQueryUtils'; import variables from '@styles/variables'; @@ -27,7 +27,7 @@ import {View} from 'react-native'; import MultiSelect from './MultiSelect'; type ExportedToSelectorProps = SearchFilterCommonProps & { - policyIDs: string[] | undefined; + policyID: Filter | undefined; }; const STANDARD_EXPORT_TEMPLATE_ID_TO_DISPLAY_LABEL: Record = { @@ -35,7 +35,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(); @@ -56,7 +56,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; @@ -97,7 +97,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 = 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 bc1eb3f32056..26c569fde658 100644 --- a/src/components/Search/FilterComponents/TagSelector.tsx +++ b/src/components/Search/FilterComponents/TagSelector.tsx @@ -1,10 +1,10 @@ -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 {getAllPolicyValues, sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -19,30 +19,17 @@ import React from 'react'; import MultiSelect from './MultiSelect'; type TagSelectorProps = SearchFilterCommonProps & { - policyIDs: string[] | undefined; + policyID: Filter | undefined; }; -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)) - ?.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) { - const tagListsUnpacked = Object.values(allPolicyTagLists ?? {}).filter((item) => !!item); - for (const tag of tagListsUnpacked.map(getTagNamesFromTagsLists).flat()) { - uniqueTagNames.add(tag); - } - } else if (selectedPoliciesTagLists.length > 0) { - 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/components/Search/FilterComponents/TaxRateSelector.tsx b/src/components/Search/FilterComponents/TaxRateSelector.tsx index 67d9d6039fd5..58dc21962d36 100644 --- a/src/components/Search/FilterComponents/TaxRateSelector.tsx +++ b/src/components/Search/FilterComponents/TaxRateSelector.tsx @@ -1,35 +1,28 @@ -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 CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy} from '@src/types/onyx'; import React from 'react'; import MultiSelect from './MultiSelect'; type TaxRateSelectorProps = SearchFilterCommonProps & { - policyIDs: string[] | undefined; + policyID: Filter | undefined; }; -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/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 f153182726c8..793c845b40d6 100644 --- a/src/components/Search/FilterComponents/WorkspaceSelector.tsx +++ b/src/components/Search/FilterComponents/WorkspaceSelector.tsx @@ -30,11 +30,9 @@ import type {MultiSelectItem} from './MultiSelect'; import ListFilterView from './ListFilterViewWrapper'; -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(); @@ -52,14 +50,12 @@ function WorkspaceSelector({policyIDQuery, value, selectionListTextInputStyle, s icons: workspace.icons, })); - const policyID = value ?? policyIDQuery ?? []; - 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); }; @@ -67,7 +63,7 @@ function WorkspaceSelector({policyIDQuery, value, selectionListTextInputStyle, s 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, })); diff --git a/src/components/Search/FilterComponents/index.tsx b/src/components/Search/FilterComponents/index.tsx index af08c948dece..617ac7b01e98 100644 --- a/src/components/Search/FilterComponents/index.tsx +++ b/src/components/Search/FilterComponents/index.tsx @@ -1,4 +1,4 @@ -import type {SearchAmountFilterKeys, SearchDateFilterKeys, SearchFilterCommonProps} from '@components/Search/types'; +import type {Filter, SearchAmountFilterKeys, SearchDateFilterKeys, SearchFilterCommonProps, SearchTextFilterKeys} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; @@ -26,12 +26,11 @@ 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; - policyIDs: string[] | undefined; - policyIDQuery: string[] | undefined; + policyID: Filter | undefined; }; type SingleSelectFilterKeys = typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.BILLABLE | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.REIMBURSABLE | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_TYPE; @@ -72,8 +71,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: @@ -115,7 +109,7 @@ function FilterComponents({filterKey, value, type, policyIDs, policyIDQuery, sel return ( { + return typeof v === 'string'; + }; 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 = () => { @@ -38,8 +37,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 76bae03660cd..e92414a4bae4 100644 --- a/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/CommonFilterContentPopupWrapper.tsx +++ b/src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/CommonFilterContentPopupWrapper.tsx @@ -1,19 +1,18 @@ -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'; import React from 'react'; -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; @@ -57,7 +56,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); @@ -130,9 +129,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} /> @@ -148,7 +146,6 @@ function useSearchFiltersBar(queryJSON: SearchQueryJSON): UseSearchFiltersBarRes const {setFilterQueryParams, updateFilterQueryParams} = useUpdateFilterQuery(queryJSON); const filters = mapFiltersFormToLabelValueList( searchAdvancedFiltersForm, - queryJSON.policyID, SKIPPED_SEARCH_FILTERS, translate, localeCompare, @@ -159,7 +156,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; @@ -112,7 +112,7 @@ function SearchStaticList({ reportAttributesDerivedValue: undefined, }); - 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 4e305b730b32..310ad1ff8c88 100644 --- a/src/components/Search/hooks/useSearchSnapshot.ts +++ b/src/components/Search/hooks/useSearchSnapshot.ts @@ -95,7 +95,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(); @@ -304,8 +304,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, @@ -338,7 +338,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 339e269762f0..08dac155a641 100644 --- a/src/components/Search/hooks/useUpdateFilterQuery.tsx +++ b/src/components/Search/hooks/useUpdateFilterQuery.tsx @@ -7,7 +7,6 @@ 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'; @@ -24,7 +23,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/types.ts b/src/components/Search/types.ts index e75f6790c4fb..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`. @@ -281,6 +280,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 @@ -317,7 +321,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 @@ -345,13 +348,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; @@ -452,7 +453,6 @@ export type { SearchDateKey, SearchAmountFilterKeys, SearchAmountValues, - SearchStatus, SearchQueryJSON, SearchQueryString, ReportFieldKey, @@ -472,6 +472,7 @@ export type { SearchRowSelectionActionsValue, ASTNode, QueryFilter, + Filter, QueryFilters, SyntaxFilterKey, RawQueryFilter, diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts index 43a9a19a61fb..3ff62d7f8ce8 100644 --- a/src/hooks/useAdvancedSearchFilters.ts +++ b/src/hooks/useAdvancedSearchFilters.ts @@ -1,3 +1,5 @@ +import type {Filter} from '@components/Search/types'; + import {isFilterableBankAccount} from '@libs/BankAccountUtils'; import {isPolicyFeatureEnabled} from '@libs/PolicyUtils'; import {getAllPolicyValues} from '@libs/SearchQueryUtils'; @@ -318,16 +320,16 @@ 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: 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}); 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/useAutocompleteSuggestions.ts b/src/hooks/useAutocompleteSuggestions.ts index 54f7a5b6cd51..a1e60b888d2d 100644 --- a/src/hooks/useAutocompleteSuggestions.ts +++ b/src/hooks/useAutocompleteSuggestions.ts @@ -338,7 +338,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) { @@ -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)) diff --git a/src/hooks/useExportedToFilterOptions.ts b/src/hooks/useExportedToFilterOptions.ts index 9f9231a3e90b..fec7b85ba887 100644 --- a/src/hooks/useExportedToFilterOptions.ts +++ b/src/hooks/useExportedToFilterOptions.ts @@ -2,8 +2,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, getConnectedIntegrationNamesForPolicies, getFilterFromQuery} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -45,7 +44,7 @@ function exportedToPoliciesSelector(policies: OnyxCollection): OnyxColle */ export default function useExportedToFilterOptions(): UseExportedToFilterDataResult { const {currentSearchQueryJSON} = useSearchQueryContext(); - const policyIDs = currentSearchQueryJSON?.policyID; + const policyIDs = getFilterFromQuery(currentSearchQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); const {translate} = useLocalize(); const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES); @@ -53,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 !== undefined ? 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]; @@ -80,7 +79,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 812260daf452..5ef69bb2bbd0 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -557,7 +557,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 = @@ -721,20 +721,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, @@ -753,7 +748,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const exportParameters = getCSVExportParameters(isBasicExport, queryJSONToExport); await exportSearchItemsToCSV( { - query: status, jsonQuery: exportParameters.jsonQuery, reportIDList: isGroupExport ? [] : reportIDList, transactionIDList: isGroupExport ? [] : selectedTransactionsKeys, @@ -774,7 +768,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { }, [ isOffline, - status, areAllMatchingItemsSelected, queryJSON, selectedReports, @@ -1458,7 +1451,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>; } @@ -2163,7 +2156,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { return options; }, [ selectedTransactionsKeys, - status, hash, selectedTransactions, queryJSON?.type, diff --git a/src/hooks/useSearchSections.ts b/src/hooks/useSearchSections.ts index 73ae65599f9d..98433ed09009 100644 --- a/src/hooks/useSearchSections.ts +++ b/src/hooks/useSearchSections.ts @@ -36,7 +36,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; @@ -64,7 +64,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..b1ab748a25d2 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'; +import type {SearchQueryString} from '@components/Search/types'; type ExportSearchItemsToCSVParams = { - query: SearchStatus; jsonQuery: SearchQueryString; reportIDList: string[]; transactionIDList: string[]; 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/ObjectUtils.ts b/src/libs/ObjectUtils.ts index 5892675b6d7b..ac73d2c7f4c2 100644 --- a/src/libs/ObjectUtils.ts +++ b/src/libs/ObjectUtils.ts @@ -42,4 +42,8 @@ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } -export {shallowCompare, getObjectValues, filterObject, isRecord, getObjectKeys}; +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/PolicyUtils.ts b/src/libs/PolicyUtils.ts index cdd33357bbd7..bb2ba9633816 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -2391,30 +2391,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, policyIDs?: string[]): 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); - - 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; @@ -2802,7 +2778,6 @@ export { getCleanedTagName, getCommaSeparatedTagNameWithSanitizedColons, getConnectedIntegration, - getConnectedIntegrationNamesForPolicies, getConnectionExporters, findVendorByID, getMatchingVendorByID, diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index d201e12a12d5..8ff1f0652540 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -212,7 +212,7 @@ function filterOutRangesWithCorrectValue( return withdrawalStatusList.includes(range.value); case CONST.SEARCH.SYNTAX_FILTER_KEYS.PAID_STATUS: return paidStatusList.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 d2400e018263..23ce77c8a6ee 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -1205,7 +1205,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(); + } + } } } } @@ -1286,23 +1292,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(); } } } @@ -4770,7 +4770,6 @@ function peg$parse(input, options) { const defaultValues = { type: "expense", - status: "", sortBy: "date", sortOrder: "desc", view: "table", @@ -4823,11 +4822,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 da0dd44b9b6c..57389645633f 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; @@ -305,6 +299,8 @@ key "key" / is / purchaseCurrency / purchaseAmount + / policyID + / status / reportFieldDynamic ) @@ -314,7 +310,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 5944004ee25f..bfb2dc086470 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1,6 +1,7 @@ import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type { ASTNode, + Filter, QueryFilter, QueryFilters, RawQueryFilter, @@ -14,7 +15,6 @@ import type { SearchFilterKey, SearchQueryJSON, SearchQueryString, - SearchStatus, SearchWithdrawalType, SyntaxFilterKey, UserFriendlyKey, @@ -29,7 +29,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, @@ -39,6 +39,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'; @@ -63,7 +64,7 @@ import {getPreservedNavigatorState} from './Navigation/AppNavigator/createSplitN import navigationRef from './Navigation/navigationRef'; import {isRecord} from './ObjectUtils'; import {getPersonalDetailByEmail, temporaryGetDisplayNameOrDefault} 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'; @@ -435,6 +436,14 @@ function getFilters(queryJSON: SearchQueryJSON) { return filters; } +function getFilterFromQuery(queryJSON: SearchQueryJSON | undefined, filterKey: SearchAdvancedFiltersKey): Filter { + 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}; +} + /** * @private * Returns an updated filter value for some query filters. @@ -534,11 +543,15 @@ function wasViewExplicitlySet(queryJSON?: SearchQueryJSON | Readonly(orderedQuery); @@ -561,6 +574,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); } @@ -661,11 +679,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); @@ -730,10 +743,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(',')}`); @@ -999,6 +998,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial( - policyID: string[] | undefined, + policyID: Filter | undefined, key: T, policyData: OnyxCollection, ): Array { - if (!policyData || !policyID) { - return []; + if (!policyData || !policyID || !policyID.value) { + return Object.values(policyData ?? {}).filter((data): data is NonNullable => !!data); } - return policyID.map((id) => policyData?.[`${key}${id}`]).filter((data) => !!data) as Array; + if (policyID.isNegated) { + return Object.keys(policyData).reduce( + (acc, curr) => { + const id = curr.replace(key, ''); + if (!policyID.value?.includes(id) && policyData[curr]) { + acc.push(policyData[curr]); + } + return acc; + }, + [] as Array, + ); + } + + return policyID.value.map((id) => policyData?.[`${key}${id}`]).filter((data): data is NonNullable => !!data); +} + +function getAllPolicyValuesMap( + policyID: Filter | undefined, + key: T, + policyData: OnyxCollection, +): OnyxCollection { + if (!policyData || !policyID || !policyID.value) { + return {}; + } + + if (policyID.isNegated) { + return Object.keys(policyData).reduce( + (acc, curr) => { + const id = curr.replace(key, ''); + if (!policyID.value?.includes(id) && policyData[curr]) { + acc[curr] = policyData[curr]; + } + return acc; + }, + {} as Exclude, undefined>, + ); + } + + return policyID.value.reduce( + (acc, curr) => { + if (policyData?.[`${key}${curr}`]) { + acc[`${key}${curr}`] = policyData?.[`${key}${curr}`]; + } + return acc; + }, + {} as Exclude, undefined>, + ); +} + +/** + * 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 policiesToCheck = getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, 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) { @@ -1127,7 +1196,7 @@ function buildFilterFormValuesFromQuery( ) { const filters = queryJSON.flatFilters; const filtersForm = {} as Partial; - const policyID = queryJSON.policyID; + 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)); @@ -1140,21 +1209,23 @@ 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 || 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) { - 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)); @@ -1182,15 +1253,15 @@ 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 >; } @@ -1203,7 +1274,7 @@ function buildFilterFormValuesFromQuery( } } 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 @@ -1212,7 +1283,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); @@ -1220,13 +1291,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 || @@ -1235,28 +1306,28 @@ 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)); } 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 = 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 ?? {})) { @@ -1265,11 +1336,11 @@ 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(); - const categoryLists = policyID ? 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); @@ -1277,10 +1348,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}"`; @@ -1354,7 +1427,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)) { @@ -1441,27 +1514,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; @@ -1732,8 +1787,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); @@ -1795,7 +1850,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[] = []; @@ -1858,9 +1913,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)}`; @@ -1870,10 +1923,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}`; @@ -1920,18 +1969,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}`; @@ -1951,11 +1998,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; } /** @@ -2320,11 +2367,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; } @@ -2390,7 +2433,19 @@ 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, ''); +} + +function isFilterNegatable(key: SearchAdvancedFiltersKey) { + return NEGATABLE_FILTERS.has(removeNegation(key) as SearchNegatableFilterKeys); } export { @@ -2420,6 +2475,8 @@ export { sortOptionsWithEmptyValue, shouldHighlight, getAllPolicyValues, + getAllPolicyValuesMap, + getConnectedIntegrationNamesForPolicies, getUserFriendlyValue, getUserFriendlyKey, shouldResetSort, @@ -2436,6 +2493,8 @@ export { getParamsState, getRoutes, isSearchRootParams, + getFilterFromQuery, + isFilterNegatable, }; export type {BuildUserReadableQueryStringParams}; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index d656973fe489..a3b34404fdd6 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2,6 +2,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 type {FilterComponentsProps} from '@components/Search/FilterComponents'; import type {MultiSelectItem} from '@components/Search/FilterComponents/MultiSelect'; import type {SingleSelectItem} from '@components/Search/FilterComponents/SingleSelect'; import type { @@ -40,7 +41,6 @@ import type { SearchPaidStatus, SearchQueryJSON, SearchSortBy, - SearchStatus, SearchTextFilterKeys, SearchView, SearchWithdrawalStatus, @@ -112,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 { @@ -182,6 +183,8 @@ import { getDateFilterKeys, getDateRangeDisplayValueFromFormValue, getDateRangeForPreset, + getFilterFromQuery, + isFilterNegatable, isFilterSupported, isSearchDatePreset, sortOptionsWithEmptyValue, @@ -411,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([ @@ -2070,6 +2072,24 @@ function hasVisibleViolations( return hasActionable && hasUserVisible; } +function isEligibleForStatus(currentQueryJSON: SearchQueryJSON | undefined, report: OnyxEntry, transactionItemReportID?: string) { + 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. @@ -2117,29 +2137,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 = currentQueryJSON.status; - 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; - } - } - } + let shouldShow = true; if (!transactionItem.transactionID) { shouldShow = false; + } else if (!isActionLoading && currentQueryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE && !isTrackedOptimisticItem) { + shouldShow = isEligibleForStatus(currentQueryJSON, report, transactionItem.reportID); } if (shouldShow) { @@ -2850,22 +2859,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 = currentQueryJSON.status; - - if (Array.isArray(status)) { - shouldShow = status.some((expenseStatus) => { - return isValidExpenseStatus(expenseStatus) ? expenseStatusActionMapping[expenseStatus](reportItem) : false; - }); - } else { - shouldShow = isValidExpenseStatus(status) ? expenseStatusActionMapping[status](reportItem) : false; - } - } - } + const isActionLoading = !!isActionLoadingSet?.has(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportItem.reportID}`); + const shouldShow = !isActionLoading && currentQueryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE ? isEligibleForStatus(currentQueryJSON, reportItem) : true; if (shouldShow) { const reportPendingAction = @@ -3786,8 +3781,7 @@ const groupByRequiredColumns: Partial> */ function getSortedSections( type: SearchDataTypes, - status: SearchStatus, - data: ListItemDataType, + data: ListItemDataType, localeCompare: LocaleContextProps['localeCompare'], translate: LocaleContextProps['translate'], sortBy?: SearchSortBy, @@ -4803,14 +4797,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; } @@ -5396,7 +5383,6 @@ function getDisplayValue( key: SearchAdvancedFiltersKey, form: Partial, type: SearchDataTypes, - policyIDQuery: string[] | undefined, translate: LocalizedTranslate, localeCompare: LocaleContextProps['localeCompare'], ) { @@ -5466,20 +5452,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(', '); } @@ -5492,6 +5471,21 @@ function getDisplayValue( return Array.isArray(formValue) ? formValue.join(', ') : formValue; } +function getFilterNegatableValue( + filterKey: K, + values: (Partial & Partial>) | undefined, +): { + isNegated: boolean; + value: SearchAdvancedFiltersForm[K] | undefined; +} { + const negatedFilterKey = `${filterKey}${CONST.SEARCH.NOT_MODIFIER}` as const; + if (!isFilterNegatable(filterKey) || !values || !hasKey(values, negatedFilterKey)) { + return {isNegated: false, value: values?.[filterKey]}; + } + + return {isNegated: true, value: values[negatedFilterKey]}; +} + 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); } @@ -5516,7 +5510,6 @@ type SearchFilter = { function mapFiltersFormToLabelValueList>( searchAdvancedFiltersForm: Partial, - policyIDQuery: string[] | undefined, skipFilters: Set | undefined, translate: LocalizedTranslate, localeCompare: LocaleContextProps['localeCompare'], @@ -5570,7 +5563,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; @@ -6550,6 +6543,7 @@ export { getSelectedGroupFilterEntry, adjustTimeRangeToDateFilters, getDateDisplayValue, + getFilterNegatableValue, shouldShowFilter, mapFiltersFormToLabelValueList, isTextFilterKey, @@ -6566,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 0109d6bd3102..2f20191c58a7 100644 --- a/src/libs/actions/IOU/SearchUpdate.ts +++ b/src/libs/actions/IOU/SearchUpdate.ts @@ -1,8 +1,8 @@ import type {SearchQueryJSON} from '@components/Search/types'; import {isExpenseReport, isOptimisticPersonalDetail} from '@libs/ReportUtils'; -import {buildSearchQueryJSON, buildSearchQueryString, getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils'; -import {getSuggestedSearches} from '@libs/SearchUIUtils'; +import {buildSearchQueryJSON, buildSearchQueryString, getCurrentSearchQueryJSON, getFilterFromQuery} from '@libs/SearchQueryUtils'; +import {getSuggestedSearches, isEligibleForStatus} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -12,7 +12,6 @@ import type {OnyxData} from '@src/types/onyx/Request'; import type {SearchResultDataType} from '@src/types/onyx/SearchResults'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import Onyx from 'react-native-onyx'; @@ -30,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 = { @@ -59,25 +57,19 @@ function shouldOptimisticallyUpdateSearch( ) { return false; } - let shouldOptimisticallyUpdateByStatus; - const status = currentSearchQueryJSON.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 (currentSearchQueryJSON.policyID?.length && iouReport?.policyID) { - if (!currentSearchQueryJSON.policyID.includes(iouReport.policyID)) { + 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; + } + + if (currentSearchPolicyIDs.isNegated && currentSearchPolicyIDs.value.includes(iouReport.policyID)) { return false; } } + const shouldOptimisticallyUpdateByStatus = isEligibleForStatus(currentSearchQueryJSON, iouReport, transaction?.reportID); if (!shouldOptimisticallyUpdateByStatus) { return false; } @@ -93,7 +85,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); @@ -197,7 +193,6 @@ function getSearchOnyxUpdate({ value: { search: { type: currentSearchQueryJSON.type, - status: currentSearchQueryJSON.status, hasResults: true, isLoading: false, }, @@ -228,7 +223,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 ec18332cf4ae..10680ded2f25 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -703,7 +703,6 @@ function getOnyxLoadingData( value: { ...(isOffline ? {} : {data: null}), search: { - status: queryJSON?.status, type: queryJSON?.type, ...(isSearchAPI && {isLoading: false}), }, @@ -1315,7 +1314,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, ) { @@ -1345,7 +1344,6 @@ function exportSearchItemsToCSV( } const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV, { - query, jsonQuery, reportIDList: Array.from(reportIDSet), transactionIDList, @@ -1376,7 +1374,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; @@ -1402,7 +1400,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 7568bb500d8a..c3c0b13aad44 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -23,7 +23,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, getFilterFromQuery, isDefaultExpenseReportsQuery, isDefaultExpensesQuery} from '@libs/SearchQueryUtils'; import type {SearchTypeMenuSection} from '@libs/SearchUIUtils'; import {TODO_SEARCH_KEYS} from '@libs/SearchUIUtils'; @@ -151,10 +151,10 @@ function EmptySearchViewContent({ const defaultChatEnabledPolicy = getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy); - const filteredPolicyID = queryJSON?.policyID; + const filteredPolicyID = getFilterFromQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); 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)?.id; 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 84a4552964ad..2d3a3b6461a7 100644 --- a/src/pages/Search/SearchAdvancedFiltersContentPage/CommonFilterContentPageWrapper.tsx +++ b/src/pages/Search/SearchAdvancedFiltersContentPage/CommonFilterContentPageWrapper.tsx @@ -1,23 +1,22 @@ import Button from '@components/Button'; import type {FilterComponentsProps} from '@components/Search/FilterComponents'; -import CommonFilterContent from '@components/Search/FilterComponents/AdvancedFilters/CommonFilterContent'; +import FilterComponents from '@components/Search/FilterComponents'; import type {CommonFilterContentWrapperProps} from '@components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent'; import useLocalize from '@hooks/useLocalize'; import React, {useState} from 'react'; -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); return ( - 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 03e8256d47b9..e115523237f9 100644 --- a/src/pages/Search/SearchSavePage.tsx +++ b/src/pages/Search/SearchSavePage.tsx @@ -169,7 +169,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 ac66af5ac70c..6665391dab84 100644 --- a/src/pages/home/SpendOverTimeSection/useSpendOverTimeData.ts +++ b/src/pages/home/SpendOverTimeSection/useSpendOverTimeData.ts @@ -92,7 +92,6 @@ function useSpendOverTimeData() { searchResults?.data && queryJSON && groupBy && login ? (getSortedSections( queryJSON.type, - queryJSON.status, getSections({ type: queryJSON.type, data: searchResults.data, diff --git a/src/pages/home/YourSpendSection/queries.ts b/src/pages/home/YourSpendSection/queries.ts index 195c6ce8ae7c..ff915a2213a5 100644 --- a/src/pages/home/YourSpendSection/queries.ts +++ b/src/pages/home/YourSpendSection/queries.ts @@ -15,7 +15,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. @@ -26,7 +26,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/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index a20c1c5f93be..e4bc7aa3498e 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -630,7 +630,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/src/types/form/SearchAdvancedFiltersForm.ts b/src/types/form/SearchAdvancedFiltersForm.ts index 7087acbc5837..c48397e56edf 100644 --- a/src/types/form/SearchAdvancedFiltersForm.ts +++ b/src/types/form/SearchAdvancedFiltersForm.ts @@ -17,7 +17,7 @@ import type { import CONST from '@src/CONST'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; -import type {ValueOf} from 'type-fest'; +import type {TupleToUnion, ValueOf} from 'type-fest'; import type Form from './Form'; @@ -43,12 +43,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', @@ -214,12 +234,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, @@ -327,12 +349,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, @@ -395,12 +419,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, @@ -489,12 +515,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, @@ -582,6 +610,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, @@ -596,6 +625,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, @@ -634,7 +664,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; @@ -695,6 +726,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[]; @@ -799,6 +831,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 {TEXT_FILTER_KEYS, DATE_FILTER_KEYS, ALLOWED_TYPE_FILTERS, AMOUNT_FILTER_KEYS}; +export {TEXT_FILTER_KEYS, 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 caa95fa809b5..21e13cd39db3 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -1,5 +1,11 @@ -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} from '@components/Search/types'; import type CONST from '@src/CONST'; import type ONYXKEYS from '@src/ONYXKEYS'; @@ -22,13 +28,15 @@ import type {TransactionViolation} from './TransactionViolation'; type SearchDataTypes = ValueOf; /** 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 = { @@ -38,8 +46,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 f67d2eaee008..64721f37308f 100644 --- a/tests/actions/IOU/RequestMoneyTest.ts +++ b/tests/actions/IOU/RequestMoneyTest.ts @@ -1,4 +1,4 @@ -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'; @@ -2621,7 +2621,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 af168bd5411b..4fb39e74d209 100644 --- a/tests/actions/IOU/SearchUpdateTest.ts +++ b/tests/actions/IOU/SearchUpdateTest.ts @@ -1,4 +1,4 @@ -import type {SearchQueryJSON, SearchStatus} from '@components/Search/types'; +import type {SearchQueryJSON} from '@components/Search/types'; import {shouldOptimisticallyUpdateSearch} from '@libs/actions/IOU/SearchUpdate'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; @@ -140,7 +140,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: { @@ -177,8 +176,8 @@ describe('actions/IOU', () => { ], }, ], - hash: 1920151829, - recentSearchHash: 2100977843, + 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}; @@ -198,7 +197,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: { @@ -210,7 +208,7 @@ describe('actions/IOU', () => { }, right: { operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, - left: 'from', + left: 'to', right: '20671314', }, }, @@ -225,7 +223,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, @@ -234,10 +232,9 @@ describe('actions/IOU', () => { ], }, ], - - hash: 1510971479, + hash: 1685631874, inputQuery: 'sortBy:date sortOrder:desc type:expense-report action:approve to:20671314', - recentSearchHash: 967911777, + 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}; @@ -257,17 +254,37 @@ describe('actions/IOU', () => { reimbursable: true, }; const currentSearchQueryJSON = { - type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - status: '' as SearchStatus, + 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: [ @@ -278,7 +295,7 @@ describe('actions/IOU', () => { ], }, ], - hash: 71801560, + hash: 1967417738, inputQuery: 'sortBy:date sortOrder:desc type:expense groupBy:from status:drafts,outstanding reimbursable:yes', recentSearchHash: 1043581824, similarSearchHash: 1832274510, @@ -302,13 +319,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, @@ -342,5 +362,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/actions/IOUTest/SplitSelfDMTest.ts b/tests/actions/IOUTest/SplitSelfDMTest.ts index 782b9b440837..1db2f4d38e74 100644 --- a/tests/actions/IOUTest/SplitSelfDMTest.ts +++ b/tests/actions/IOUTest/SplitSelfDMTest.ts @@ -333,7 +333,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/actions/IOUTest/SplitTest.ts b/tests/actions/IOUTest/SplitTest.ts index 98471a7be180..d72f69113c6a 100644 --- a/tests/actions/IOUTest/SplitTest.ts +++ b/tests/actions/IOUTest/SplitTest.ts @@ -1994,7 +1994,7 @@ describe('updateSplitTransactionsFromSplitExpensesFlow', () => { }; // When splitting the expense - const hash = 1; + const hash = unapprovedCashHash; let allTransactions: OnyxCollection; let allReports: OnyxCollection; diff --git a/tests/ui/SearchPageTest.tsx b/tests/ui/SearchPageTest.tsx index 4ccdc141f2cf..80f7f450d0de 100644 --- a/tests/ui/SearchPageTest.tsx +++ b/tests/ui/SearchPageTest.tsx @@ -193,8 +193,8 @@ describe('SearchPageNarrow', () => { errors: {error: 'Something went wrong'}, search: { type: CONST.SEARCH.DATA_TYPES.CHAT, - status: '', offset: 0, + hash: failedQueryJSON?.hash, isLoading: false, hasMoreResults: false, }, diff --git a/tests/unit/HomePage/YourSpendSection/rowStateTest.ts b/tests/unit/HomePage/YourSpendSection/rowStateTest.ts index 7edf3b85bc2f..355f80de4f82 100644 --- a/tests/unit/HomePage/YourSpendSection/rowStateTest.ts +++ b/tests/unit/HomePage/YourSpendSection/rowStateTest.ts @@ -6,27 +6,26 @@ import type {OnyxEntry} from 'react-native-onyx'; // Helpers -const makeSearchResults = (overrides: Partial = {}): SearchResults => - ({ - search: { - offset: 0, - type: 'expense', - status: '', - hasMoreResults: false, - hasResults: true, - isLoading: false, - count: 5, - }, - data: {}, - ...overrides, - }) as SearchResults; +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({ 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 499a27c69252..c8d594f1ae9c 100644 --- a/tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts +++ b/tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts @@ -140,15 +140,15 @@ function makeSearchResultsWithCount(count: number): SearchResults { return { search: { type: 'expense', - status: '', offset: 0, + hash: 0, hasMoreResults: false, hasResults: count > 0, isLoading: false, count, }, data: {}, - } as SearchResults; + }; } /** Populates onyxData with a single-entry policies collection. */ @@ -591,17 +591,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', status: '', 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); @@ -631,13 +631,13 @@ 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, 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', status: '', 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 12b2fa6066a9..bb1eb15e71ed 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -19,7 +19,6 @@ import { getActivePoliciesWithExpenseChatAndPerDiemEnabledAndHasRates, getAllTaxRates, getAllTaxRatesNamesAndValues, - getConnectedIntegrationNamesForPolicies, getCustomUnitsForDuplication, getDefaultChatEnabledPolicy, getDefaultTimeTrackingRate, @@ -2754,76 +2753,6 @@ describe('PolicyUtils', () => { }); }); - describe('getConnectedIntegrationNamesForPolicies', () => { - it('returns empty Set when policies is undefined', () => { - expect(getConnectedIntegrationNamesForPolicies(undefined)).toEqual(new Set()); - }); - - it('returns empty Set when policies is empty object', () => { - expect(getConnectedIntegrationNamesForPolicies({})).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)).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, ['1'])).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); - 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/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/ExpenseFlatSearchViewTest.tsx b/tests/unit/Search/ExpenseFlatSearchViewTest.tsx index edb772aae689..bf91cdef2427 100644 --- a/tests/unit/Search/ExpenseFlatSearchViewTest.tsx +++ b/tests/unit/Search/ExpenseFlatSearchViewTest.tsx @@ -117,14 +117,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/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 ac880024f444..de981e99ada0 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -15,16 +15,22 @@ import { buildSearchQueryString, buildUserReadableQueryString, getAdvancedFiltersToReset, + getAllPolicyValues, + getAllPolicyValuesMap, + getConnectedIntegrationNamesForPolicies, getCurrentSearchQueryJSON, getDateRangeDisplayValueFromFormValue, getDisplayQueryFiltersForKey, getFilterDisplayValue, + getFilterFromQuery, getKeywordQueryWithCurrentSearchContext, getLastRouteByName, getParamsState, getQueryWithUpdatedValues, getRangeBoundariesFromFormValue, getRoutes, + isDefaultExpenseReportsQuery, + isDefaultExpensesQuery, isSearchRootParams, serializeQueryJSONForBackend, shouldHighlight, @@ -37,11 +43,15 @@ 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 type {Connections} from '@src/types/onyx/Policy'; /* eslint-disable @typescript-eslint/naming-convention */ // we need "dirty" object key names in these tests import type {OnyxCollection} from 'react-native-onyx'; +import createMock from 'tests/utils/createMock'; + +import createRandomPolicy from '../../utils/collections/policies'; import {localeCompare, translateLocal} from '../../utils/TestHelper'; const mockGetRootState = jest.fn(); @@ -376,7 +386,6 @@ describe('SearchQueryUtils', () => { test('simple filter value', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, policyID: ['12345'], amountLessThan: '100', }; @@ -389,7 +398,6 @@ describe('SearchQueryUtils', () => { test('receipt type filter value', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, receiptType: ['ereceipt', 'hotel'], }; @@ -401,7 +409,6 @@ describe('SearchQueryUtils', () => { test('negated receipt type filter value', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, receiptTypeNot: ['hotel'], }; @@ -423,7 +430,6 @@ describe('SearchQueryUtils', () => { test('with keywords', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, policyID: ['67890'], merchant: 'Amazon', description: 'Electronics', @@ -439,7 +445,6 @@ describe('SearchQueryUtils', () => { test('currencies and categories', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, category: ['services', 'consulting'], currency: ['USD', 'EUR'], }; @@ -452,7 +457,6 @@ describe('SearchQueryUtils', () => { test('has empty category values', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, category: ['equipment', 'consulting', 'none,Uncategorized'], }; @@ -464,7 +468,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], }; @@ -477,7 +480,6 @@ describe('SearchQueryUtils', () => { test('serializes real tag values as tag filters', () => { const filterValues: Partial = { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, tag: ['Engineering'], }; @@ -636,7 +638,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, }; @@ -648,7 +649,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], }; @@ -660,7 +660,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], }; @@ -672,7 +671,6 @@ describe('SearchQueryUtils', () => { test('with single paid status filter', () => { const filterValues: Partial = { type: 'expense-report', - status: CONST.SEARCH.STATUS.EXPENSE_REPORT.ALL, paidStatus: [CONST.SEARCH.PAID_STATUS.MARKED_AS_PAID], }; @@ -684,7 +682,6 @@ describe('SearchQueryUtils', () => { test('with multi-value paid status filter', () => { const filterValues: Partial = { 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], }; @@ -696,7 +693,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, }; @@ -1130,7 +1126,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, category: ['Maintenance', 'none'], }); }); @@ -1155,7 +1150,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, action: 'submit', }); @@ -1171,7 +1165,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, action: undefined, }); }); @@ -1196,7 +1189,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], }); @@ -1212,7 +1204,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], }); }); @@ -1237,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], }); @@ -1253,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], }); }); @@ -1278,7 +1267,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, amountLessThan: '-12345', amountGreaterThan: '-67890', amountEqualTo: '-54321', @@ -1309,7 +1297,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'], }); }); @@ -1505,7 +1492,6 @@ describe('SearchQueryUtils', () => { expect(result).toEqual({ type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, tag: [CONST.SEARCH.TAG_EMPTY_VALUE], }); }); @@ -1521,7 +1507,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], }); @@ -2386,7 +2371,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(getFilterFromQuery(newQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS).value).toBe(undefined); }); test('does not add quotes to non-keyword filter values', () => { @@ -3522,24 +3507,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, @@ -3564,6 +3531,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({ @@ -3602,4 +3570,303 @@ 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 every policy value when the filter is undefined', () => { + expect(getAllPolicyValues(undefined, ONYXKEYS.COLLECTION.POLICY, policyData)).toEqual([policy1, policy2, policy3]); + }); + + 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', () => { + 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); + }); + }); + + 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/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index b92dded3f3be..9927bc26593a 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -549,7 +549,7 @@ const searchResults: OnyxTypes.SearchResults = { hasMoreResults: false, hasResults: true, offset: 0, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, isLoading: false, type: 'expense', }, @@ -590,7 +590,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', @@ -640,7 +640,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', @@ -677,7 +677,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', @@ -709,7 +709,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', @@ -1924,7 +1924,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', @@ -1981,7 +1981,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', @@ -2062,7 +2062,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', @@ -2091,7 +2091,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', @@ -2122,7 +2122,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', @@ -2151,7 +2151,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', @@ -4168,7 +4168,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, @@ -4433,7 +4432,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, @@ -4483,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 +4530,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, @@ -4583,7 +4579,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, @@ -4654,7 +4649,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, @@ -4966,7 +4960,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, @@ -5645,15 +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[]) { + 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})), + }, + ] + : []; 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, inputQuery: 'type:expense' as const, recentSearchHash: TEST_QUERY_HASH, similarSearchHash: TEST_QUERY_HASH, @@ -5706,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); }); @@ -5734,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); }); @@ -5746,10 +5747,35 @@ 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)}); + 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], 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 = { @@ -5760,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); }); @@ -5774,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); } }); @@ -5783,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); @@ -5964,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); }); @@ -5990,11 +6016,36 @@ 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], 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}`]); 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); @@ -6002,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); @@ -6253,13 +6304,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) @@ -6267,7 +6312,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, ); }); @@ -6336,7 +6381,6 @@ describe('SearchUIUtils', () => { const ascendingResult = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', [...unorderedTransactions], localeCompare, translateLocal, @@ -6347,7 +6391,6 @@ describe('SearchUIUtils', () => { ); const descendingResult = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', [...unorderedTransactions], localeCompare, translateLocal, @@ -6378,7 +6421,6 @@ describe('SearchUIUtils', () => { const ascendingResult = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', [...emptyPostedTransactions], localeCompare, translateLocal, @@ -6388,7 +6430,6 @@ describe('SearchUIUtils', () => { ); const descendingResult = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', [...emptyPostedTransactions], localeCompare, translateLocal, @@ -6420,7 +6461,6 @@ describe('SearchUIUtils', () => { const ascendingResult = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', [...tiedTransactions], localeCompare, translateLocal, @@ -6430,7 +6470,6 @@ describe('SearchUIUtils', () => { ); const descendingResult = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', [...tiedTransactions], localeCompare, translateLocal, @@ -6444,41 +6483,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); }); @@ -6486,7 +6516,6 @@ describe('SearchUIUtils', () => { expect( SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionWithdrawalIDGroupListItems, localeCompare, translateLocal, @@ -6501,7 +6530,6 @@ describe('SearchUIUtils', () => { expect( SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionCategoryGroupListItems, localeCompare, translateLocal, @@ -6515,7 +6543,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, @@ -6532,7 +6559,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, @@ -6549,7 +6575,6 @@ describe('SearchUIUtils', () => { it('should sort category data by total amount', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionCategoryGroupListItems, localeCompare, translateLocal, @@ -6568,7 +6593,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, @@ -6585,7 +6609,6 @@ describe('SearchUIUtils', () => { it('should sort category data by expenses count', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionCategoryGroupListItems, localeCompare, translateLocal, @@ -6604,7 +6627,6 @@ describe('SearchUIUtils', () => { expect( SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionMerchantGroupListItems, localeCompare, translateLocal, @@ -6618,7 +6640,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, @@ -6635,7 +6656,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, @@ -6652,7 +6672,6 @@ describe('SearchUIUtils', () => { it('should sort merchant data by total amount', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionMerchantGroupListItems, localeCompare, translateLocal, @@ -6671,7 +6690,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, @@ -6688,7 +6706,6 @@ describe('SearchUIUtils', () => { it('should sort merchant data by expenses count', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionMerchantGroupListItems, localeCompare, translateLocal, @@ -6742,7 +6759,6 @@ describe('SearchUIUtils', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', merchantDataWithEmpty, localeCompare, translateLocal, @@ -6762,7 +6778,6 @@ describe('SearchUIUtils', () => { expect( SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionTagGroupListItems, localeCompare, translateLocal, @@ -6776,7 +6791,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, @@ -6793,7 +6807,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, @@ -6810,7 +6823,6 @@ describe('SearchUIUtils', () => { it('should sort tag data by total amount', () => { const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', transactionTagGroupListItems, localeCompare, translateLocal, @@ -6870,7 +6882,6 @@ describe('SearchUIUtils', () => { // Then sort the sections const result = SearchUIUtils.getSortedSections( CONST.SEARCH.DATA_TYPES.EXPENSE, - '', sections, localeCompare, translateLocal, @@ -8020,6 +8031,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 = { @@ -8055,7 +8121,7 @@ describe('SearchUIUtils', () => { }, search: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, offset: 0, hasMoreResults: false, hasResults: true, @@ -8187,7 +8253,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/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/Search/getSpendOverTimeStateTest.ts b/tests/unit/Search/getSpendOverTimeStateTest.ts index 55adf0ab91be..f4998f4392f3 100644 --- a/tests/unit/Search/getSpendOverTimeStateTest.ts +++ b/tests/unit/Search/getSpendOverTimeStateTest.ts @@ -11,7 +11,6 @@ const queryJSON: SearchQueryJSON = { recentSearchHash: 0, similarSearchHash: 0, 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, @@ -21,7 +20,7 @@ const queryJSON: SearchQueryJSON = { }; const defaultSearchResults: SearchResults = { - search: {offset: 0, type: queryJSON.type, status: queryJSON.status, hasMoreResults: false, hasResults: true, isLoading: false}, + search: {offset: 0, hash: 0, type: queryJSON.type, hasMoreResults: false, hasResults: true, isLoading: false}, data: {}, }; 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/Search/yourSpendQueryBuildersTest.ts b/tests/unit/Search/yourSpendQueryBuildersTest.ts index f8763c8a7cdb..056791a439f7 100644 --- a/tests/unit/Search/yourSpendQueryBuildersTest.ts +++ b/tests/unit/Search/yourSpendQueryBuildersTest.ts @@ -9,7 +9,7 @@ import {buildAwaitingApprovalQuery, buildRecentCardTransactionsQuery, buildRepaidLast30DaysQuery} from '@pages/home/YourSpendSection/queries'; import CONST from '@src/CONST'; -import {buildSearchQueryJSON} from '@src/libs/SearchQueryUtils'; +import {buildSearchQueryJSON, getFilterFromQuery} from '@src/libs/SearchQueryUtils'; const ACCOUNT_ID = 12345; const CARD_ID = 67890; @@ -49,7 +49,7 @@ describe('buildAwaitingApprovalQuery', () => { it('produces status:outstanding', () => { const queryJSON = buildSearchQueryJSON(queryString); - expect(queryJSON?.status).toBe(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])', () => { @@ -74,24 +74,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(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(queryJSON?.policyID).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`); }); @@ -99,7 +92,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(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])); @@ -135,7 +128,7 @@ describe('buildRepaidLast30DaysQuery', () => { it('produces status:paid', () => { const queryJSON = buildSearchQueryJSON(queryString); - expect(queryJSON?.status).toBe(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 4d63c9ceb59e..a51976dc9831 100644 --- a/tests/unit/SearchActionsTest.ts +++ b/tests/unit/SearchActionsTest.ts @@ -6,8 +6,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, @@ -36,7 +34,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 ae1a99a38024..f53542dc1f91 100644 --- a/tests/unit/SearchParserTest.ts +++ b/tests/unit/SearchParserTest.ts @@ -11,7 +11,6 @@ const tests = [ query: parserCommonTests.simple, expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -22,7 +21,6 @@ const tests = [ query: parserCommonTests.userFriendlyNames, expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -61,7 +59,6 @@ const tests = [ query: parserCommonTests.oldNames, expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -100,7 +97,6 @@ const tests = [ query: parserCommonTests.complex, expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -147,7 +143,6 @@ const tests = [ query: parserCommonTests.quotesIOS, expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -162,7 +157,6 @@ const tests = [ query: ',', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -177,7 +171,6 @@ const tests = [ query: 'currency:,', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -192,7 +185,6 @@ const tests = [ query: 'tag:,,travel,', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -207,7 +199,6 @@ const tests = [ query: 'category:', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -222,7 +213,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', @@ -253,7 +243,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', @@ -284,7 +273,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', @@ -315,7 +303,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', @@ -386,7 +373,6 @@ const tests = [ query: 'amount>200 las vegas', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -409,18 +395,147 @@ 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: '-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: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -451,7 +566,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', @@ -506,7 +620,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', @@ -521,7 +634,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', @@ -536,7 +648,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', @@ -551,7 +662,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', @@ -566,7 +676,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', @@ -581,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', @@ -596,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', @@ -611,7 +718,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', @@ -626,7 +732,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', @@ -638,7 +743,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', @@ -650,7 +754,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', @@ -662,7 +765,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', @@ -674,7 +776,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', @@ -686,7 +787,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', @@ -698,7 +798,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', @@ -710,7 +809,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', @@ -722,7 +820,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', @@ -734,7 +831,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', @@ -746,7 +842,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', @@ -761,7 +856,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', @@ -776,7 +870,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', @@ -791,7 +884,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', @@ -806,7 +898,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', @@ -821,7 +912,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', @@ -836,7 +926,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', @@ -851,7 +940,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', @@ -866,7 +954,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', @@ -881,7 +968,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', @@ -896,7 +982,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', @@ -918,7 +1003,6 @@ const keywordTests = [ query: '" " " "', // Multiple whitespaces wrapped in quotes expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -933,7 +1017,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', @@ -948,7 +1031,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', @@ -963,7 +1045,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', @@ -978,7 +1059,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', @@ -1009,7 +1089,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', @@ -1032,7 +1111,6 @@ const keywordTests = [ query: 'expense-type:per-diem', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1047,7 +1125,6 @@ const keywordTests = [ query: 'receipt-type:ereceipt', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1062,7 +1139,6 @@ const keywordTests = [ query: 'receipt-type:hotel,itemized', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1077,7 +1153,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', @@ -1097,7 +1172,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', @@ -1115,7 +1189,6 @@ const keywordTests = [ query: 'columns:purchase-amount,tax,report-id', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1127,7 +1200,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', @@ -1148,7 +1220,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', @@ -1168,7 +1239,6 @@ const keywordTests = [ query: 'columns:tax', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1180,7 +1250,6 @@ const keywordTests = [ query: 'merchant:tax', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1195,7 +1264,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', @@ -1215,7 +1283,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', @@ -1227,7 +1294,6 @@ const viewAndGroupByTests = [ query: 'type:expense view:table', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1238,11 +1304,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 @@ -1250,7 +1319,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', @@ -1264,7 +1332,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', @@ -1276,7 +1343,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', @@ -1290,7 +1356,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', @@ -1304,7 +1369,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', @@ -1316,7 +1380,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', @@ -1330,7 +1393,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', @@ -1344,7 +1406,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', @@ -1358,7 +1419,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', @@ -1370,7 +1430,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', @@ -1382,7 +1441,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', @@ -1394,7 +1452,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', @@ -1410,7 +1467,6 @@ const limitTests = [ query: 'type:expense limit:10', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1423,7 +1479,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', @@ -1440,7 +1495,6 @@ const limitTests = [ query: 'type:expense LIMIT:25', expected: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', view: 'table', @@ -1453,7 +1507,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 489b26f1f0e8..d674b9a35980 100644 --- a/tests/unit/hooks/useAdvancedSearchFilters.test.ts +++ b/tests/unit/hooks/useAdvancedSearchFilters.test.ts @@ -215,7 +215,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(); @@ -228,7 +228,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(); 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/useAutocompleteSuggestions.test.ts b/tests/unit/hooks/useAutocompleteSuggestions.test.ts index ce41f57ac4a3..61027e145191 100644 --- a/tests/unit/hooks/useAutocompleteSuggestions.test.ts +++ b/tests/unit/hooks/useAutocompleteSuggestions.test.ts @@ -264,7 +264,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/useExportedToFilterOptions.test.ts b/tests/unit/hooks/useExportedToFilterOptions.test.ts index c130e6d38827..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/PolicyUtils', () => ({ - 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])); }); }); diff --git a/tests/unit/hooks/useSearchBulkActionsDeleteTest.ts b/tests/unit/hooks/useSearchBulkActionsDeleteTest.ts index 1bc8c5079b65..c60898ce4a21 100644 --- a/tests/unit/hooks/useSearchBulkActionsDeleteTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsDeleteTest.ts @@ -259,7 +259,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, @@ -352,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/useSearchBulkActionsDownloadPDFTest.ts b/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts index dedf9295de19..a70a78bc4ad8 100644 --- a/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts @@ -87,7 +87,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 775d84b80390..6d500da071be 100644 --- a/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts @@ -193,7 +193,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, @@ -224,7 +223,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 7b7bef30a810..e39922e4977e 100644 --- a/tests/unit/hooks/useSearchBulkActionsExportTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsExportTest.ts @@ -254,7 +254,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, @@ -322,7 +321,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 7daeed8ff4ed..877513b6911a 100644 --- a/tests/unit/hooks/useSearchBulkActionsTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsTest.ts @@ -192,7 +192,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/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 diff --git a/tests/unit/useSearchHighlightAndScrollTest.ts b/tests/unit/useSearchHighlightAndScrollTest.ts index 83e4428e13e0..dc812f75fa56 100644 --- a/tests/unit/useSearchHighlightAndScrollTest.ts +++ b/tests/unit/useSearchHighlightAndScrollTest.ts @@ -6,7 +6,6 @@ import type {UseSearchHighlightAndScroll} from '@hooks/useSearchHighlightAndScro import {search} from '@libs/actions/Search'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import Onyx from 'react-native-onyx'; @@ -46,7 +45,7 @@ describe('useSearchHighlightAndScroll', () => { hasMoreResults: false, hasResults: true, offset: 0, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hash: 0, type: 'expense', isLoading: false, }, @@ -57,7 +56,6 @@ describe('useSearchHighlightAndScroll', () => { previousReportActions: {}, queryJSON: { type: 'expense', - status: CONST.SEARCH.STATUS.EXPENSE.ALL, sortBy: 'date', sortOrder: 'desc', filters: {operator: 'and', left: 'tag', right: ''},