From a20414b3ce2af8d738548ff7269995833cc95625 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 13 Jan 2026 17:48:38 -0800 Subject: [PATCH 01/17] Migrate first batch of components --- src/components/Form/FormWrapper.tsx | 131 +++++-------- src/components/InvertedFlatList/index.e2e.tsx | 32 ++-- src/components/Navigation/SearchSidebar.tsx | 10 +- src/components/ReportWelcomeText.tsx | 47 ++--- .../Search/TransactionGroupListExpanded.tsx | 55 ++---- .../Search/TransactionListItem.tsx | 104 +++-------- .../WideRHPContextProvider/index.tsx | 172 +++++++----------- .../WorkspaceMembersSelectionList.tsx | 92 ++++------ 8 files changed, 230 insertions(+), 413 deletions(-) diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index bc5ca52af418..178a8cddaf87 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo, useRef} from 'react'; +import React, {useRef} from 'react'; import type {RefObject} from 'react'; // eslint-disable-next-line no-restricted-imports import type {ScrollView as RNScrollView, StyleProp, ViewStyle} from 'react-native'; @@ -103,9 +103,9 @@ function FormWrapper({ const [formState] = useOnyx(`${formID}`, {canBeMissing: true}); - const errorMessage = useMemo(() => (formState ? getLatestErrorMessage(formState) : undefined), [formState]); + const errorMessage = formState ? getLatestErrorMessage(formState) : undefined; - const onFixTheErrorsLinkPressed = useCallback(() => { + const onFixTheErrorsLinkPressed = () => { const errorFields = !isEmptyObject(errors) ? errors : (formState?.errorFields ?? {}); const focusKey = Object.keys(inputRefs.current ?? {}).find((key) => key in errorFields); @@ -134,7 +134,7 @@ function FormWrapper({ // Focus the input after scrolling, as on the Web it gives a slightly better visual result focusInput?.focus?.(); - }, [errors, formState?.errorFields, inputRefs]); + }; // If either of `addBottomSafeAreaPadding` or `shouldSubmitButtonStickToBottom` is explicitly set, // we expect that the user wants to use the new edge-to-edge mode. @@ -159,88 +159,53 @@ function FormWrapper({ style: submitButtonStyles, }); - const SubmitButton = useMemo( - () => - isSubmitButtonVisible && ( - - ), - [ - disablePressOnEnter, - enterKeyEventListenerPriority, - enabledWhenOffline, - errorMessage, - errors, - footerContent, - formState?.errorFields, - formState?.isLoading, - isLoading, - isSubmitActionDangerous, - isSubmitButtonVisible, - isSubmitDisabled, - onFixTheErrorsLinkPressed, - onSubmit, - shouldHideFixErrorsAlert, - shouldSubmitButtonBlendOpacity, - shouldSubmitButtonStickToBottom, - style, - styles.flex1, - styles.mh0, - styles.mt5, - styles.stickToBottom, - submitButtonStylesWithBottomSafeAreaPadding, - submitButtonText, - submitFlexEnabled, - shouldRenderFooterAboveSubmit, - shouldPreventDefaultFocusOnPressSubmit, - ], + const SubmitButton = isSubmitButtonVisible && ( + ); - const scrollViewContent = useCallback( - () => ( - { - if (!shouldScrollToEnd) { - return; - } - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - requestAnimationFrame(() => { - formRef.current?.scrollToEnd({animated: true}); - }); + const scrollViewContent = () => ( + { + if (!shouldScrollToEnd) { + return; + } + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + requestAnimationFrame(() => { + formRef.current?.scrollToEnd({animated: true}); }); - }} - > - {children} - {!shouldSubmitButtonStickToBottom && SubmitButton} - - ), - [formID, style, styles.pb5, children, shouldSubmitButtonStickToBottom, SubmitButton, shouldScrollToEnd], + }); + }} + > + {children} + {!shouldSubmitButtonStickToBottom && SubmitButton} + ); if (!shouldUseScrollView) { diff --git a/src/components/InvertedFlatList/index.e2e.tsx b/src/components/InvertedFlatList/index.e2e.tsx index b0cb84050188..caa08ea9b023 100644 --- a/src/components/InvertedFlatList/index.e2e.tsx +++ b/src/components/InvertedFlatList/index.e2e.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import type {ScrollViewProps, ViewToken} from 'react-native'; import {DeviceEventEmitter, FlatList} from 'react-native'; import type {ReportAction} from '@src/types/onyx'; @@ -9,33 +9,25 @@ const AUTOSCROLL_TO_TOP_THRESHOLD = 128; function InvertedFlatListE2E({ref, ...props}: InvertedFlatListProps) { const {shouldEnableAutoScrollToTopThreshold, ...rest} = props; - const handleViewableItemsChanged = useMemo( - () => - ({viewableItems}: {viewableItems: ViewToken[]}) => { - DeviceEventEmitter.emit('onViewableItemsChanged', viewableItems); - }, - [], - ); - - const maintainVisibleContentPosition = useMemo(() => { - const config: ScrollViewProps['maintainVisibleContentPosition'] = { - // This needs to be 1 to avoid using loading views as anchors. - minIndexForVisible: rest.data?.length ? Math.min(1, rest.data.length - 1) : 0, - }; + const handleViewableItemsChanged = ({viewableItems}: {viewableItems: ViewToken[]}) => { + DeviceEventEmitter.emit('onViewableItemsChanged', viewableItems); + }; - if (shouldEnableAutoScrollToTopThreshold) { - config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; - } + const config: ScrollViewProps['maintainVisibleContentPosition'] = { + // This needs to be 1 to avoid using loading views as anchors. + minIndexForVisible: rest.data?.length ? Math.min(1, rest.data.length - 1) : 0, + }; - return config; - }, [shouldEnableAutoScrollToTopThreshold, rest.data?.length]); + if (shouldEnableAutoScrollToTopThreshold) { + config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; + } return ( // eslint-disable-next-line react/jsx-props-no-spreading {...rest} ref={ref} - maintainVisibleContentPosition={maintainVisibleContentPosition} + maintainVisibleContentPosition={config} inverted onViewableItemsChanged={handleViewableItemsChanged} /> diff --git a/src/components/Navigation/SearchSidebar.tsx b/src/components/Navigation/SearchSidebar.tsx index dffea363935b..4002fa480381 100644 --- a/src/components/Navigation/SearchSidebar.tsx +++ b/src/components/Navigation/SearchSidebar.tsx @@ -1,6 +1,6 @@ import type {ParamListBase} from '@react-navigation/native'; import {searchResultsSelector} from '@selectors/Snapshot'; -import React, {useEffect, useMemo} from 'react'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; import {useSearchContext} from '@components/Search/SearchContext'; import useLocalize from '@hooks/useLocalize'; @@ -33,13 +33,7 @@ function SearchSidebar({state}: SearchSidebarProps) { const params = route?.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT] | undefined; const {lastSearchType, setLastSearchType} = useSearchContext(); - const queryJSON = useMemo(() => { - if (!params?.q) { - return undefined; - } - - return buildSearchQueryJSON(params.q, params.rawQuery); - }, [params?.q, params?.rawQuery]); + const queryJSON = params?.q ? buildSearchQueryJSON(params.q, params.rawQuery) : undefined; const currentSearchResultsKey = queryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchResultsKey}`, { diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 707944fb0f77..332d6cf56b6a 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -1,5 +1,5 @@ import {createPersonalDetailsSelector} from '@selectors/PersonalDetails'; -import React, {useMemo} from 'react'; +import React from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import useEnvironment from '@hooks/useEnvironment'; @@ -90,37 +90,20 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK) || moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT); - const reportDetailsLink = useMemo(() => { - if (!report?.reportID) { - return ''; - } - - return `${environmentURL}/${ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, Navigation.getReportRHPActiveRoute())}`; - }, [environmentURL, report?.reportID]); - - const welcomeHeroText = useMemo(() => { - if (isInvoiceRoom) { - return translate('reportActionsView.sayHello'); - } - - if (isChatRoom) { - return translate('reportActionsView.welcomeToRoom', {roomName: reportName}); - } - - if (isSelfDM) { - return translate('reportActionsView.yourSpace'); - } - - if (isSystemChat) { - return reportName; - } - - if (isPolicyExpenseChat) { - return translate('reportActionsView.welcomeToRoom', {roomName: policyName}); - } - - return translate('reportActionsView.sayHello'); - }, [isChatRoom, isInvoiceRoom, isPolicyExpenseChat, isSelfDM, isSystemChat, translate, policyName, reportName]); + const reportDetailsLink = report?.reportID ? `${environmentURL}/${ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, Navigation.getReportRHPActiveRoute())}` : ''; + + let welcomeHeroText = translate('reportActionsView.sayHello'); + if (isInvoiceRoom) { + welcomeHeroText = translate('reportActionsView.sayHello'); + } else if (isChatRoom) { + welcomeHeroText = translate('reportActionsView.welcomeToRoom', {roomName: reportName}); + } else if (isSelfDM) { + welcomeHeroText = translate('reportActionsView.yourSpace'); + } else if (isSystemChat) { + welcomeHeroText = reportName; + } else if (isPolicyExpenseChat) { + welcomeHeroText = translate('reportActionsView.welcomeToRoom', {roomName: policyName}); + } // If we are the only participant (e.g. solo group chat) then keep the current user personal details so the welcome message does not show up empty. const shouldExcludeCurrentUser = participantAccountIDs.length > 0; diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx index 58fadb2469f4..57a304062ec3 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useContext, useMemo} from 'react'; +import React, {useContext} from 'react'; import {View} from 'react-native'; import ActivityIndicator from '@components/ActivityIndicator'; import Button from '@components/Button'; @@ -56,33 +56,22 @@ function TransactionGroupListExpanded({ const [isMobileSelectionModeEnabled] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE, {canBeMissing: true}); const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true, selector: columnsSelector}); - const transactionsSnapshotMetadata = useMemo(() => { - return transactionsSnapshot?.search; - }, [transactionsSnapshot?.search]); + const transactionsSnapshotMetadata = transactionsSnapshot?.search; - const visibleTransactions = useMemo(() => { - if (isExpenseReportType) { - return transactions.slice(0, transactionsVisibleLimit); - } - return transactions; - }, [transactions, transactionsVisibleLimit, isExpenseReportType]); + const visibleTransactions = isExpenseReportType ? transactions.slice(0, transactionsVisibleLimit) : transactions; - const isLastTransaction = useCallback( - (index: number) => { - return index === visibleTransactions.length - 1; - }, - [visibleTransactions.length], - ); + const isLastTransaction = (index: number) => { + return index === visibleTransactions.length - 1; + }; - const currentColumns = useMemo(() => { - if (isExpenseReportType) { - return columns ?? []; - } + let currentColumns = columns ?? []; + if (!isExpenseReportType) { if (!transactionsSnapshot?.data) { - return []; + currentColumns = []; + } else { + currentColumns = getColumnsToShow(accountID, transactionsSnapshot?.data, visibleColumns, false, transactionsSnapshot?.search.type); } - return getColumnsToShow(accountID, transactionsSnapshot?.data, visibleColumns, false, transactionsSnapshot?.search.type); - }, [accountID, columns, isExpenseReportType, transactionsSnapshot?.data, transactionsSnapshot?.search.type, visibleColumns]); + } // Currently only the transaction report groups have transactions where the empty view makes sense const shouldDisplayShowMoreButton = isExpenseReportType ? transactions.length > transactionsVisibleLimit : !!transactionsSnapshotMetadata?.hasMoreResults && !isOffline; @@ -91,16 +80,12 @@ function TransactionGroupListExpanded({ const shouldDisplayLoadingIndicator = !isExpenseReportType && !!transactionsSnapshotMetadata?.isLoading && shouldShowLoadingOnSearch; const {isLargeScreenWidth} = useResponsiveLayout(); - const {amountColumnSize, dateColumnSize, taxAmountColumnSize} = useMemo(() => { - const isAmountColumnWide = transactions.some((transaction) => transaction.isAmountColumnWide); - const isTaxAmountColumnWide = transactions.some((transaction) => transaction.isTaxAmountColumnWide); - const shouldShowYearForSomeTransaction = transactions.some((transaction) => transaction.shouldShowYear); - return { - amountColumnSize: isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - taxAmountColumnSize: isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - dateColumnSize: shouldShowYearForSomeTransaction ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - }; - }, [transactions]); + const isAmountColumnWide = transactions.some((transaction) => transaction.isAmountColumnWide); + const isTaxAmountColumnWide = transactions.some((transaction) => transaction.isTaxAmountColumnWide); + const shouldShowYearForSomeTransaction = transactions.some((transaction) => transaction.shouldShowYear); + const amountColumnSize = isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const taxAmountColumnSize = isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const dateColumnSize = shouldShowYearForSomeTransaction ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; const {markReportIDAsExpense} = useContext(WideRHPContext); const openReportInRHP = (transactionItem: TransactionListItemType) => { @@ -134,13 +119,13 @@ function TransactionGroupListExpanded({ }); }; - const onShowMoreButtonPress = useCallback(() => { + const onShowMoreButtonPress = () => { if (isExpenseReportType) { setTransactionsVisibleLimit((currentPageSize) => currentPageSize + CONST.TRANSACTION.RESULTS_PAGE_SIZE); } else if (!isOffline && transactionsQueryJSON) { searchTransactions(CONST.SEARCH.RESULTS_PAGE_SIZE); } - }, [isExpenseReportType, isOffline, transactionsQueryJSON, setTransactionsVisibleLimit, searchTransactions]); + }; if (shouldDisplayEmptyView) { return ( diff --git a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx index ef398b51994a..8899dda6b4c3 100644 --- a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useContext, useMemo, useRef} from 'react'; +import React, {useContext, useRef} from 'react'; import type {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; // Use the original useOnyx hook to get the real-time data from Onyx and not from the snapshot @@ -56,35 +56,30 @@ function TransactionListItem({ const {isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const {currentSearchHash, currentSearchKey} = useSearchContext(); const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true}); - const snapshotReport = useMemo(() => { - return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as Report; - }, [snapshot, transactionItem.reportID]); + const snapshotReport = (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as Report; const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionItem.reportID}`, {canBeMissing: true, selector: isActionLoadingSelector}); - const snapshotPolicy = useMemo(() => { - return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy; - }, [snapshot, transactionItem.policyID]); + const snapshotPolicy = (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy; const [lastPaymentMethod] = useOnyx(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {canBeMissing: true}); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true}); const [parentReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(transactionItem.reportID)}`, {canBeMissing: true}); const [transactionThreadReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionItem?.reportAction?.childReportID}`, {canBeMissing: true}); const [transaction] = originalUseOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionItem.transactionID)}`, {canBeMissing: true}); - const parentReportActionSelector = useCallback( - (reportActions: OnyxEntry): OnyxEntry => reportActions?.[`${transactionItem?.reportAction?.reportActionID}`], - [transactionItem?.reportAction?.reportActionID], - ); + const parentReportActionSelector = (reportActions: OnyxEntry): OnyxEntry => reportActions?.[`${transactionItem?.reportAction?.reportActionID}`]; const [parentReportAction] = originalUseOnyx( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(transactionItem.reportID)}`, {selector: parentReportActionSelector, canBeMissing: true}, [transactionItem], ); const currentUserDetails = useCurrentUserPersonalDetails(); - const transactionPreviewData: TransactionPreviewData = useMemo( - () => ({hasParentReport: !!parentReport, hasTransaction: !!transaction, hasParentReportAction: !!parentReportAction, hasTransactionThreadReport: !!transactionThreadReport}), - [parentReport, transaction, parentReportAction, transactionThreadReport], - ); + const transactionPreviewData: TransactionPreviewData = { + hasParentReport: !!parentReport, + hasTransaction: !!transaction, + hasParentReportAction: !!parentReportAction, + hasTransactionThreadReport: !!transactionThreadReport, + }; const pressableStyle = [ styles.transactionListItemStyle, @@ -100,37 +95,23 @@ function TransactionListItem({ backgroundColor: theme.highlightBG, }); - const {amountColumnSize, dateColumnSize, taxAmountColumnSize, submittedColumnSize, approvedColumnSize, postedColumnSize, exportedColumnSize} = useMemo(() => { - return { - amountColumnSize: transactionItem.isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - taxAmountColumnSize: transactionItem.isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - dateColumnSize: transactionItem.shouldShowYear ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - submittedColumnSize: transactionItem.shouldShowYearSubmitted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - approvedColumnSize: transactionItem.shouldShowYearApproved ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - postedColumnSize: transactionItem.shouldShowYearPosted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - exportedColumnSize: transactionItem.shouldShowYearExported ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, - }; - }, [ - transactionItem.isAmountColumnWide, - transactionItem.isTaxAmountColumnWide, - transactionItem.shouldShowYear, - transactionItem.shouldShowYearSubmitted, - transactionItem.shouldShowYearApproved, - transactionItem.shouldShowYearPosted, - transactionItem.shouldShowYearExported, - ]); - - const transactionViolations = useMemo(() => { - return (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter( - (violation: TransactionViolation) => - !isViolationDismissed(transactionItem, violation, currentUserDetails.email ?? '', currentUserDetails.accountID, snapshotReport, snapshotPolicy) && - shouldShowViolation(snapshotReport, snapshotPolicy, violation.name, currentUserDetails.email ?? '', false, transactionItem), - ); - }, [snapshotPolicy, snapshotReport, transactionItem, violations, currentUserDetails.email, currentUserDetails.accountID]); + const amountColumnSize = transactionItem.isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const taxAmountColumnSize = transactionItem.isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const dateColumnSize = transactionItem.shouldShowYear ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const submittedColumnSize = transactionItem.shouldShowYearSubmitted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const approvedColumnSize = transactionItem.shouldShowYearApproved ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const postedColumnSize = transactionItem.shouldShowYearPosted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + const exportedColumnSize = transactionItem.shouldShowYearExported ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL; + + const transactionViolations = (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter( + (violation: TransactionViolation) => + !isViolationDismissed(transactionItem, violation, currentUserDetails.email ?? '', currentUserDetails.accountID, snapshotReport, snapshotPolicy) && + shouldShowViolation(snapshotReport, snapshotPolicy, violation.name, currentUserDetails.email ?? '', false, transactionItem), + ); const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); - const handleActionButtonPress = useCallback(() => { + const handleActionButtonPress = () => { handleActionButtonPressUtil({ hash: currentSearchHash, item: transactionItem, @@ -145,34 +126,7 @@ function TransactionListItem({ onDelegateAccessRestricted: showDelegateNoAccessModal, personalPolicyID, }); - }, [ - currentSearchHash, - transactionItem, - transactionPreviewData, - snapshotReport, - snapshotPolicy, - lastPaymentMethod, - personalPolicyID, - currentSearchKey, - onSelectRow, - item, - onDEWModalOpen, - isDEWBetaEnabled, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - ]); - - const handleCheckboxPress = useCallback(() => { - onCheckboxPress?.(item); - }, [item, onCheckboxPress]); - - const onPress = useCallback(() => { - onSelectRow(item, transactionPreviewData); - }, [item, onSelectRow, transactionPreviewData]); - - const onLongPress = useCallback(() => { - onLongPressRow?.(item); - }, [item, onLongPressRow]); + }; const StyleUtils = useStyleUtils(); const pressableRef = useRef(null); @@ -183,8 +137,8 @@ function TransactionListItem({ onLongPressRow?.(item)} + onPress={() => onSelectRow(item, transactionPreviewData)} disabled={isDisabled && !item.isSelected} accessibilityLabel={item.text ?? ''} role={getButtonRole(true)} @@ -216,7 +170,7 @@ function TransactionListItem({ report={transactionItem.report} shouldShowTooltip={showTooltip} onButtonPress={handleActionButtonPress} - onCheckboxPress={handleCheckboxPress} + onCheckboxPress={() => onCheckboxPress?.(item)} shouldUseNarrowLayout={!isLargeScreenWidth} columns={columns} isActionLoading={isLoading ?? isActionLoading} @@ -231,7 +185,7 @@ function TransactionListItem({ shouldShowCheckbox={!!canSelectMultiple} style={[styles.p3, styles.pv2, shouldUseNarrowLayout ? styles.pt2 : {}]} violations={transactionViolations} - onArrowRightPress={onPress} + onArrowRightPress={() => onSelectRow(item, transactionPreviewData)} isHover={hovered} customCardNames={customCardNames} /> diff --git a/src/components/WideRHPContextProvider/index.tsx b/src/components/WideRHPContextProvider/index.tsx index 941c3d20b6df..4a01db142cc8 100644 --- a/src/components/WideRHPContextProvider/index.tsx +++ b/src/components/WideRHPContextProvider/index.tsx @@ -1,5 +1,5 @@ import {findFocusedRoute} from '@react-navigation/native'; -import React, {createContext, useCallback, useEffect, useMemo, useState} from 'react'; +import React, {createContext, useEffect, useState} from 'react'; // We use Animated for all functionality related to wide RHP to make it easier // to interact with react-navigation components (e.g., CardContainer, interpolator), which also use Animated. // eslint-disable-next-line no-restricted-imports @@ -122,13 +122,9 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) { }; }); - const isWideRHPFocused = useMemo(() => { - return !!focusedRoute?.key && allWideRHPRouteKeys.includes(focusedRoute.key); - }, [focusedRoute?.key, allWideRHPRouteKeys]); + const isWideRHPFocused = !!focusedRoute?.key && allWideRHPRouteKeys.includes(focusedRoute.key); - const isSuperWideRHPFocused = useMemo(() => { - return !!focusedRoute?.key && allSuperWideRHPRouteKeys.includes(focusedRoute.key); - }, [focusedRoute?.key, allSuperWideRHPRouteKeys]); + const isSuperWideRHPFocused = !!focusedRoute?.key && allSuperWideRHPRouteKeys.includes(focusedRoute.key); const isRHPFocused = focusedNavigator === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; @@ -136,18 +132,18 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) { const {isWideRHPBelow, isSuperWideRHPBelow} = getIsRHPDisplayedBelow(focusedRoute?.key, allSuperWideRHPRouteKeys, allWideRHPRouteKeys); // Updates the Wide RHP visible keys table from the all keys table - const syncRHPKeys = useCallback(() => { + const syncRHPKeys = () => { const {visibleSuperWideRHPRouteKeys, visibleWideRHPRouteKeys} = getVisibleRHPKeys(allSuperWideRHPRouteKeys, allWideRHPRouteKeys); setWideRHPRouteKeys(visibleWideRHPRouteKeys); setSuperWideRHPRouteKeys(visibleSuperWideRHPRouteKeys); setExpandedRHPProgress(visibleSuperWideRHPRouteKeys, visibleWideRHPRouteKeys); - }, [allSuperWideRHPRouteKeys, allWideRHPRouteKeys]); + }; - const clearWideRHPKeys = useCallback(() => { + const clearWideRHPKeys = () => { setWideRHPRouteKeys([]); setSuperWideRHPRouteKeys([]); expandedRHPProgress.setValue(0); - }, []); + }; // Once we have updated the array of all Super Wide RHP keys, we should sync it with the array of RHP keys visible on the screen useEffect(() => { @@ -180,109 +176,94 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) { /** * Removes a route from the super wide RHP route keys list, disabling wide RHP display for that route. */ - const removeSuperWideRHPRouteKey = useCallback((route: NavigationRoute) => removeWideRHPRoute(route, setAllSuperWideRHPRouteKeys), []); + const removeSuperWideRHPRouteKey = (route: NavigationRoute) => removeWideRHPRoute(route, setAllSuperWideRHPRouteKeys); /** * Removes a route from the wide RHP route keys list, disabling wide RHP display for that route. */ - const removeWideRHPRouteKey = useCallback((route: NavigationRoute) => removeWideRHPRoute(route, setAllWideRHPRouteKeys), []); + const removeWideRHPRouteKey = (route: NavigationRoute) => removeWideRHPRoute(route, setAllWideRHPRouteKeys); /** * Adds a route to the wide RHP route keys list, enabling wide RHP display for that route. */ - const showWideRHPVersion = useCallback( - (route: NavigationRoute) => { - removeSuperWideRHPRouteKey(route); - showWideRHPRoute(route, setAllWideRHPRouteKeys); - }, - [removeSuperWideRHPRouteKey], - ); + const showWideRHPVersion = (route: NavigationRoute) => { + removeSuperWideRHPRouteKey(route); + showWideRHPRoute(route, setAllWideRHPRouteKeys); + }; /** * Adds a route to the super wide RHP route keys list, enabling super wide RHP display for that route. */ - const showSuperWideRHPVersion = useCallback( - (route: NavigationRoute) => { - removeWideRHPRouteKey(route); - showWideRHPRoute(route, setAllSuperWideRHPRouteKeys); - }, - [removeWideRHPRouteKey], - ); + const showSuperWideRHPVersion = (route: NavigationRoute) => { + removeWideRHPRouteKey(route); + showWideRHPRoute(route, setAllSuperWideRHPRouteKeys); + }; /** * Marks a report ID as an expense report, adding it to the expense reports set. * This enables optimistic wide RHP display for expense reports. * It helps us open expense as wide, before it fully loads. */ - const markReportIDAsExpense = useCallback( - (reportID?: string) => { - if (!reportID) { - return; - } - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - const isInvoice = report?.type === CONST.REPORT.TYPE.INVOICE; - const isTask = report?.type === CONST.REPORT.TYPE.TASK; - if (isInvoice || isTask) { - return; - } - setExpenseReportIDs((prev) => { - const newSet = new Set(prev); - newSet.add(reportID); - return newSet; - }); - }, - [allReports], - ); + const markReportIDAsExpense = (reportID?: string) => { + if (!reportID) { + return; + } + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const isInvoice = report?.type === CONST.REPORT.TYPE.INVOICE; + const isTask = report?.type === CONST.REPORT.TYPE.TASK; + if (isInvoice || isTask) { + return; + } + setExpenseReportIDs((prev) => { + const newSet = new Set(prev); + newSet.add(reportID); + return newSet; + }); + }; /** * Checks if a report ID is marked as an expense report. * Used to determine if wide RHP should be displayed optimistically. * It helps us open expense as wide, before it fully loads. */ - const isReportIDMarkedAsExpense = useCallback( - (reportID: string) => { - return expenseReportIDs.has(reportID); - }, - [expenseReportIDs], - ); + const isReportIDMarkedAsExpense = (reportID: string) => { + return expenseReportIDs.has(reportID); + }; /** * Marks a report ID as a multi-transaction expense report, adding it to the expense reports set. * This enables optimistic super wide RHP display for expense reports. * It helps us open expense as super wide, before it fully loads. */ - const markReportIDAsMultiTransactionExpense = useCallback((reportID: string) => { + const markReportIDAsMultiTransactionExpense = (reportID: string) => { setMultiTransactionExpenseReportIDs((prev) => { const newSet = new Set(prev); newSet.add(reportID); return newSet; }); - }, []); + }; /** * Removes a report ID from the multi-transaction expense reports set. * This disables optimistic super wide RHP display for that specific report * (e.g., when transactions are deleted or report no longer qualifies as multi-transaction) */ - const unmarkReportIDAsMultiTransactionExpense = useCallback((reportID: string) => { + const unmarkReportIDAsMultiTransactionExpense = (reportID: string) => { setMultiTransactionExpenseReportIDs((prev) => { const newSet = new Set(prev); newSet.delete(reportID); return newSet; }); - }, []); + }; /** * Checks if a report ID is marked as a multi-transaction expense report. * Used to determine if super wide RHP should be displayed optimistically. * It helps us open expense as super wide, before it fully loads. */ - const isReportIDMarkedAsMultiTransactionExpense = useCallback( - (reportID: string) => { - return multiTransactionExpenseReportIDs.has(reportID); - }, - [multiTransactionExpenseReportIDs], - ); + const isReportIDMarkedAsMultiTransactionExpense = (reportID: string) => { + return multiTransactionExpenseReportIDs.has(reportID); + }; /** * Effect that handles responsive RHP width calculation when window dimensions change. @@ -311,51 +292,28 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) { return () => subscription?.remove(); }, []); - const value = useMemo( - () => ({ - expandedRHPProgress, - wideRHPRouteKeys, - superWideRHPRouteKeys, - showWideRHPVersion, - showSuperWideRHPVersion, - removeWideRHPRouteKey, - removeSuperWideRHPRouteKey, - shouldRenderSecondaryOverlayForRHPOnSuperWideRHP, - shouldRenderSecondaryOverlayForRHPOnWideRHP, - shouldRenderSecondaryOverlayForWideRHP, - shouldRenderTertiaryOverlay, - markReportIDAsExpense, - markReportIDAsMultiTransactionExpense, - unmarkReportIDAsMultiTransactionExpense, - isReportIDMarkedAsExpense, - isReportIDMarkedAsMultiTransactionExpense, - isWideRHPFocused, - isSuperWideRHPFocused, - syncRHPKeys, - clearWideRHPKeys, - }), - [ - wideRHPRouteKeys, - superWideRHPRouteKeys, - showWideRHPVersion, - showSuperWideRHPVersion, - removeWideRHPRouteKey, - removeSuperWideRHPRouteKey, - shouldRenderSecondaryOverlayForRHPOnSuperWideRHP, - shouldRenderSecondaryOverlayForRHPOnWideRHP, - shouldRenderSecondaryOverlayForWideRHP, - shouldRenderTertiaryOverlay, - markReportIDAsExpense, - markReportIDAsMultiTransactionExpense, - unmarkReportIDAsMultiTransactionExpense, - isReportIDMarkedAsExpense, - isReportIDMarkedAsMultiTransactionExpense, - isWideRHPFocused, - isSuperWideRHPFocused, - syncRHPKeys, - clearWideRHPKeys, - ], - ); + const value = { + expandedRHPProgress, + wideRHPRouteKeys, + superWideRHPRouteKeys, + showWideRHPVersion, + showSuperWideRHPVersion, + removeWideRHPRouteKey, + removeSuperWideRHPRouteKey, + shouldRenderSecondaryOverlayForRHPOnSuperWideRHP, + shouldRenderSecondaryOverlayForRHPOnWideRHP, + shouldRenderSecondaryOverlayForWideRHP, + shouldRenderTertiaryOverlay, + markReportIDAsExpense, + markReportIDAsMultiTransactionExpense, + unmarkReportIDAsMultiTransactionExpense, + isReportIDMarkedAsExpense, + isReportIDMarkedAsMultiTransactionExpense, + isWideRHPFocused, + isSuperWideRHPFocused, + syncRHPKeys, + clearWideRHPKeys, + }; return {children}; } diff --git a/src/components/WorkspaceMembersSelectionList.tsx b/src/components/WorkspaceMembersSelectionList.tsx index 3bf855ae4d1f..ec60837ad5a9 100644 --- a/src/components/WorkspaceMembersSelectionList.tsx +++ b/src/components/WorkspaceMembersSelectionList.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import useDebouncedState from '@hooks/useDebouncedState'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -43,67 +43,53 @@ function WorkspaceMembersSelectionList({policyID, selectedApprover, setApprover} const policy = usePolicy(policyID); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); - const orderedApprovers = useMemo(() => { - const approvers: SelectionListApprover[] = []; - - if (policy?.employeeList) { - const availableApprovers = Object.values(policy.employeeList) - .map((employee): SelectionListApprover | null => { - const email = employee.email; - - if (!email || employee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - return null; - } - - const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList); - const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); - const {avatar, displayName = email, login} = personalDetails?.[accountID] ?? {}; - - return { - text: displayName, - alternateText: email, - keyForList: email, - isSelected: selectedApprover === email, - login: email, - icons: [{source: avatar ?? icons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}], - rightElement: ( - - ), - }; - }) - .filter((approver): approver is SelectionListApprover => !!approver); - - approvers.push(...availableApprovers); + const approvers: SelectionListApprover[] = []; + + if (policy?.employeeList) { + for (const employee of Object.values(policy.employeeList)) { + const email = employee.email; + + if (!email || employee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + continue; + } + + const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList); + const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); + const {avatar, displayName = email, login} = personalDetails?.[accountID] ?? {}; + + approvers.push({ + text: displayName, + alternateText: email, + keyForList: email, + isSelected: selectedApprover === email, + login: email, + icons: [{source: avatar ?? icons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}], + rightElement: ( + + ), + }); } + } - const filteredApprovers = tokenizedSearch(approvers, getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode), (approver) => [approver.text ?? '', approver.login ?? '']); - - return sortAlphabetically(filteredApprovers, 'text', localeCompare); - }, [policy?.employeeList, policy?.owner, debouncedSearchTerm, countryCode, localeCompare, personalDetails, selectedApprover, icons.FallbackAvatar]); + const filteredApprovers = tokenizedSearch(approvers, getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode), (approver) => [approver.text ?? '', approver.login ?? '']); + const orderedApprovers = sortAlphabetically(filteredApprovers, 'text', localeCompare); - const handleOnSelectRow = (approver: SelectionListApprover) => { - setApprover(approver.login); + const textInputOptions = { + label: translate('selectionList.nameEmailOrPhoneNumber'), + value: searchTerm, + headerMessage: searchTerm && !orderedApprovers.length ? translate('common.noResultsFound') : '', + onChangeText: setSearchTerm, }; - const textInputOptions = useMemo( - () => ({ - label: translate('selectionList.nameEmailOrPhoneNumber'), - value: searchTerm, - headerMessage: searchTerm && !orderedApprovers.length ? translate('common.noResultsFound') : '', - onChangeText: setSearchTerm, - }), - [searchTerm, orderedApprovers.length, setSearchTerm, translate], - ); - return ( setApprover(approver.login)} textInputOptions={textInputOptions} showLoadingPlaceholder={!didScreenTransitionEnd} shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} From 2f3e8cf84f411e0818e92bed55fc8ddc9706e13f Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 13 Jan 2026 17:48:53 -0800 Subject: [PATCH 02/17] Migrate first batch of hooks --- src/hooks/useBulkPayOptions.ts | 92 ++++-------- src/hooks/useCardFeedsForDisplay.ts | 41 ++--- src/hooks/useIsPaidPolicyAdmin.ts | 10 +- src/hooks/useSearchHighlightAndScroll.ts | 108 ++++++-------- src/hooks/useSelectedTransactionsActions.ts | 141 ++++++------------ .../useTransactionsAndViolationsForReport.ts | 26 +--- 6 files changed, 153 insertions(+), 265 deletions(-) diff --git a/src/hooks/useBulkPayOptions.ts b/src/hooks/useBulkPayOptions.ts index 8beeb9099f2f..63f837d7fd82 100644 --- a/src/hooks/useBulkPayOptions.ts +++ b/src/hooks/useBulkPayOptions.ts @@ -1,5 +1,4 @@ import truncate from 'lodash/truncate'; -import {useCallback, useMemo} from 'react'; import type {TupleToUnion} from 'type-fest'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import type {BankAccountMenuItem} from '@components/Search/types'; @@ -99,48 +98,41 @@ function useBulkPayOptions({ }); } - const getPaymentSubitems = useCallback( - (payAsBusiness: boolean) => { - const requiredAccountType = payAsBusiness ? CONST.BANK_ACCOUNT.TYPE.BUSINESS : CONST.BANK_ACCOUNT.TYPE.PERSONAL; - - return formattedPaymentMethods - .filter((method) => { - const accountData = method?.accountData as AccountData; - return accountData?.type === requiredAccountType; - }) - .map((formattedPaymentMethod) => ({ - text: formattedPaymentMethod?.title ?? '', - description: formattedPaymentMethod?.description ?? '', - icon: formattedPaymentMethod?.icon, - shouldUpdateSelectedIndex: true, - iconStyles: formattedPaymentMethod?.iconStyles, - iconHeight: formattedPaymentMethod?.iconSize, - iconWidth: formattedPaymentMethod?.iconSize, - key: CONST.IOU.PAYMENT_TYPE.EXPENSIFY, - additionalData: { - payAsBusiness, - methodID: formattedPaymentMethod.methodID, - paymentMethod: formattedPaymentMethod.accountType, - }, - })); - }, - [formattedPaymentMethods], - ); + const getPaymentSubitems = (payAsBusiness: boolean) => { + const requiredAccountType = payAsBusiness ? CONST.BANK_ACCOUNT.TYPE.BUSINESS : CONST.BANK_ACCOUNT.TYPE.PERSONAL; + return formattedPaymentMethods + .filter((method) => { + const accountData = method?.accountData as AccountData; + return accountData?.type === requiredAccountType; + }) + .map((formattedPaymentMethod) => ({ + text: formattedPaymentMethod?.title ?? '', + description: formattedPaymentMethod?.description ?? '', + icon: formattedPaymentMethod?.icon, + shouldUpdateSelectedIndex: true, + iconStyles: formattedPaymentMethod?.iconStyles, + iconHeight: formattedPaymentMethod?.iconSize, + iconWidth: formattedPaymentMethod?.iconSize, + key: CONST.IOU.PAYMENT_TYPE.EXPENSIFY, + additionalData: { + payAsBusiness, + methodID: formattedPaymentMethod.methodID, + paymentMethod: formattedPaymentMethod.accountType, + }, + })); + }; const latestBankItems = getLatestBankAccountItem(); const personalBankAccountList = formattedPaymentMethods.filter((ba) => (ba.accountData as AccountData)?.type === CONST.BANK_ACCOUNT.TYPE.PERSONAL); - const bulkPayButtonOptions = useMemo(() => { + let bulkPayButtonOptions; + if (!selectedReportID || !selectedPolicyID) { + bulkPayButtonOptions = undefined; + } else if (onlyShowPayElsewhere) { + bulkPayButtonOptions = [paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]]; + } else { const buttonOptions = []; - if (!selectedReportID || !selectedPolicyID) { - return undefined; - } - - if (onlyShowPayElsewhere) { - return [paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]]; - } - if (shouldShowBusinessBankAccountOptions) { buttonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]); } @@ -221,30 +213,8 @@ function useBulkPayOptions({ } } - return buttonOptions; - }, [ - translate, - icons.Building, - icons.User, - selectedReportID, - selectedPolicyID, - shouldShowBusinessBankAccountOptions, - canUseWallet, - hasMultiplePolicies, - hasSinglePolicy, - isPersonalOnlyOption, - shouldShowPayElsewhereOption, - isInvoiceReport, - paymentMethods, - personalBankAccountList.length, - canUsePersonalBankAccount, - activeAdminPolicies, - currency, - chatReport, - getPaymentSubitems, - formattedAmount, - onlyShowPayElsewhere, - ]); + bulkPayButtonOptions = buttonOptions; + } return { bulkPayButtonOptions, diff --git a/src/hooks/useCardFeedsForDisplay.ts b/src/hooks/useCardFeedsForDisplay.ts index ec83214640f2..0248d41d9f6f 100644 --- a/src/hooks/useCardFeedsForDisplay.ts +++ b/src/hooks/useCardFeedsForDisplay.ts @@ -1,4 +1,3 @@ -import {useMemo} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import {getCardFeedsForDisplay, getCardFeedsForDisplayPerPolicy} from '@libs/CardFeedUtils'; import {filterPersonalCards, isCustomFeed, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; @@ -27,40 +26,42 @@ const useCardFeedsForDisplay = () => { canBeMissing: true, }); - const cardFeedsByPolicy = useMemo(() => getCardFeedsForDisplayPerPolicy(allFeeds), [allFeeds]); - - const defaultCardFeed = useMemo(() => { - if (!eligiblePoliciesIDs) { - return undefined; - } + const cardFeedsByPolicy = getCardFeedsForDisplayPerPolicy(allFeeds); + let defaultCardFeed; + if (eligiblePoliciesIDs) { // Prioritize the active policy if eligible if (activePolicyID && eligiblePoliciesIDs.has(activePolicyID)) { const policyCardFeeds = cardFeedsByPolicy[activePolicyID]; if (policyCardFeeds?.length) { - return policyCardFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); + defaultCardFeed = policyCardFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); } } - // If the active policy doesn't have card feeds, use the first eligible policy that does - for (const eligiblePolicyID of eligiblePoliciesIDs) { - const policyCardFeeds = cardFeedsByPolicy[eligiblePolicyID]; - if (policyCardFeeds?.length) { - return policyCardFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); + if (!defaultCardFeed) { + // If the active policy doesn't have card feeds, use the first eligible policy that does + for (const eligiblePolicyID of eligiblePoliciesIDs) { + const policyCardFeeds = cardFeedsByPolicy[eligiblePolicyID]; + if (policyCardFeeds?.length) { + defaultCardFeed = policyCardFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); + break; + } } } - // Commercial feeds don't have preferred policies, so we need to include these in the list - const commercialFeeds = Object.values(cardFeedsByPolicy) - .flat() - .filter((feed) => !isCustomFeed(feed.name as CompanyCardFeed)); + if (!defaultCardFeed) { + // Commercial feeds don't have preferred policies, so we need to include these in the list + const commercialFeeds = Object.values(cardFeedsByPolicy) + .flat() + .filter((feed) => !isCustomFeed(feed.name as CompanyCardFeed)); - return commercialFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); - }, [eligiblePoliciesIDs, activePolicyID, cardFeedsByPolicy, localeCompare]); + defaultCardFeed = commercialFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); + } + } const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); - const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]); + const allCards = mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList); const expensifyCards = getCardFeedsForDisplay({}, allCards); const defaultExpensifyCard = Object.values(expensifyCards)?.at(0); diff --git a/src/hooks/useIsPaidPolicyAdmin.ts b/src/hooks/useIsPaidPolicyAdmin.ts index fcdfdc8e11dc..dcbef1526eaf 100644 --- a/src/hooks/useIsPaidPolicyAdmin.ts +++ b/src/hooks/useIsPaidPolicyAdmin.ts @@ -1,4 +1,3 @@ -import {useCallback} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -12,12 +11,9 @@ import useOnyx from './useOnyx'; function useIsPaidPolicyAdmin() { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const isUserPaidPolicyAdminSelector = useCallback( - (policies: OnyxCollection) => { - return Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, currentUserPersonalDetails.login)); - }, - [currentUserPersonalDetails?.login], - ); + const isUserPaidPolicyAdminSelector = (policies: OnyxCollection) => { + return Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, currentUserPersonalDetails.login)); + }; const [isCurrentUserPolicyAdmin = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { canBeMissing: true, diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index bb2826105dc4..ee27a56232e6 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -1,5 +1,5 @@ import {useIsFocused} from '@react-navigation/native'; -import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {SearchQueryJSON} from '@components/Search/types'; @@ -52,31 +52,22 @@ function useSearchHighlightAndScroll({ const hasPendingSearchRef = useRef(false); const isChat = queryJSON.type === CONST.SEARCH.DATA_TYPES.CHAT; - const existingSearchResultIDs = useMemo(() => { - if (!searchResults?.data) { - return []; - } - return isChat ? extractReportActionIDsFromSearchResults(searchResults.data) : extractTransactionIDsFromSearchResults(searchResults.data); - }, [searchResults?.data, isChat]); - - const newTransactions = useMemo(() => { - const previousTransactionsIDs = Object.keys(previousTransactions ?? {}); - - if (previousTransactionsIDs.length === 0) { - return []; - } + const existingSearchResultIDs = searchResults?.data + ? isChat + ? extractReportActionIDsFromSearchResults(searchResults.data) + : extractTransactionIDsFromSearchResults(searchResults.data) + : []; + const previousTransactionsIDs = Object.keys(previousTransactions ?? {}); + const newTransactions: Transaction[] = []; + if (previousTransactionsIDs.length > 0) { const previousIDs = new Set(previousTransactionsIDs); - const result: Transaction[] = []; - for (const [id, transaction] of Object.entries(transactions ?? {})) { if (!previousIDs.has(id) && transaction) { - result.push(transaction); + newTransactions.push(transaction); } } - - return result; - }, [previousTransactions, transactions]); + } // Trigger search when a new report action is added while on chat or when a new transaction is added for the other search types. useEffect(() => { @@ -235,51 +226,48 @@ function useSearchHighlightAndScroll({ /** * Callback to handle scrolling to the new search result. */ - const handleSelectionListScroll = useCallback( - (data: SearchListItem[], ref: SelectionListHandle | null) => { - // Early return if there's no ref, new transaction wasn't brought in by this hook - // or there's no new search result key - const newSearchResultKey = newSearchResultKeys?.values().next().value; - if (!ref || !triggeredByHookRef.current || !newSearchResultKey) { - return; - } + const handleSelectionListScroll = (data: SearchListItem[], ref: SelectionListHandle | null) => { + // Early return if there's no ref, new transaction wasn't brought in by this hook + // or there's no new search result key + const newSearchResultKey = newSearchResultKeys?.values().next().value; + if (!ref || !triggeredByHookRef.current || !newSearchResultKey) { + return; + } - // Extract the transaction/report action ID from the newSearchResultKey - const newID = newSearchResultKey.replace(isChat ? ONYXKEYS.COLLECTION.REPORT_ACTIONS : ONYXKEYS.COLLECTION.TRANSACTION, ''); - - // Find the index of the new transaction/report action in the data array - const indexOfNewItem = data.findIndex((item) => { - if (isChat) { - if ('reportActionID' in item && item.reportActionID === newID) { - return true; - } - } else { - // Handle TransactionListItemType - if ('transactionID' in item && item.transactionID === newID) { - return true; - } - - // Handle TransactionGroupListItemType with transactions array - if ('transactions' in item && Array.isArray(item.transactions)) { - return item.transactions.some((transaction) => transaction?.transactionID === newID); - } - } + // Extract the transaction/report action ID from the newSearchResultKey + const newID = newSearchResultKey.replace(isChat ? ONYXKEYS.COLLECTION.REPORT_ACTIONS : ONYXKEYS.COLLECTION.TRANSACTION, ''); - return false; - }); + // Find the index of the new transaction/report action in the data array + const indexOfNewItem = data.findIndex((item) => { + if (isChat) { + if ('reportActionID' in item && item.reportActionID === newID) { + return true; + } + } else { + // Handle TransactionListItemType + if ('transactionID' in item && item.transactionID === newID) { + return true; + } - // Early return if the new item is not found in the data array - if (indexOfNewItem <= 0) { - return; + // Handle TransactionGroupListItemType with transactions array + if ('transactions' in item && Array.isArray(item.transactions)) { + return item.transactions.some((transaction) => transaction?.transactionID === newID); + } } - // Perform the scrolling action - ref.scrollToIndex(indexOfNewItem); - // Reset the trigger flag to prevent unintended future scrolls and highlights - triggeredByHookRef.current = false; - }, - [newSearchResultKeys, isChat], - ); + return false; + }); + + // Early return if the new item is not found in the data array + if (indexOfNewItem <= 0) { + return; + } + + // Perform the scrolling action + ref.scrollToIndex(indexOfNewItem); + // Reset the trigger flag to prevent unintended future scrolls and highlights + triggeredByHookRef.current = false; + }; return {newSearchResultKeys, handleSelectionListScroll, newTransactions}; } diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 0291873e3e24..7d87ddb12c0c 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -1,4 +1,4 @@ -import {useCallback, useMemo, useState} from 'react'; +import {useState} from 'react'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import {useSearchContext} from '@components/Search/SearchContext'; import {initSplitExpense, unholdRequest} from '@libs/actions/IOU'; @@ -79,59 +79,44 @@ function useSelectedTransactionsActions({ const isReportArchived = useReportIsArchived(report?.reportID); const {deleteTransactions} = useDeleteTransactions({report, reportActions, policy}); const {login} = useCurrentUserPersonalDetails(); - const selectedTransactionsList = useMemo( - () => - selectedTransactionIDs.reduce((acc, transactionID) => { - const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (transaction) { - acc.push(transaction); - } - return acc; - }, [] as Transaction[]), - [allTransactions, selectedTransactionIDs], - ); - const hasTransactionsFromMultipleOwners = useMemo(() => { - const knownOwnerIDs = new Set(); - let hasUnknownOwner = false; - - for (const selectedTransactionInfo of Object.values(selectedTransactionsMeta ?? {})) { - const ownerAccountID = selectedTransactionInfo?.ownerAccountID; - if (typeof ownerAccountID === 'number') { - knownOwnerIDs.add(ownerAccountID); - if (knownOwnerIDs.size > 1) { - return true; - } - } else { - hasUnknownOwner = true; - } + const selectedTransactionsList = selectedTransactionIDs.reduce((acc, transactionID) => { + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + if (transaction) { + acc.push(transaction); } + return acc; + }, [] as Transaction[]); + + const knownOwnerIDs = new Set(); + let hasUnknownOwner = false; + + for (const selectedTransactionInfo of Object.values(selectedTransactionsMeta ?? {})) { + const ownerAccountID = selectedTransactionInfo?.ownerAccountID; + if (typeof ownerAccountID === 'number') { + knownOwnerIDs.add(ownerAccountID); + } else { + hasUnknownOwner = true; + } + } - for (const selectedTransaction of selectedTransactionsList) { - const reportID = selectedTransaction?.reportID; - if (!reportID || reportID === CONST.REPORT.UNREPORTED_REPORT_ID) { - hasUnknownOwner = true; - continue; - } - - const parentReport = getReportOrDraftReport(reportID); - const ownerAccountID = parentReport?.ownerAccountID; - - if (typeof ownerAccountID === 'number') { - knownOwnerIDs.add(ownerAccountID); - if (knownOwnerIDs.size > 1) { - return true; - } - } else { - hasUnknownOwner = true; - } + for (const selectedTransaction of selectedTransactionsList) { + const reportID = selectedTransaction?.reportID; + if (!reportID || reportID === CONST.REPORT.UNREPORTED_REPORT_ID) { + hasUnknownOwner = true; + continue; } - if (hasUnknownOwner) { - return knownOwnerIDs.size > 0 || selectedTransactionIDs.length > 1; + const parentReport = getReportOrDraftReport(reportID); + const ownerAccountID = parentReport?.ownerAccountID; + + if (typeof ownerAccountID === 'number') { + knownOwnerIDs.add(ownerAccountID); + } else { + hasUnknownOwner = true; } + } - return false; - }, [selectedTransactionsList, selectedTransactionsMeta, selectedTransactionIDs.length]); + const hasTransactionsFromMultipleOwners = hasUnknownOwner ? knownOwnerIDs.size > 0 || selectedTransactionIDs.length > 1 : knownOwnerIDs.size > 1; const {translate, localeCompare} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); @@ -149,25 +134,23 @@ function useSelectedTransactionsActions({ iouType = CONST.IOU.TYPE.INVOICE; } - const handleDeleteTransactions = useCallback(() => { + const handleDeleteTransactions = () => { const deletedThreadReportIDs = deleteTransactions(selectedTransactionIDs, duplicateTransactions, duplicateTransactionViolations, currentSearchHash, false); clearSelectedTransactions(true); setIsDeleteModalVisible(false); Navigation.removeReportScreen(new Set(deletedThreadReportIDs)); - }, [deleteTransactions, selectedTransactionIDs, duplicateTransactions, duplicateTransactionViolations, currentSearchHash, clearSelectedTransactions]); + }; - const showDeleteModal = useCallback(() => { + const showDeleteModal = () => { setIsDeleteModalVisible(true); - }, []); + }; - const hideDeleteModal = useCallback(() => { + const hideDeleteModal = () => { setIsDeleteModalVisible(false); - }, []); + }; - const computedOptions = useMemo(() => { - if (!selectedTransactionIDs.length) { - return []; - } + let computedOptions = []; + if (selectedTransactionIDs.length) { const options = []; const isMoneyRequestReport = isMoneyRequestReportUtils(report); const isReportReimbursed = report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; @@ -364,46 +347,8 @@ function useSelectedTransactionsActions({ onSelected: showDeleteModal, }); } - return options; - }, [ - session?.email, - selectedTransactionIDs, - report, - selectedTransactionsList, - translate, - isReportArchived, - hasTransactionsFromMultipleOwners, - policy, - reportActions, - clearSelectedTransactions, - allTransactionsLength, - integrationsExportTemplates, - csvExportLayouts, - isOffline, - onExportOffline, - onExportFailed, - beginExportWithTemplate, - outstandingReportsByPolicyID, - iouType, - lastVisitedPath, - allTransactions, - allReports, - session?.accountID, - showDeleteModal, - allTransactionViolations, - expensifyIcons.Stopwatch, - expensifyIcons.ThumbsDown, - expensifyIcons.Table, - expensifyIcons.Export, - expensifyIcons.ArrowRight, - expensifyIcons.ArrowSplit, - expensifyIcons.DocumentMerge, - expensifyIcons.ArrowCollapse, - expensifyIcons.Trashcan, - localeCompare, - isOnSearch, - login, - ]); + computedOptions = options; + } return { options: computedOptions, diff --git a/src/hooks/useTransactionsAndViolationsForReport.ts b/src/hooks/useTransactionsAndViolationsForReport.ts index d92ec801bdd4..512030a1f3ed 100644 --- a/src/hooks/useTransactionsAndViolationsForReport.ts +++ b/src/hooks/useTransactionsAndViolationsForReport.ts @@ -1,4 +1,3 @@ -import {useMemo} from 'react'; import {useAllReportsTransactionsAndViolations} from '@components/OnyxListItemProvider'; import {getTransactionViolations} from '@libs/TransactionUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -17,25 +16,14 @@ function useTransactionsAndViolationsForReport(reportID?: string) { const {transactions, violations} = reportID ? (allReportsTransactionsAndViolations?.[reportID] ?? DEFAULT_RETURN_VALUE) : DEFAULT_RETURN_VALUE; - const transactionsAndViolations = useMemo(() => { - const filteredViolations = Object.keys(violations).reduce( - (filteredTransactionViolations, transactionViolationKey) => { - const transactionID = transactionViolationKey.split('_').at(1) ?? ''; - const transaction = transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const filteredViolations: Record = {}; + for (const transactionViolationKey of Object.keys(violations)) { + const transactionID = transactionViolationKey.split('_').at(1) ?? ''; + const transaction = transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + filteredViolations[transactionViolationKey] = getTransactionViolations(transaction, violations, currentUserDetails.email ?? '', currentUserDetails.accountID, report, policy) ?? []; + } - // This is our accumulator, it's okay to reassign - // eslint-disable-next-line no-param-reassign - filteredTransactionViolations[transactionViolationKey] = - getTransactionViolations(transaction, violations, currentUserDetails.email ?? '', currentUserDetails.accountID, report, policy) ?? []; - return filteredTransactionViolations; - }, - {} as Record, - ); - - return {transactions, violations: filteredViolations}; - }, [transactions, violations, currentUserDetails?.email, currentUserDetails?.accountID, report, policy]); - - return transactionsAndViolations; + return {transactions, violations: filteredViolations}; } export default useTransactionsAndViolationsForReport; From e5e324384456e0142486c820e4577216f7c4086c Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 13 Jan 2026 17:49:09 -0800 Subject: [PATCH 03/17] Migrate AddUnreportedExpense --- src/pages/AddUnreportedExpense.tsx | 182 ++++++++++++----------------- 1 file changed, 73 insertions(+), 109 deletions(-) diff --git a/src/pages/AddUnreportedExpense.tsx b/src/pages/AddUnreportedExpense.tsx index 0e95b2130b96..752ed5450902 100644 --- a/src/pages/AddUnreportedExpense.tsx +++ b/src/pages/AddUnreportedExpense.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import Button from '@components/Button'; @@ -65,35 +65,32 @@ function AddUnreportedExpense({route}: AddUnreportedExpensePageType) { const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const shouldShowUnreportedTransactionsSkeletons = isLoadingUnreportedTransactions && hasMoreUnreportedTransactionsResults && !isOffline; - const getUnreportedTransactions = useCallback( - (transactions: OnyxCollection) => { - if (!transactions) { - return []; + const getUnreportedTransactions = (transactions: OnyxCollection) => { + if (!transactions) { + return []; + } + return Object.values(transactions || {}).filter((item) => { + const isUnreported = item?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID || item?.reportID === ''; + if (!isUnreported) { + return false; } - return Object.values(transactions || {}).filter((item) => { - const isUnreported = item?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID || item?.reportID === ''; - if (!isUnreported) { - return false; - } - // Negative values are not allowed for unreported expenses - if ((getTransactionDetails(item)?.amount ?? 0) < 0) { - return false; - } + // Negative values are not allowed for unreported expenses + if ((getTransactionDetails(item)?.amount ?? 0) < 0) { + return false; + } - if (isPerDiemRequest(item)) { - // Only show per diem expenses if the target workspace has per diem enabled and the per diem expense was created in the same workspace - const workspacePerDiemUnit = getPerDiemCustomUnit(policy); - const perDiemCustomUnitID = item?.comment?.customUnit?.customUnitID; + if (isPerDiemRequest(item)) { + // Only show per diem expenses if the target workspace has per diem enabled and the per diem expense was created in the same workspace + const workspacePerDiemUnit = getPerDiemCustomUnit(policy); + const perDiemCustomUnitID = item?.comment?.customUnit?.customUnitID; - return canSubmitPerDiemExpenseFromWorkspace(policy) && (!perDiemCustomUnitID || perDiemCustomUnitID === workspacePerDiemUnit?.customUnitID); - } + return canSubmitPerDiemExpenseFromWorkspace(policy) && (!perDiemCustomUnitID || perDiemCustomUnitID === workspacePerDiemUnit?.customUnitID); + } - return true; - }); - }, - [policy], - ); + return true; + }); + }; const [transactions = getEmptyArray()] = useOnyx( ONYXKEYS.COLLECTION.TRANSACTION, @@ -101,7 +98,7 @@ function AddUnreportedExpense({route}: AddUnreportedExpensePageType) { selector: getUnreportedTransactions, canBeMissing: true, }, - [getUnreportedTransactions], + [policy], ); const fetchMoreUnreportedTransactions = () => { @@ -119,16 +116,11 @@ function AddUnreportedExpense({route}: AddUnreportedExpensePageType) { const styles = useThemeStyles(); const selectionListRef = useRef>(null); - const shouldShowTextInput = useMemo(() => { - return transactions.length >= CONST.SEARCH_ITEM_LIMIT; - }, [transactions.length]); + const shouldShowTextInput = transactions.length >= CONST.SEARCH_ITEM_LIMIT; - const filteredTransactions = useMemo(() => { - if (!debouncedSearchValue.trim() || !shouldShowTextInput) { - return transactions; - } - - return tokenizedSearch(transactions, debouncedSearchValue, (transaction) => { + let filteredTransactions = transactions; + if (debouncedSearchValue.trim() && shouldShowTextInput) { + filteredTransactions = tokenizedSearch(transactions, debouncedSearchValue, (transaction) => { const searchableFields: string[] = []; const merchant = getMerchant(transaction); @@ -152,16 +144,14 @@ function AddUnreportedExpense({route}: AddUnreportedExpensePageType) { return searchableFields; }); - }, [debouncedSearchValue, shouldShowTextInput, transactions]); + } - const unreportedExpenses = useMemo(() => { - return createUnreportedExpenses(filteredTransactions).map((item) => ({ - ...item, - isSelected: selectedIds.has(item.transactionID), - })); - }, [filteredTransactions, selectedIds]); + const unreportedExpenses = createUnreportedExpenses(filteredTransactions).map((item) => ({ + ...item, + isSelected: selectedIds.has(item.transactionID), + })); - const handleConfirm = useCallback(() => { + const handleConfirm = () => { if (selectedIds.size === 0) { setErrorMessage(translate('iou.selectUnreportedExpense')); return; @@ -194,78 +184,52 @@ function AddUnreportedExpense({route}: AddUnreportedExpensePageType) { } }); setErrorMessage(''); - }, [ - selectedIds, - translate, - report, - isASAPSubmitBetaEnabled, - session?.accountID, - session?.email, - transactionViolations, - reportToConfirm, - policy, - reportNextStep, - policyCategories, - policyRecentlyUsedCurrencies, - ]); + }; - const footerContent = useMemo(() => { - return ( - <> - {!!errorMessage && ( - - )} -