Skip to content

Commit f3f5d3e

Browse files
authored
Merge pull request Expensify#82966 from callstack-internal/callstack-internal/szymonzalarski/decouple-badge-logic-consolidate-policy-loop
Decouple badge logic and consolidate policy loop in createTypeMenuSec…
2 parents d253f2b + 79d94a3 commit f3f5d3e

5 files changed

Lines changed: 133 additions & 136 deletions

File tree

src/hooks/useSearchTypeMenu.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import Navigation from '@libs/Navigation/Navigation';
1111
import {getAllTaxRates} from '@libs/PolicyUtils';
1212
import {buildSearchQueryJSON, buildUserReadableQueryString, shouldSkipSuggestedSearchNavigation as shouldSkipSuggestedSearchNavigationForQuery} from '@libs/SearchQueryUtils';
1313
import type {SavedSearchMenuItem} from '@libs/SearchUIUtils';
14-
import {createBaseSavedSearchMenuItem, getOverflowMenu as getOverflowMenuUtil} from '@libs/SearchUIUtils';
14+
import {createBaseSavedSearchMenuItem, getItemBadgeText, getOverflowMenu as getOverflowMenuUtil} from '@libs/SearchUIUtils';
1515
import variables from '@styles/variables';
1616
import CONST from '@src/CONST';
1717
import ONYXKEYS from '@src/ONYXKEYS';
1818
import ROUTES from '@src/ROUTES';
19+
import todosReportCountsSelector from '@src/selectors/Todos';
1920
import type {Report} from '@src/types/onyx';
2021
import {getEmptyObject} from '@src/types/utils/EmptyObject';
2122
import useDeleteSavedSearch from './useDeleteSavedSearch';
@@ -49,6 +50,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {
4950
const [personalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true});
5051
const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true});
5152
const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false});
53+
const [reportCounts = CONST.EMPTY_TODOS_REPORT_COUNTS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {canBeMissing: true, selector: todosReportCountsSelector});
5254
const expensifyIcons = useMemoizedLazyExpensifyIcons([
5355
'Basket',
5456
'Bookmark',
@@ -212,7 +214,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {
212214
const icon = typeof item.icon === 'string' ? expensifyIcons[item.icon] : item.icon;
213215

214216
sectionItems.push({
215-
badgeText: item.badgeText,
217+
badgeText: getItemBadgeText(item.key, reportCounts),
216218
text: translate(item.translationPath),
217219
isSelected,
218220
icon,
@@ -230,7 +232,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {
230232
return sectionItems;
231233
})
232234
.flat();
233-
}, [typeMenuSections, translate, styles.textSupporting, savedSearchesMenuItems, activeItemIndex, theme.border, expensifyIcons, singleExecution]);
235+
}, [typeMenuSections, translate, styles.textSupporting, savedSearchesMenuItems, activeItemIndex, theme.border, expensifyIcons, singleExecution, reportCounts]);
234236

235237
const openMenu = useCallback(() => {
236238
setIsPopoverVisible(true);

src/hooks/useSearchTypeMenuSections.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import {useCallback, useEffect, useMemo, useState} from 'react';
33
import type {OnyxEntry} from 'react-native-onyx';
44
import {areAllGroupPoliciesExpenseChatDisabled} from '@libs/PolicyUtils';
55
import {createTypeMenuSections} from '@libs/SearchUIUtils';
6-
import CONST from '@src/CONST';
76
import ONYXKEYS from '@src/ONYXKEYS';
8-
import todosReportCountsSelector from '@src/selectors/Todos';
97
import type {NonPersonalAndWorkspaceCardListDerivedValue, Policy, Session} from '@src/types/onyx';
108
import useCardFeedsForDisplay from './useCardFeedsForDisplay';
119
import useCreateEmptyReportConfirmation from './useCreateEmptyReportConfirmation';
@@ -25,6 +23,9 @@ const policyMapper = (policy: OnyxEntry<Policy>): OnyxEntry<Policy> =>
2523
connections: policy.connections,
2624
outputCurrency: policy.outputCurrency,
2725
isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled,
26+
isJoinRequestPending: policy.isJoinRequestPending,
27+
pendingAction: policy.pendingAction,
28+
errors: policy.errors,
2829
reimburser: policy.reimburser,
2930
exporter: policy.exporter,
3031
approver: policy.approver,
@@ -58,8 +59,6 @@ const useSearchTypeMenuSections = () => {
5859
const [allPolicies] = useMappedPolicies(policyMapper);
5960
const [currentUserLoginAndAccountID] = useOnyx(ONYXKEYS.SESSION, {selector: currentUserLoginAndAccountIDSelector, canBeMissing: false});
6061
const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true});
61-
const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true});
62-
const [reportCounts = CONST.EMPTY_TODOS_REPORT_COUNTS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {canBeMissing: true, selector: todosReportCountsSelector});
6362
const shouldRedirectToExpensifyClassic = useMemo(() => areAllGroupPoliciesExpenseChatDisabled(allPolicies ?? {}), [allPolicies]);
6463
const [pendingReportCreation, setPendingReportCreation] = useState<{policyID: string; policyName?: string; onConfirm: (shouldDismissEmptyReportsConfirmation: boolean) => void} | null>(
6564
null,
@@ -108,8 +107,6 @@ const useSearchTypeMenuSections = () => {
108107
isOffline,
109108
defaultExpensifyCard,
110109
shouldRedirectToExpensifyClassic,
111-
allTransactionDrafts,
112-
reportCounts,
113110
),
114111
[
115112
currentUserLoginAndAccountID?.email,
@@ -121,9 +118,7 @@ const useSearchTypeMenuSections = () => {
121118
savedSearches,
122119
isOffline,
123120
shouldRedirectToExpensifyClassic,
124-
allTransactionDrafts,
125121
icons,
126-
reportCounts,
127122
],
128123
);
129124

src/libs/SearchUIUtils.ts

Lines changed: 56 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,7 @@ import isSearchTopmostFullScreenRoute from './Navigation/helpers/isSearchTopmost
9898
import Navigation from './Navigation/Navigation';
9999
import Parser from './Parser';
100100
import {getDisplayNameOrDefault} from './PersonalDetailsUtils';
101-
import {
102-
arePaymentsEnabled,
103-
canSendInvoice,
104-
getCommaSeparatedTagNameWithSanitizedColons,
105-
getGroupPaidPoliciesWithExpenseChatEnabled,
106-
getSubmitToAccountID,
107-
isPaidGroupPolicy,
108-
isPolicyPayer,
109-
} from './PolicyUtils';
101+
import {arePaymentsEnabled, canSendInvoice, getCommaSeparatedTagNameWithSanitizedColons, getSubmitToAccountID, isPaidGroupPolicy, isPolicyPayer} from './PolicyUtils';
110102
import {
111103
getIOUActionForReportID,
112104
getOriginalMessage,
@@ -356,6 +348,13 @@ function formatBadgeText(count: number): string {
356348
return count > CONST.SEARCH.TODO_BADGE_MAX_COUNT ? `${CONST.SEARCH.TODO_BADGE_MAX_COUNT}+` : count.toString();
357349
}
358350

351+
function getItemBadgeText(itemKey: string, reportCounts: Record<string, number>): string | undefined {
352+
if (itemKey in reportCounts) {
353+
return formatBadgeText(reportCounts[itemKey]);
354+
}
355+
return undefined;
356+
}
357+
359358
function getExpenseStatusOptions(translate: LocalizedTranslate): Array<MultiSelectItem<SingularSearchStatus>> {
360359
return [
361360
{text: translate('common.unreported'), value: CONST.SEARCH.STATUS.EXPENSE.UNREPORTED},
@@ -852,7 +851,7 @@ function getSuggestedSearchesVisibility(
852851
cardFeedsByPolicy: Record<string, CardFeedForDisplay[]>,
853852
policies: OnyxCollection<OnyxTypes.Policy>,
854853
defaultExpensifyCard: CardFeedForDisplay | undefined,
855-
): Record<ValueOf<typeof CONST.SEARCH.SEARCH_KEYS>, boolean> {
854+
): {visibility: Record<ValueOf<typeof CONST.SEARCH.SEARCH_KEYS>, boolean>; hasGroupPoliciesWithExpenseChat: boolean} {
856855
let shouldShowSubmitSuggestion = false;
857856
let shouldShowPaySuggestion = false;
858857
let shouldShowApproveSuggestion = false;
@@ -864,6 +863,7 @@ function getSuggestedSearchesVisibility(
864863
let shouldShowTopSpendersSuggestion = false;
865864
let shouldShowTopCategoriesSuggestion = false;
866865
let shouldShowTopMerchantsSuggestion = false;
866+
let hasGroupPoliciesWithExpenseChat = false;
867867
let shouldShowSpendOverTimeSuggestion = false;
868868

869869
const hasCardFeed = Object.values(cardFeedsByPolicy ?? {}).some((feeds) => feeds.length > 0);
@@ -916,6 +916,12 @@ function getSuggestedSearchesVisibility(
916916
shouldShowTopSpendersSuggestion ||= isEligibleForTopSpendersSuggestion;
917917
shouldShowTopCategoriesSuggestion ||= isEligibleForTopCategoriesSuggestion;
918918
shouldShowTopMerchantsSuggestion ||= isEligibleForTopMerchantsSuggestion;
919+
hasGroupPoliciesWithExpenseChat ||=
920+
isPaidPolicy &&
921+
!!policy.isPolicyExpenseChatEnabled &&
922+
!policy.isJoinRequestPending &&
923+
(policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) &&
924+
!!policy.role;
919925
shouldShowSpendOverTimeSuggestion ||= isEligibleForSpendOverTimeSuggestion;
920926

921927
// We don't need to check the rest of the policies if we already determined that all suggestions should be displayed
@@ -930,26 +936,30 @@ function getSuggestedSearchesVisibility(
930936
shouldShowReconciliationSuggestion &&
931937
shouldShowTopSpendersSuggestion &&
932938
shouldShowTopCategoriesSuggestion &&
933-
shouldShowTopMerchantsSuggestion
939+
shouldShowTopMerchantsSuggestion &&
940+
hasGroupPoliciesWithExpenseChat
934941
);
935942
});
936943

937944
return {
938-
[CONST.SEARCH.SEARCH_KEYS.EXPENSES]: true,
939-
[CONST.SEARCH.SEARCH_KEYS.REPORTS]: true,
940-
[CONST.SEARCH.SEARCH_KEYS.CHATS]: true,
941-
[CONST.SEARCH.SEARCH_KEYS.SUBMIT]: shouldShowSubmitSuggestion,
942-
[CONST.SEARCH.SEARCH_KEYS.PAY]: shouldShowPaySuggestion,
943-
[CONST.SEARCH.SEARCH_KEYS.APPROVE]: shouldShowApproveSuggestion,
944-
[CONST.SEARCH.SEARCH_KEYS.EXPORT]: shouldShowExportSuggestion,
945-
[CONST.SEARCH.SEARCH_KEYS.STATEMENTS]: shouldShowStatementsSuggestion,
946-
[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]: shouldShowUnapprovedCashSuggestion,
947-
[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]: shouldShowUnapprovedCardSuggestion,
948-
[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]: shouldShowReconciliationSuggestion,
949-
[CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS]: shouldShowTopSpendersSuggestion,
950-
[CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES]: shouldShowTopCategoriesSuggestion,
951-
[CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS]: shouldShowTopMerchantsSuggestion,
952-
[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]: shouldShowSpendOverTimeSuggestion,
945+
visibility: {
946+
[CONST.SEARCH.SEARCH_KEYS.EXPENSES]: true,
947+
[CONST.SEARCH.SEARCH_KEYS.REPORTS]: true,
948+
[CONST.SEARCH.SEARCH_KEYS.CHATS]: true,
949+
[CONST.SEARCH.SEARCH_KEYS.SUBMIT]: shouldShowSubmitSuggestion,
950+
[CONST.SEARCH.SEARCH_KEYS.PAY]: shouldShowPaySuggestion,
951+
[CONST.SEARCH.SEARCH_KEYS.APPROVE]: shouldShowApproveSuggestion,
952+
[CONST.SEARCH.SEARCH_KEYS.EXPORT]: shouldShowExportSuggestion,
953+
[CONST.SEARCH.SEARCH_KEYS.STATEMENTS]: shouldShowStatementsSuggestion,
954+
[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]: shouldShowUnapprovedCashSuggestion,
955+
[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]: shouldShowUnapprovedCardSuggestion,
956+
[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]: shouldShowReconciliationSuggestion,
957+
[CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS]: shouldShowTopSpendersSuggestion,
958+
[CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES]: shouldShowTopCategoriesSuggestion,
959+
[CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS]: shouldShowTopMerchantsSuggestion,
960+
[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]: shouldShowSpendOverTimeSuggestion,
961+
},
962+
hasGroupPoliciesWithExpenseChat,
953963
};
954964
}
955965

@@ -3417,7 +3427,6 @@ function isTodoSearch(hash: number, suggestedSearches: Record<string, SearchType
34173427
return !!matchedSearchKey && TODO_KEYS.includes(matchedSearchKey);
34183428
}
34193429

3420-
// eslint-disable-next-line @typescript-eslint/max-params
34213430
function createTypeMenuSections(
34223431
icons: Record<'Document' | 'Send' | 'ThumbsUp', IconAsset>,
34233432
currentUserEmail: string | undefined,
@@ -3429,13 +3438,11 @@ function createTypeMenuSections(
34293438
isOffline: boolean,
34303439
defaultExpensifyCard: CardFeedForDisplay | undefined,
34313440
shouldRedirectToExpensifyClassic: boolean,
3432-
draftTransactions: OnyxCollection<OnyxTypes.Transaction>,
3433-
reportCounts: {[CONST.SEARCH.SEARCH_KEYS.SUBMIT]: number; [CONST.SEARCH.SEARCH_KEYS.APPROVE]: number; [CONST.SEARCH.SEARCH_KEYS.PAY]: number; [CONST.SEARCH.SEARCH_KEYS.EXPORT]: number},
34343441
): SearchTypeMenuSection[] {
34353442
const typeMenuSections: SearchTypeMenuSection[] = [];
34363443

34373444
const suggestedSearches = getSuggestedSearches(currentUserAccountID, defaultCardFeed?.id, icons);
3438-
const suggestedSearchesVisibility = getSuggestedSearchesVisibility(currentUserEmail, cardFeedsByPolicy, policies, defaultExpensifyCard);
3445+
const {visibility: suggestedSearchesVisibility, hasGroupPoliciesWithExpenseChat} = getSuggestedSearchesVisibility(currentUserEmail, cardFeedsByPolicy, policies, defaultExpensifyCard);
34393446

34403447
// Todo section
34413448
{
@@ -3445,39 +3452,35 @@ function createTypeMenuSections(
34453452
};
34463453

34473454
if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.SUBMIT]) {
3448-
const groupPoliciesWithChatEnabled = getGroupPaidPoliciesWithExpenseChatEnabled(policies);
34493455
todoSection.menuItems.push({
34503456
...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SUBMIT],
3451-
badgeText: formatBadgeText(reportCounts[CONST.SEARCH.SEARCH_KEYS.SUBMIT]),
34523457
emptyState: {
34533458
title: 'search.searchResults.emptySubmitResults.title',
34543459
subtitle: 'search.searchResults.emptySubmitResults.subtitle',
3455-
buttons:
3456-
groupPoliciesWithChatEnabled.length > 0
3457-
? [
3458-
{
3459-
success: true,
3460-
buttonText: 'report.newReport.createExpense',
3461-
buttonAction: () => {
3462-
interceptAnonymousUser(() => {
3463-
if (shouldRedirectToExpensifyClassic) {
3464-
setIsOpenConfirmNavigateExpensifyClassicModalOpen(true);
3465-
return;
3466-
}
3467-
3468-
startMoneyRequest(CONST.IOU.TYPE.CREATE, generateReportID(), CONST.IOU.REQUEST_TYPE.SCAN, false, undefined, draftTransactions);
3469-
});
3470-
},
3460+
buttons: hasGroupPoliciesWithExpenseChat
3461+
? [
3462+
{
3463+
success: true,
3464+
buttonText: 'report.newReport.createExpense',
3465+
buttonAction: () => {
3466+
interceptAnonymousUser(() => {
3467+
if (shouldRedirectToExpensifyClassic) {
3468+
setIsOpenConfirmNavigateExpensifyClassicModalOpen(true);
3469+
return;
3470+
}
3471+
3472+
startMoneyRequest(CONST.IOU.TYPE.CREATE, generateReportID(), CONST.IOU.REQUEST_TYPE.SCAN);
3473+
});
34713474
},
3472-
]
3473-
: [],
3475+
},
3476+
]
3477+
: [],
34743478
},
34753479
});
34763480
}
34773481
if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.APPROVE]) {
34783482
todoSection.menuItems.push({
34793483
...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.APPROVE],
3480-
badgeText: formatBadgeText(reportCounts[CONST.SEARCH.SEARCH_KEYS.APPROVE]),
34813484
emptyState: {
34823485
title: 'search.searchResults.emptyApproveResults.title',
34833486
subtitle: 'search.searchResults.emptyApproveResults.subtitle',
@@ -3487,7 +3490,6 @@ function createTypeMenuSections(
34873490
if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.PAY]) {
34883491
todoSection.menuItems.push({
34893492
...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.PAY],
3490-
badgeText: formatBadgeText(reportCounts[CONST.SEARCH.SEARCH_KEYS.PAY]),
34913493
emptyState: {
34923494
title: 'search.searchResults.emptyPayResults.title',
34933495
subtitle: 'search.searchResults.emptyPayResults.subtitle',
@@ -3497,7 +3499,6 @@ function createTypeMenuSections(
34973499
if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.EXPORT]) {
34983500
todoSection.menuItems.push({
34993501
...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.EXPORT],
3500-
badgeText: formatBadgeText(reportCounts[CONST.SEARCH.SEARCH_KEYS.EXPORT]),
35013502
emptyState: {
35023503
title: 'search.searchResults.emptyExportResults.title',
35033504
subtitle: 'search.searchResults.emptyExportResults.subtitle',
@@ -4389,6 +4390,8 @@ export {
43894390
isTaskListItemType,
43904391
getActions,
43914392
createTypeMenuSections,
4393+
formatBadgeText,
4394+
getItemBadgeText,
43924395
createBaseSavedSearchMenuItem,
43934396
shouldShowEmptyState,
43944397
compareValues,

src/pages/Search/SearchTypeMenu.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ import Navigation from '@libs/Navigation/Navigation';
2828
import {getAllTaxRates} from '@libs/PolicyUtils';
2929
import {buildSearchQueryJSON, buildUserReadableQueryString, shouldSkipSuggestedSearchNavigation as shouldSkipSuggestedSearchNavigationForQuery} from '@libs/SearchQueryUtils';
3030
import type {SavedSearchMenuItem} from '@libs/SearchUIUtils';
31-
import {createBaseSavedSearchMenuItem, getOverflowMenu as getOverflowMenuUtil} from '@libs/SearchUIUtils';
31+
import {createBaseSavedSearchMenuItem, getItemBadgeText, getOverflowMenu as getOverflowMenuUtil} from '@libs/SearchUIUtils';
3232
import variables from '@styles/variables';
3333
import CONST from '@src/CONST';
3434
import ONYXKEYS from '@src/ONYXKEYS';
3535
import ROUTES from '@src/ROUTES';
36+
import todosReportCountsSelector from '@src/selectors/Todos';
3637
import type {SaveSearchItem} from '@src/types/onyx/SaveSearch';
3738
import SavedSearchItemThreeDotMenu from './SavedSearchItemThreeDotMenu';
3839
import SuggestedSearchSkeleton from './SuggestedSearchSkeleton';
@@ -84,6 +85,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
8485
const taxRates = getAllTaxRates(allPolicies);
8586
const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false});
8687
const {clearSelectedTransactions} = useSearchContext();
88+
const [reportCounts = CONST.EMPTY_TODOS_REPORT_COUNTS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {canBeMissing: true, selector: todosReportCountsSelector});
8789

8890
const flattenedMenuItems = useMemo(() => typeMenuSections.flatMap((section) => section.menuItems), [typeMenuSections]);
8991

@@ -289,7 +291,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
289291
iconWidth={variables.iconSizeNormal}
290292
iconHeight={variables.iconSizeNormal}
291293
wrapperStyle={styles.sectionMenuItem}
292-
badgeText={item.badgeText}
294+
badgeText={getItemBadgeText(item.key, reportCounts)}
293295
focused={focused}
294296
onPress={onPress}
295297
shouldIconUseAutoWidthStyle

0 commit comments

Comments
 (0)