diff --git a/src/CONST.ts b/src/CONST.ts index 7ea66fab2b95..bc788923026a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -773,6 +773,7 @@ const CONST = { TABLE_REPORT_VIEW: 'tableReportView', HELP_SIDE_PANEL: 'newDotHelpSidePanel', RECEIPT_LINE_ITEMS: 'receiptLineItems', + LEFT_HAND_BAR: 'leftHandBar', }, BUTTON_STATES: { DEFAULT: 'default', @@ -6442,7 +6443,6 @@ const CONST = { STATUS: 'status', SORT_BY: 'sortBy', SORT_ORDER: 'sortOrder', - POLICY_ID: 'policyID', GROUP_BY: 'groupBy', }, SYNTAX_FILTER_KEYS: { @@ -6467,6 +6467,7 @@ const CONST = { PAID: 'paid', EXPORTED: 'exported', POSTED: 'posted', + POLICY_ID: 'policyID', }, EMPTY_VALUE: 'none', SEARCH_ROUTER_ITEM_TYPE: { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b839de81ab24..6b55f97b40e8 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -68,6 +68,7 @@ const ROUTES = { SEARCH_ADVANCED_FILTERS_PAID: 'search/filters/paid', SEARCH_ADVANCED_FILTERS_EXPORTED: 'search/filters/exported', SEARCH_ADVANCED_FILTERS_POSTED: 'search/filters/posted', + SEARCH_ADVANCED_FILTERS_WORKSPACE: 'search/filters/workspace', SEARCH_REPORT: { route: 'search/view/:reportID/:reportActionID?', getRoute: ({ diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 5b00de790ba5..562b047efe97 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -61,6 +61,7 @@ const SCREENS = { ADVANCED_FILTERS_TAG_RHP: 'Search_Advanced_Filters_Tag_RHP', ADVANCED_FILTERS_FROM_RHP: 'Search_Advanced_Filters_From_RHP', ADVANCED_FILTERS_TO_RHP: 'Search_Advanced_Filters_To_RHP', + ADVANCED_FILTERS_WORKSPACE_RHP: 'Search_Advanced_Filters_Workspace_RHP', SAVED_SEARCH_RENAME_RHP: 'Search_Saved_Search_Rename_RHP', ADVANCED_FILTERS_IN_RHP: 'Search_Advanced_Filters_In_RHP', TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP', diff --git a/src/components/Navigation/BottomTabBar/index.tsx b/src/components/Navigation/BottomTabBar/index.tsx index efbedba70d88..a104479c7eb3 100644 --- a/src/components/Navigation/BottomTabBar/index.tsx +++ b/src/components/Navigation/BottomTabBar/index.tsx @@ -102,7 +102,7 @@ function BottomTabBar({selectedTab, isTooltipAllowed = false}: BottomTabBarProps } } // when navigating to search we might have an activePolicyID set from workspace switcher - const query = activeWorkspaceID ? `${defaultCannedQuery} ${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${activeWorkspaceID}` : defaultCannedQuery; + const query = activeWorkspaceID ? `${defaultCannedQuery} ${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${activeWorkspaceID}` : defaultCannedQuery; Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query})); }); }, [activeWorkspaceID, selectedTab]); diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx index 3b50c7f071c8..0be5ef0ae02c 100644 --- a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx @@ -20,6 +20,7 @@ import type {SearchQueryItem} from '@components/SelectionList/Search/SearchQuery import type {SelectionListHandle} from '@components/SelectionList/types'; import HelpButton from '@components/SidePanel/HelpComponents/HelpButton'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -31,7 +32,14 @@ import Navigation from '@libs/Navigation/Navigation'; import {getAllTaxRates} from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; import {getAutocompleteQueryWithComma, getQueryWithoutAutocompletedPart} from '@libs/SearchAutocompleteUtils'; -import {buildUserReadableQueryString, getQueryWithUpdatedValues, isDefaultExpensesQuery, sanitizeSearchValue} from '@libs/SearchQueryUtils'; +import { + buildUserReadableQueryString, + buildUserReadableQueryStringWithPolicyID, + getQueryWithUpdatedValues, + isDefaultExpensesQuery, + isDefaultExpensesQueryWithPolicyIDCheck, + sanitizeSearchValue, +} from '@libs/SearchQueryUtils'; import StringUtils from '@libs/StringUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -54,12 +62,14 @@ type SearchPageHeaderInputProps = { function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRouterList, onSearchRouterFocus, searchName, inputRightComponent}: SearchPageHeaderInputProps) { const {translate} = useLocalize(); + const {canUseLeftHandBar} = usePermissions(); const [showPopupButton, setShowPopupButton] = useState(true); const styles = useThemeStyles(); const theme = useTheme(); const {shouldUseNarrowLayout: displayNarrowHeader} = useResponsiveLayout(); const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); + const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const taxRates = useMemo(() => getAllTaxRates(), []); const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST); const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST); @@ -68,9 +78,11 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo return getCardFeedNamesWithType({workspaceCardFeeds, translate}); }, [translate, workspaceCardFeeds]); const {inputQuery: originalInputQuery} = queryJSON; - const isDefaultQuery = isDefaultExpensesQuery(queryJSON); + const isDefaultQuery = canUseLeftHandBar ? isDefaultExpensesQueryWithPolicyIDCheck(queryJSON) : isDefaultExpensesQuery(queryJSON); const [shouldUseAnimation, setShouldUseAnimation] = useState(false); - const queryText = buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType); + const queryText = canUseLeftHandBar + ? buildUserReadableQueryStringWithPolicyID(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType, policies) + : buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType); // The actual input text that the user sees const [textInputValue, setTextInputValue] = useState(isDefaultQuery ? '' : queryText); diff --git a/src/components/Search/SearchPageHeader/SearchTypeMenuPopover.tsx b/src/components/Search/SearchPageHeader/SearchTypeMenuPopover.tsx index 02e488750c78..b9106cdabecc 100644 --- a/src/components/Search/SearchPageHeader/SearchTypeMenuPopover.tsx +++ b/src/components/Search/SearchPageHeader/SearchTypeMenuPopover.tsx @@ -10,6 +10,7 @@ import type {SearchQueryJSON} from '@components/Search/types'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import useDeleteSavedSearch from '@hooks/useDeleteSavedSearch'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useSingleExecution from '@hooks/useSingleExecution'; import useTheme from '@hooks/useTheme'; @@ -20,7 +21,13 @@ import {getCardFeedNamesWithType} from '@libs/CardFeedUtils'; import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getAllTaxRates} from '@libs/PolicyUtils'; -import {buildSearchQueryJSON, buildUserReadableQueryString, isCannedSearchQuery} from '@libs/SearchQueryUtils'; +import { + buildSearchQueryJSON, + buildUserReadableQueryString, + buildUserReadableQueryStringWithPolicyID, + isCannedSearchQuery, + isCannedSearchQueryWithPolicyIDCheck, +} from '@libs/SearchQueryUtils'; import {createBaseSavedSearchMenuItem, createTypeMenuItems, getOverflowMenu as getOverflowMenuUtil} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import * as Expensicons from '@src/components/Icon/Expensicons'; @@ -58,6 +65,7 @@ function SearchTypeMenuPopover({queryJSON, searchName}: SearchTypeMenuNarrowProp const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST); const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]); const {unmodifiedPaddings} = useSafeAreaPaddings(); + const {canUseLeftHandBar} = usePermissions(); const shouldGroupByReports = groupBy === CONST.SEARCH.GROUP_BY.REPORTS; const cardFeedNamesWithType = useMemo(() => { return getCardFeedNamesWithType({workspaceCardFeeds, translate}); @@ -78,9 +86,16 @@ function SearchTypeMenuPopover({queryJSON, searchName}: SearchTypeMenuNarrowProp }, 100); }, []); + const getBuildUserReadableQueryString = useCallback(() => { + if (canUseLeftHandBar) { + return buildUserReadableQueryStringWithPolicyID(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType, allPolicies); + } + return buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType); + }, [allCards, allPolicies, canUseLeftHandBar, cardFeedNamesWithType, personalDetails, queryJSON, reports, taxRates]); + const typeMenuItems = useMemo(() => createTypeMenuItems(allPolicies, session?.email), [allPolicies, session?.email]); - const isCannedQuery = isCannedSearchQuery(queryJSON); - const title = searchName ?? (isCannedQuery ? undefined : buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType)); + const isCannedQuery = canUseLeftHandBar ? isCannedSearchQueryWithPolicyIDCheck(queryJSON) : isCannedSearchQuery(queryJSON); + const title = searchName ?? (isCannedQuery ? undefined : getBuildUserReadableQueryString()); const activeItemIndex = isCannedQuery ? typeMenuItems.findIndex((item) => item.type === queryJSON.type) : -1; const getOverflowMenu = useCallback( @@ -93,7 +108,11 @@ function SearchTypeMenuPopover({queryJSON, searchName}: SearchTypeMenuNarrowProp let savedSearchTitle = item.name; if (savedSearchTitle === item.query) { const jsonQuery = buildSearchQueryJSON(item.query) ?? ({} as SearchQueryJSON); - savedSearchTitle = buildUserReadableQueryString(jsonQuery, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType); + if (canUseLeftHandBar) { + savedSearchTitle = buildUserReadableQueryStringWithPolicyID(jsonQuery, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType, allPolicies); + } else { + savedSearchTitle = buildUserReadableQueryString(jsonQuery, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType); + } } const isItemFocused = Number(key) === hash; const baseMenuItem: SavedSearchMenuItem = createBaseSavedSearchMenuItem(item, key, index, savedSearchTitle, isItemFocused); @@ -124,7 +143,7 @@ function SearchTypeMenuPopover({queryJSON, searchName}: SearchTypeMenuNarrowProp shouldIconUseAutoWidthStyle: false, }; }, - [hash, getOverflowMenu, styles.textSupporting, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType], + [hash, getOverflowMenu, styles.textSupporting, canUseLeftHandBar, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType, allPolicies], ); const savedSearchItems = useMemo(() => { diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 04151ee4c7e3..5c8f089c330d 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -98,7 +98,7 @@ type SearchFilterKey = | ValueOf | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS - | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID + | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY; type UserFriendlyKey = ValueOf; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 0d6e61af8c11..416e5160fafe 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -717,6 +717,7 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersFromPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersToPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersInPage').default, + [SCREENS.SEARCH.ADVANCED_FILTERS_WORKSPACE_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersWorkspacePage').default, }); const SearchSavedSearchModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts index feee223c233c..5bb7b03ba723 100644 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts @@ -24,6 +24,7 @@ const SEARCH_TO_RHP: string[] = [ SCREENS.SEARCH.ADVANCED_FILTERS_FROM_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_WORKSPACE_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP, SCREENS.SEARCH.SAVED_SEARCH_RENAME_RHP, ]; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index beb7681902e7..36d3897afcfc 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1431,6 +1431,7 @@ const config: LinkingOptions['config'] = { [SCREENS.SEARCH.ADVANCED_FILTERS_FROM_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_FROM, [SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_TO, [SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_IN, + [SCREENS.SEARCH.ADVANCED_FILTERS_WORKSPACE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_WORKSPACE, }, }, [SCREENS.RIGHT_MODAL.SEARCH_SAVED_SEARCH]: { diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 4495bf1b7ec9..f412cce51612 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -66,6 +66,10 @@ function canUseProhibitedExpenses(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.RECEIPT_LINE_ITEMS) || canUseAllBetas(betas); } +function canUseLeftHandBar(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.LEFT_HAND_BAR) || canUseAllBetas(betas); +} + export default { canUseDefaultRooms, canUseLinkPreviews, @@ -81,4 +85,5 @@ export default { canUseHelpSidePanel, canUseTalkToAISales, canUseProhibitedExpenses, + canUseLeftHandBar, }; diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 571d54428e5d..9ca5176bc877 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -251,7 +251,7 @@ function getQueryHashes(query: SearchQueryJSON): {primaryHash: number; recentSea orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY}:${query.sortBy}`; orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_ORDER}:${query.sortOrder}`; if (query.policyID) { - orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${query.policyID} `; + orderedQuery += ` ${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${query.policyID} `; } const primaryHash = hashText(orderedQuery, 2 ** 32); @@ -305,6 +305,10 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) { } } + if (queryJSON?.policyID) { + queryParts.push(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${queryJSON.policyID}`); + } + if (!queryJSON) { return queryParts.join(' '); } @@ -345,7 +349,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial, + taxRates: Record, + cardList: OnyxTypes.CardList, + cardFeedNamesWithType: CardFeedNamesWithType, + policies: OnyxCollection, +) { + const {type, status, groupBy, policyID} = queryJSON; + const filters = queryJSON.flatFilters; + + let title = `type:${type} status:${Array.isArray(status) ? status.join(',') : status}`; + + if (groupBy) { + title += ` group-by:${groupBy}`; + } + + if (policyID) { + const workspace = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.name ?? policyID; + title += ` workspace:${sanitizeSearchValue(workspace)}`; + } + + for (const filterObject of filters) { + const key = filterObject.key; + const queryFilter = filterObject.filters; + + let displayQueryFilters: QueryFilter[] = []; + if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) { + const taxRateIDs = queryFilter.map((filter) => filter.value.toString()); + const taxRateNames = taxRateIDs + .map((id) => { + const taxRate = Object.entries(taxRates) + .filter(([, IDs]) => IDs.includes(id)) + .map(([name]) => name); + return taxRate.length > 0 ? taxRate : id; + }) + .flat(); + + const uniqueTaxRateNames = [...new Set(taxRateNames)]; + + displayQueryFilters = uniqueTaxRateNames.map((taxRate) => ({ + operator: queryFilter.at(0)?.operator ?? CONST.SEARCH.SYNTAX_OPERATORS.AND, + value: taxRate, + })); + } else { + displayQueryFilters = queryFilter.map((filter) => ({ + operator: filter.operator, + value: getFilterDisplayValue(key, filter.value.toString(), PersonalDetails, reports, cardList, cardFeedNamesWithType), + })); + } + title += buildFilterValuesString(getUserFriendlyKey(key), displayQueryFilters); + } + + return title; +} + /** * Formats a given `SearchQueryJSON` object into the human-readable string version of query. * This format of query is the one which we want to display to users. @@ -680,6 +749,20 @@ function buildCannedSearchQuery({ return buildSearchQueryString(normalizedQueryJSON); } +/** + * A copy of `isCannedSearchQuery` handling the policy ID, used if you have access to the leftHandBar beta. + * When this beta is no longer needed, this method will be renamed to `isCannedSearchQuery` and will replace the old method. + * + * Returns whether a given search query is a Canned query. + * + * Canned queries are simple predefined queries, that are defined only using type and status and no additional filters. + * In addition, they can contain an optional policyID. + * For example: "type:trip status:all" is a canned query. + */ +function isCannedSearchQueryWithPolicyIDCheck(queryJSON: SearchQueryJSON) { + return !queryJSON.filters && !queryJSON.policyID; +} + /** * Returns whether a given search query is a Canned query. * @@ -691,6 +774,15 @@ function isCannedSearchQuery(queryJSON: SearchQueryJSON) { return !queryJSON.filters; } +/** + * A copy of `isDefaultExpensesQuery` handling the policy ID, used if you have access to the leftHandBar beta. + * When this beta is no longer needed, this method will be renamed to `isDefaultExpensesQuery` and will replace the old method. + * + */ +function isDefaultExpensesQueryWithPolicyIDCheck(queryJSON: SearchQueryJSON) { + return queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE && queryJSON.status === CONST.SEARCH.STATUS.EXPENSE.ALL && !queryJSON.filters && !queryJSON.groupBy && !queryJSON.policyID; +} + function isDefaultExpensesQuery(queryJSON: SearchQueryJSON) { return queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE && queryJSON.status === CONST.SEARCH.STATUS.EXPENSE.ALL && !queryJSON.filters && !queryJSON.groupBy; } @@ -765,14 +857,17 @@ export { buildSearchQueryJSON, buildSearchQueryString, buildUserReadableQueryString, + buildUserReadableQueryStringWithPolicyID, getFilterDisplayValue, buildQueryStringFromFilterFormValues, buildFilterFormValuesFromQuery, getPolicyIDFromSearchQuery, buildCannedSearchQuery, isCannedSearchQuery, + isCannedSearchQueryWithPolicyIDCheck, sanitizeSearchValue, getQueryWithUpdatedValues, getUserFriendlyKey, isDefaultExpensesQuery, + isDefaultExpensesQueryWithPolicyIDCheck, }; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index e905e408603b..f6facf8c8c7d 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -21,6 +21,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm'; import type {LastPaymentMethod, LastPaymentMethodType, SearchResults} from '@src/types/onyx'; import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; +import type Nullable from '@src/types/utils/Nullable'; let lastPaymentMethod: OnyxEntry; Onyx.connect({ @@ -394,7 +395,7 @@ function exportSearchItemsToCSV({query, jsonQuery, reportIDList, transactionIDLi /** * Updates the form values for the advanced filters search form. */ -function updateAdvancedFilters(values: Partial>) { +function updateAdvancedFilters(values: Nullable>>) { Onyx.merge(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, values); } diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index 964f8efa76aa..fe12ab4d9fdd 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -12,6 +12,7 @@ import ScrollView from '@components/ScrollView'; import type {SearchDateFilterKeys, SearchFilterKey} from '@components/Search/types'; import SpacerView from '@components/SpacerView'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; @@ -24,7 +25,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {createDisplayName} from '@libs/PersonalDetailsUtils'; import {getAllTaxRates, getCleanedTagName, getTagNamesFromTagsLists, isPolicyFeatureEnabled} from '@libs/PolicyUtils'; import {getReportName} from '@libs/ReportUtils'; -import {buildCannedSearchQuery, buildQueryStringFromFilterFormValues, buildSearchQueryJSON, isCannedSearchQuery} from '@libs/SearchQueryUtils'; +import {buildCannedSearchQuery, buildQueryStringFromFilterFormValues, buildSearchQueryJSON, isCannedSearchQuery, isCannedSearchQueryWithPolicyIDCheck} from '@libs/SearchQueryUtils'; import {getExpenseTypeTranslationKey} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -136,6 +137,11 @@ const baseFilterConfig = { description: 'common.in' as const, route: ROUTES.SEARCH_ADVANCED_FILTERS_IN, }, + policyID: { + getTitle: getFilterWorkspaceDisplayTitle, + description: 'workspace.common.workspace' as const, + route: ROUTES.SEARCH_ADVANCED_FILTERS_WORKSPACE, + }, }; /** @@ -213,6 +219,11 @@ const typeFiltersKeys: Record, policies: OnyxCollection) { + const workspaceFilter = filters[CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID]; + return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${workspaceFilter}`]?.name ?? workspaceFilter; +} + function getFilterCardDisplayTitle(filters: Partial, cards: CardList, translate: LocaleContextProps['translate']) { const cardIdsFilter = filters[CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID] ?? []; const feedFilter = filters[CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED] ?? []; @@ -402,7 +413,26 @@ function AdvancedSearchFilters() { const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList, true), [userCardList, workspaceCardFeeds]); const taxRates = getAllTaxRates(); const personalDetails = usePersonalDetails(); - + const {canUseLeftHandBar} = usePermissions(); + + // If users have access to the leftHandBar beta, then the workspace filter is displyed in the first section of the advanced search filters + const typeFiltersKeysWithOptionalPolicy = useMemo( + () => + canUseLeftHandBar + ? Object.fromEntries( + Object.entries(typeFiltersKeys).map(([key, arrays]) => { + const firstFiltersSection = arrays.at(0); + if (!firstFiltersSection) { + return [key, arrays]; + } + + const modifiedFirstFiltersSection = [...firstFiltersSection, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID]; + return [key, [modifiedFirstFiltersSection, ...arrays.slice(1)]]; + }), + ) + : typeFiltersKeys, + [canUseLeftHandBar], + ); const [policies = {}] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [allPolicyCategories = {}] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, { selector: (policyCategories) => @@ -440,7 +470,7 @@ function AdvancedSearchFilters() { const shouldDisplayTaxFilter = shouldDisplayFilter(Object.keys(taxRates).length, areTaxEnabled); let currentType = searchAdvancedFilters?.type ?? CONST.SEARCH.DATA_TYPES.EXPENSE; - if (!Object.keys(typeFiltersKeys).includes(currentType)) { + if (!Object.keys(typeFiltersKeysWithOptionalPolicy).includes(currentType)) { currentType = CONST.SEARCH.DATA_TYPES.EXPENSE; } @@ -472,7 +502,7 @@ function AdvancedSearchFilters() { applyFiltersAndNavigate(); }; - const filters = typeFiltersKeys[currentType] + const filters = typeFiltersKeysWithOptionalPolicy[currentType] .map((section) => { return section .map((key) => { @@ -527,6 +557,8 @@ function AdvancedSearchFilters() { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters[key] ?? [], personalDetails); } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.IN) { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, translate, reports); + } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID) { + filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, policies); } return { key, @@ -538,7 +570,7 @@ function AdvancedSearchFilters() { .filter((filter): filter is NonNullable => !!filter); }) .filter((section) => !!section.length); - const displaySearchButton = queryJSON && !isCannedSearchQuery(queryJSON); + const displaySearchButton = queryJSON && (canUseLeftHandBar ? !isCannedSearchQueryWithPolicyIDCheck(queryJSON) : !isCannedSearchQuery(queryJSON)); return ( <> diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersWorkspacePage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersWorkspacePage.tsx new file mode 100644 index 000000000000..3fa85c51b5a0 --- /dev/null +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersWorkspacePage.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import UserListItem from '@components/SelectionList/UserListItem'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {WorkspaceListItem} from '@hooks/useWorkspaceList'; +import useWorkspaceList from '@hooks/useWorkspaceList'; +import {updateAdvancedFilters} from '@libs/actions/Search'; +import Navigation from '@libs/Navigation/Navigation'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; + +const updateWorkspaceFilter = (policyID: string | null) => { + updateAdvancedFilters({ + policyID, + }); + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); +}; + +function SearchFiltersWorkspacePage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + + const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); + const [policies, policiesResult] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); + const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const shouldShowLoadingIndicator = isLoadingApp && !isOffline; + + const {sections, shouldShowNoResultsFoundMessage, shouldShowSearchInput} = useWorkspaceList({ + policies, + currentUserLogin, + shouldShowPendingDeletePolicy: false, + selectedPolicyID: searchAdvancedFiltersForm?.policyID, + searchTerm: debouncedSearchTerm, + }); + + return ( + + {({didScreenTransitionEnd}) => ( + <> + { + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); + }} + /> + {shouldShowLoadingIndicator ? ( + + ) : ( + + ListItem={UserListItem} + sections={sections} + onSelectRow={(option) => { + if (option.policyID === searchAdvancedFiltersForm?.policyID || !option.policyID) { + updateWorkspaceFilter(null); + return; + } + updateWorkspaceFilter(option.policyID); + }} + textInputLabel={shouldShowSearchInput ? translate('common.search') : undefined} + textInputValue={searchTerm} + onChangeText={setSearchTerm} + headerMessage={shouldShowNoResultsFoundMessage ? translate('common.noResultsFound') : ''} + initiallyFocusedOptionKey={searchAdvancedFiltersForm?.policyID} + showLoadingPlaceholder={isLoadingOnyxValue(policiesResult) || !didScreenTransitionEnd} + /> + )} + + )} + + ); +} + +SearchFiltersWorkspacePage.displayName = 'SearchFiltersWorkspacePage'; + +export default SearchFiltersWorkspacePage; diff --git a/src/pages/Search/SearchPageNarrow.tsx b/src/pages/Search/SearchPageNarrow.tsx index c08ba61d9fe3..a642ba383d75 100644 --- a/src/pages/Search/SearchPageNarrow.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -17,6 +17,7 @@ import SearchStatusBar from '@components/Search/SearchPageHeader/SearchStatusBar import type {SearchQueryJSON} from '@components/Search/types'; import useHandleBackButton from '@hooks/useHandleBackButton'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useScrollEventEmitter from '@hooks/useScrollEventEmitter'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -24,7 +25,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import Navigation from '@libs/Navigation/Navigation'; -import {buildCannedSearchQuery, isCannedSearchQuery} from '@libs/SearchQueryUtils'; +import {buildCannedSearchQuery, isCannedSearchQuery, isCannedSearchQueryWithPolicyIDCheck} from '@libs/SearchQueryUtils'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -52,6 +53,7 @@ function SearchPageNarrow({queryJSON, policyID, searchName, headerButtonsOptions const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); const {clearSelectedTransactions} = useSearchContext(); const [searchRouterListVisible, setSearchRouterListVisible] = useState(false); + const {canUseLeftHandBar} = usePermissions(); const searchResults = currentSearchResults?.data ? currentSearchResults : lastNonEmptySearchResults; // Controls the visibility of the educational tooltip based on user scrolling. @@ -96,7 +98,17 @@ function SearchPageNarrow({queryJSON, policyID, searchName, headerButtonsOptions const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()})); - const shouldDisplayCancelSearch = shouldUseNarrowLayout && ((!!queryJSON && !isCannedSearchQuery(queryJSON)) || searchRouterListVisible); + let isCannedQuery = false; + + if (queryJSON) { + if (canUseLeftHandBar) { + isCannedQuery = isCannedSearchQueryWithPolicyIDCheck(queryJSON); + } else { + isCannedQuery = isCannedSearchQuery(queryJSON); + } + } + + const shouldDisplayCancelSearch = shouldUseNarrowLayout && (!isCannedQuery || searchRouterListVisible); const cancelSearchCallback = useCallback(() => { if (searchRouterListVisible) { setSearchRouterListVisible(false); diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 1cfede643771..87a8ae891b37 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -17,6 +17,7 @@ import Text from '@components/Text'; import useDeleteSavedSearch from '@hooks/useDeleteSavedSearch'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearAllFilters} from '@libs/actions/Search'; @@ -24,7 +25,13 @@ import {getCardFeedNamesWithType} from '@libs/CardFeedUtils'; import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getAllTaxRates} from '@libs/PolicyUtils'; -import {buildSearchQueryJSON, buildUserReadableQueryString, isCannedSearchQuery} from '@libs/SearchQueryUtils'; +import { + buildSearchQueryJSON, + buildUserReadableQueryString, + buildUserReadableQueryStringWithPolicyID, + isCannedSearchQuery, + isCannedSearchQueryWithPolicyIDCheck, +} from '@libs/SearchQueryUtils'; import {createBaseSavedSearchMenuItem, createTypeMenuItems, getOverflowMenu as getOverflowMenuUtil} from '@libs/SearchUIUtils'; import type {SavedSearchMenuItem, SearchTypeMenuItem} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; @@ -44,6 +51,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { const styles = useThemeStyles(); const {singleExecution} = useSingleExecution(); const {translate} = useLocalize(); + const {canUseLeftHandBar} = usePermissions(); const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES); const {isOffline} = useNetwork(); const shouldShowSavedSearchesMenuItemTitle = Object.values(savedSearches ?? {}).filter((s) => s.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline).length > 0; @@ -74,7 +82,11 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { let title = item.name; if (title === item.query) { const jsonQuery = buildSearchQueryJSON(item.query) ?? ({} as SearchQueryJSON); - title = buildUserReadableQueryString(jsonQuery, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType); + if (canUseLeftHandBar) { + title = buildUserReadableQueryStringWithPolicyID(jsonQuery, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType, allPolicies); + } else { + title = buildUserReadableQueryString(jsonQuery, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType); + } } const isItemFocused = Number(key) === hash; @@ -121,6 +133,8 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { hideProductTrainingTooltip, renderProductTrainingTooltip, cardFeedNamesWithType, + allPolicies, + canUseLeftHandBar, ], ); @@ -170,7 +184,16 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { [styles], ); - const isCannedQuery = queryJSON ? isCannedSearchQuery(queryJSON) : false; + let isCannedQuery = false; + + if (queryJSON) { + if (canUseLeftHandBar) { + isCannedQuery = isCannedSearchQueryWithPolicyIDCheck(queryJSON); + } else { + isCannedQuery = isCannedSearchQuery(queryJSON); + } + } + const activeItemIndex = isCannedQuery ? typeMenuItems.findIndex((item) => { if (groupBy === CONST.SEARCH.GROUP_BY.REPORTS) { diff --git a/src/types/utils/Nullable.ts b/src/types/utils/Nullable.ts new file mode 100644 index 000000000000..caba46ef5b58 --- /dev/null +++ b/src/types/utils/Nullable.ts @@ -0,0 +1,3 @@ +type Nullable = {[K in keyof T]: T[K] | null}; + +export default Nullable;