From b44ed9e06f4f2ff5557f6cbecefee5a0aa54da09 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 7 Aug 2025 21:01:19 +0700 Subject: [PATCH 1/2] using OnyxListItemProvider --- .../LHNOptionsList/OptionRowLHNData.tsx | 10 +++------- src/components/OnyxListItemProvider.tsx | 18 +++++++++++++++++- .../useGetExpensifyCardFromReportAction.ts | 17 +++++++---------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index a930609520f5..35f5d0495910 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -2,13 +2,11 @@ import {deepEqual} from 'fast-equals'; import React, {useMemo, useRef} from 'react'; import useCurrentReportID from '@hooks/useCurrentReportID'; import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; -import useOnyx from '@hooks/useOnyx'; import {getSortedReportActions, shouldReportActionBeVisibleAsLastAction} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; import OptionRowLHN from './OptionRowLHN'; import type {OptionRowLHNDataProps} from './types'; @@ -45,22 +43,20 @@ function OptionRowLHNData({ const optionItemRef = useRef(undefined); - const [reportActionsData] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canBeMissing: true}); - const lastAction = useMemo(() => { - if (!reportActionsData || !fullReport) { + if (!reportActions || !fullReport) { return undefined; } const canUserPerformWriteAction = canUserPerformWriteActionUtil(fullReport); - const actionsArray = getSortedReportActions(Object.values(reportActionsData)); + const actionsArray = getSortedReportActions(Object.values(reportActions)); const reportActionsForDisplay = actionsArray.filter( (reportAction) => shouldReportActionBeVisibleAsLastAction(reportAction, canUserPerformWriteAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, ); return reportActionsForDisplay.at(-1); - }, [reportActionsData, fullReport]); + }, [reportActions, fullReport]); const card = useGetExpensifyCardFromReportAction({reportAction: lastAction, policyID: fullReport?.policyID}); const optionItem = useMemo(() => { diff --git a/src/components/OnyxListItemProvider.tsx b/src/components/OnyxListItemProvider.tsx index e1a9927b6b9e..606b99b21def 100644 --- a/src/components/OnyxListItemProvider.tsx +++ b/src/components/OnyxListItemProvider.tsx @@ -15,6 +15,8 @@ const [SessionProvider, , useSession] = createOnyxContext(ONYXKEYS.SESSION); const [PolicyCategoriesProvider, , usePolicyCategories] = createOnyxContext(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); const [PolicyTagsProvider, , usePolicyTags] = createOnyxContext(ONYXKEYS.COLLECTION.POLICY_TAGS); const [ReportTransactionsAndViolationsProvider, , useAllReportsTransactionsAndViolations] = createOnyxContext(ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS); +const [CardListProvider, , useCardList] = createOnyxContext(ONYXKEYS.CARD_LIST); +const [WorkspaceCardListProvider, , useWorkspaceCardList] = createOnyxContext(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST); type OnyxListItemProviderProps = { /** Rendered child component */ @@ -32,6 +34,8 @@ function OnyxListItemProvider(props: OnyxListItemProviderProps) { PolicyCategoriesProvider, PolicyTagsProvider, ReportTransactionsAndViolationsProvider, + CardListProvider, + WorkspaceCardListProvider, ]} > {props.children} @@ -43,4 +47,16 @@ OnyxListItemProvider.displayName = 'OnyxListItemProvider'; export default OnyxListItemProvider; -export {usePersonalDetails, BetasContext, useBetas, PersonalDetailsContext, useBlockedFromConcierge, useSession, usePolicyCategories, usePolicyTags, useAllReportsTransactionsAndViolations}; +export { + usePersonalDetails, + BetasContext, + useBetas, + PersonalDetailsContext, + useBlockedFromConcierge, + useSession, + usePolicyCategories, + usePolicyTags, + useAllReportsTransactionsAndViolations, + useCardList, + useWorkspaceCardList, +}; diff --git a/src/hooks/useGetExpensifyCardFromReportAction.ts b/src/hooks/useGetExpensifyCardFromReportAction.ts index 653a772044c9..a88229859d72 100644 --- a/src/hooks/useGetExpensifyCardFromReportAction.ts +++ b/src/hooks/useGetExpensifyCardFromReportAction.ts @@ -1,25 +1,22 @@ +import {useCardList, useWorkspaceCardList} from '@components/OnyxListItemProvider'; import {getPolicy, getWorkspaceAccountID, isPolicyAdmin} from '@libs/PolicyUtils'; import {getOriginalMessage, isCardIssuedAction} from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Card, ReportAction} from '@src/types/onyx'; -import useOnyx from './useOnyx'; function useGetExpensifyCardFromReportAction({reportAction, policyID}: {reportAction?: ReportAction; policyID?: string}): Card | undefined { - const [allUserCards] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); - const [allExpensifyCards] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, { - selector: (val) => { - const workspaceAccountID = getWorkspaceAccountID(policyID); - return val?.[`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`] ?? {}; - }, - canBeMissing: true, - }); + const allUserCards = useCardList(); + const workspaceAccountID = getWorkspaceAccountID(policyID); + const allExpensifyCards = useWorkspaceCardList(); + const expensifyCards = allExpensifyCards?.[`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`] ?? {}; + const cardIssuedActionOriginalMessage = isCardIssuedAction(reportAction) ? getOriginalMessage(reportAction) : undefined; const cardID = cardIssuedActionOriginalMessage?.cardID ?? CONST.DEFAULT_NUMBER_ID; // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation - return isPolicyAdmin(getPolicy(policyID)) ? allExpensifyCards?.[cardID] : allUserCards?.[cardID]; + return isPolicyAdmin(getPolicy(policyID)) ? expensifyCards?.[cardID] : allUserCards?.[cardID]; } export default useGetExpensifyCardFromReportAction; From d25c4b93d8aade3e46395c0cf3d64c3b09254c7b Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 7 Aug 2025 22:53:06 +0700 Subject: [PATCH 2/2] fix UTs --- ...useGetExpensifyCardFromReportActionTest.ts | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/tests/unit/useGetExpensifyCardFromReportActionTest.ts b/tests/unit/useGetExpensifyCardFromReportActionTest.ts index 1aa4496b939a..176fa622b20f 100644 --- a/tests/unit/useGetExpensifyCardFromReportActionTest.ts +++ b/tests/unit/useGetExpensifyCardFromReportActionTest.ts @@ -1,5 +1,6 @@ import {renderHook} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; +import {useCardList, useWorkspaceCardList} from '@components/OnyxListItemProvider'; import {getPolicy, getWorkspaceAccountID, isPolicyAdmin} from '@libs/PolicyUtils'; import {getOriginalMessage, isCardIssuedAction} from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; @@ -12,6 +13,10 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // Mock the dependencies jest.mock('@libs/PolicyUtils'); jest.mock('@libs/ReportActionsUtils'); +jest.mock('@components/OnyxListItemProvider', () => ({ + useCardList: jest.fn(), + useWorkspaceCardList: jest.fn(), +})); // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation @@ -20,6 +25,8 @@ const mockGetWorkspaceAccountID = getWorkspaceAccountID as jest.MockedFunction; const mockGetOriginalMessage = getOriginalMessage as jest.MockedFunction; const mockIsCardIssuedAction = isCardIssuedAction as jest.MockedFunction; +const mockUseCardList = useCardList as jest.MockedFunction; +const mockUseWorkspaceCardList = useWorkspaceCardList as jest.MockedFunction; describe('useGetExpensifyCardFromReportAction', () => { const mockCard: Card = { @@ -76,6 +83,8 @@ describe('useGetExpensifyCardFromReportAction', () => { mockIsPolicyAdmin.mockReturnValue(false); mockGetOriginalMessage.mockReturnValue({cardID: 123, assigneeAccountID: 1}); mockIsCardIssuedAction.mockReturnValue(true); + mockUseCardList.mockReturnValue({}); + mockUseWorkspaceCardList.mockReturnValue({}); }); describe('when reportAction is not a card issued action', () => { @@ -102,8 +111,7 @@ describe('useGetExpensifyCardFromReportAction', () => { it('returns card from allUserCards when card exists', async () => { // eslint-disable-next-line @typescript-eslint/naming-convention - Onyx.set(ONYXKEYS.CARD_LIST, {'123': mockCard}); - await waitForBatchedUpdatesWithAct(); + mockUseCardList.mockReturnValue({'123': mockCard}); const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); await waitForBatchedUpdatesWithAct(); @@ -112,8 +120,7 @@ describe('useGetExpensifyCardFromReportAction', () => { }); it('returns undefined when card does not exist in allUserCards', async () => { - Onyx.set(ONYXKEYS.CARD_LIST, {}); - await waitForBatchedUpdatesWithAct(); + mockUseCardList.mockReturnValue({}); const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); await waitForBatchedUpdatesWithAct(); @@ -143,8 +150,7 @@ describe('useGetExpensifyCardFromReportAction', () => { it('returns card from allExpensifyCards when card exists', async () => { const workspaceCardsKey = `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}123_${CONST.EXPENSIFY_CARD.BANK}` as OnyxKey; // eslint-disable-next-line @typescript-eslint/naming-convention - Onyx.set(workspaceCardsKey, {123: mockCard}); - await waitForBatchedUpdatesWithAct(); + mockUseWorkspaceCardList.mockReturnValue({[workspaceCardsKey]: {123: mockCard}}); const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); await waitForBatchedUpdatesWithAct(); @@ -155,8 +161,7 @@ describe('useGetExpensifyCardFromReportAction', () => { it('returns undefined when card does not exist in allExpensifyCards', async () => { const workspaceCardsKey = `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}123_${CONST.EXPENSIFY_CARD.BANK}` as OnyxKey; // eslint-disable-next-line @typescript-eslint/naming-convention - Onyx.set(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {[workspaceCardsKey]: {}}); - await waitForBatchedUpdatesWithAct(); + mockUseWorkspaceCardList.mockReturnValue({[workspaceCardsKey]: {}}); const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); await waitForBatchedUpdatesWithAct(); @@ -168,16 +173,22 @@ describe('useGetExpensifyCardFromReportAction', () => { describe('reactivity to Onyx changes', () => { it('updates when allUserCards changes', async () => { + mockUseCardList.mockReturnValue({}); + mockUseWorkspaceCardList.mockReturnValue({}); + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); await waitForBatchedUpdatesWithAct(); expect(result.current).toBeUndefined(); // eslint-disable-next-line @typescript-eslint/naming-convention - Onyx.set(ONYXKEYS.CARD_LIST, {'123': mockCard}); + mockUseCardList.mockReturnValue({'123': mockCard}); + + // Re-render the hook to get the updated result + const {result: updatedResult} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); await waitForBatchedUpdatesWithAct(); - expect(result.current).toEqual(mockCard); + expect(updatedResult.current).toEqual(mockCard); }); it('updates when allExpensifyCards changes for policy admin', async () => { @@ -193,26 +204,30 @@ describe('useGetExpensifyCardFromReportAction', () => { isPolicyExpenseChatEnabled: false, workspaceAccountID: 123, }); + + // Set initial state + mockUseCardList.mockReturnValue({}); + mockUseWorkspaceCardList.mockReturnValue({}); + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); await waitForBatchedUpdatesWithAct(); expect(result.current).toBeUndefined(); - // eslint-disable-next-line @typescript-eslint/naming-convention const workspaceCardsKey = `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}123_${CONST.EXPENSIFY_CARD.BANK}` as OnyxKey; // eslint-disable-next-line @typescript-eslint/naming-convention - Onyx.set(workspaceCardsKey, {123: mockCard}); + mockUseWorkspaceCardList.mockReturnValue({[workspaceCardsKey]: {123: mockCard}}); + const {result: updatedResult} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); await waitForBatchedUpdatesWithAct(); - expect(result.current).toEqual(mockCard); + expect(updatedResult.current).toEqual(mockCard); }); }); describe('workspace account ID generation', () => { it('calls getWorkspaceAccountID with correct policyID', async () => { // eslint-disable-next-line @typescript-eslint/naming-convention - Onyx.set(ONYXKEYS.CARD_LIST, {'123': mockCard}); - await waitForBatchedUpdatesWithAct(); + mockUseCardList.mockReturnValue({'123': mockCard}); const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'test-policy-123'})); await waitForBatchedUpdatesWithAct(); @@ -238,8 +253,7 @@ describe('useGetExpensifyCardFromReportAction', () => { mockGetPolicy.mockReturnValue(testPolicy); // eslint-disable-next-line @typescript-eslint/naming-convention - Onyx.set(ONYXKEYS.CARD_LIST, {'123': mockCard}); - await waitForBatchedUpdatesWithAct(); + mockUseCardList.mockReturnValue({'123': mockCard}); const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); await waitForBatchedUpdatesWithAct();