diff --git a/src/hooks/useCurrentReportID.tsx b/src/hooks/useCurrentReportID.tsx index c6574ac4658e..bbdaf3f48da8 100644 --- a/src/hooks/useCurrentReportID.tsx +++ b/src/hooks/useCurrentReportID.tsx @@ -1,9 +1,11 @@ -import type {NavigationState} from '@react-navigation/native'; +import type {NavigationState, PartialState} from '@react-navigation/native'; import React, {createContext, startTransition, useCallback, useContext, useMemo, useRef, useState} from 'react'; import Navigation from '@libs/Navigation/Navigation'; +import NAVIGATORS from '@src/NAVIGATORS'; type CurrentReportIDStateContextType = { currentReportID: string | undefined; + currentRHPReportID?: string | undefined; }; type CurrentReportIDActionsContextType = { @@ -20,28 +22,46 @@ type CurrentReportIDContextProviderProps = { onSetCurrentReportID?: (reportID: string | undefined) => void; }; +/** + * Traverse the focused route at each level of the navigation state to find a reportID param. + * This handles modal navigators (e.g. RightModalNavigator > ExpenseReport) that carry a reportID + * in their screen params but are not part of the ReportsSplitNavigator hierarchy. + */ +function getFocusedRouteReportID(state: NavigationState | PartialState): string | undefined { + const index = state.index ?? state.routes.length - 1; + const focusedRoute = state.routes[index]; + if (!focusedRoute) { + return; + } + if (focusedRoute.params && 'reportID' in focusedRoute.params && typeof focusedRoute.params.reportID === 'string') { + return focusedRoute.params.reportID; + } + if (focusedRoute.state) { + return getFocusedRouteReportID(focusedRoute.state); + } +} + const defaultCurrentReportIDActionsContext: CurrentReportIDActionsContextType = { updateCurrentReportID: () => {}, }; -const CurrentReportIDStateContext = createContext({currentReportID: undefined}); +const CurrentReportIDStateContext = createContext({currentReportID: undefined, currentRHPReportID: undefined}); const CurrentReportIDActionsContext = createContext(defaultCurrentReportIDActionsContext); function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderProps) { const [currentReportID, setCurrentReportID] = useState(''); + const [currentRHPReportID, setCurrentRHPReportID] = useState(undefined); // Tracks the most recently requested reportID synchronously so the dedupe // check below stays accurate even while a startTransition is pending. const pendingReportIDRef = useRef(''); /** - * This function is used to update the currentReportID + * This function is used to update the currentReportID and currentRHPReportID * @param state root navigation state */ const updateCurrentReportID = useCallback( (state: NavigationState) => { - const reportID = Navigation.getTopmostReportId(state); - /* * Make sure we don't make the reportID undefined when switching between the chat list and settings tab. * This helps prevent unnecessary re-renders. @@ -50,25 +70,31 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro if (params && 'screen' in params && typeof params.screen === 'string' && params.screen.indexOf('Settings_') !== -1) { return; } - if (pendingReportIDRef.current === reportID) { - return; - } - if (!pendingReportIDRef.current && !reportID) { - return; + const reportID = Navigation.getTopmostReportId(state); + + if (pendingReportIDRef.current !== reportID) { + if (pendingReportIDRef.current || reportID) { + pendingReportIDRef.current = reportID; + props.onSetCurrentReportID?.(reportID); + // Mark the report ID update as a non-urgent transition so React can keep the + // UI responsive to user input while the (potentially expensive) report screen + // re-render is processed in the background. + startTransition(() => { + setCurrentReportID(reportID); + }); + } } - pendingReportIDRef.current = reportID; - props.onSetCurrentReportID?.(reportID); - // Mark the report ID update as a non-urgent transition so React can keep the - // UI responsive to user input while the (potentially expensive) report screen - // re-render is processed in the background. - startTransition(() => { - setCurrentReportID(reportID); - }); + const focusedTopRoute = state.routes[state.index]; + const modalReportID = focusedTopRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && focusedTopRoute.state ? getFocusedRouteReportID(focusedTopRoute.state) : undefined; + + if (currentRHPReportID !== modalReportID && (currentRHPReportID || modalReportID)) { + setCurrentRHPReportID(modalReportID); + } }, // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to re-render when onSetCurrentReportID changes - [setCurrentReportID], + [setCurrentReportID, setCurrentRHPReportID, currentRHPReportID], ); const actionsContextValue = useMemo( @@ -81,8 +107,9 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro const stateContextValue = useMemo( () => ({ currentReportID, + currentRHPReportID, }), - [currentReportID], + [currentReportID, currentRHPReportID], ); return ( diff --git a/src/libs/API/parameters/AddCommentOrAttachmentParams.ts b/src/libs/API/parameters/AddCommentOrAttachmentParams.ts index c4f973e8ac96..ead10653119a 100644 --- a/src/libs/API/parameters/AddCommentOrAttachmentParams.ts +++ b/src/libs/API/parameters/AddCommentOrAttachmentParams.ts @@ -14,6 +14,7 @@ type AddCommentOrAttachmentParams = { pageHTML?: string; optimisticConciergeReportActionID?: string; pregeneratedResponse?: string; + sidePanelContext?: string; }; export default AddCommentOrAttachmentParams; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 1a01a288d3fc..0712a27a8a4f 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -219,6 +219,7 @@ import type { ReportAttributesDerivedValue, ReportNextStepDeprecated, ReportUserIsTyping, + SidePanelContext, Transaction, TransactionViolations, VisibleReportActionsDerivedValue, @@ -355,6 +356,7 @@ type AddCommentParams = { currentUserAccountID: number; shouldPlaySound?: boolean; isInSidePanel?: boolean; + sidePanelContext?: SidePanelContext; pregeneratedResponseParams?: PregeneratedResponseParams; reportActionID?: string; delegateAccountID: number | undefined; @@ -369,6 +371,7 @@ type AddActionsParams = { text?: string; file?: FileObject; isInSidePanel?: boolean; + sidePanelContext?: SidePanelContext; pregeneratedResponseParams?: PregeneratedResponseParams; reportActionID?: string; delegateAccountID: number | undefined; @@ -385,6 +388,7 @@ type AddAttachmentWithCommentParams = { shouldPlaySound?: boolean; isInSidePanel?: boolean; delegateAccountID: number | undefined; + sidePanelContext?: SidePanelContext; }; const addNewMessageWithText = new Set([WRITE_COMMANDS.ADD_COMMENT, WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT]); @@ -682,6 +686,7 @@ function addActions({ text = '', file, isInSidePanel = false, + sidePanelContext, pregeneratedResponseParams, reportActionID, delegateAccountID, @@ -797,7 +802,8 @@ function addActions({ idempotencyKey: Str.guid(), }; - if (reportIDDeeplinkedFromOldDot === reportID && isConciergeChatReport(report)) { + const isConciergeChat = isConciergeChatReport(report); + if (reportIDDeeplinkedFromOldDot === reportID && isConciergeChat) { parameters.isOldDotConciergeChat = true; } @@ -805,13 +811,17 @@ function addActions({ parameters.attachmentID = attachmentID; } - if (isInSidePanel && (isConciergeChatReport(report) || isAdminRoom(report))) { + if (isInSidePanel && (isConciergeChat || isAdminRoom(report))) { const pageHTML = capturePageHTML(); if (pageHTML) { parameters.pageHTML = pageHTML; } } + if (isInSidePanel && isConciergeChat && sidePanelContext && commandName === WRITE_COMMANDS.ADD_COMMENT) { + parameters.sidePanelContext = JSON.stringify(sidePanelContext); + } + // Add pregenerated params if (pregeneratedResponseParams) { parameters.optimisticConciergeReportActionID = pregeneratedResponseParams.optimisticConciergeReportActionID; @@ -938,6 +948,7 @@ function addAttachmentWithComment({ shouldPlaySound = false, isInSidePanel = false, delegateAccountID, + sidePanelContext, }: AddAttachmentWithCommentParams) { if (!report?.reportID) { return; @@ -952,7 +963,7 @@ function addAttachmentWithComment({ // Single attachment if (!Array.isArray(attachments)) { - addActions({report, notifyReportID, ancestors, timezoneParam: timezone, currentUserAccountID, text, file: attachments, isInSidePanel, delegateAccountID}); + addActions({report, notifyReportID, ancestors, timezoneParam: timezone, currentUserAccountID, text, file: attachments, isInSidePanel, delegateAccountID, sidePanelContext}); handlePlaySound(); return; } @@ -962,7 +973,18 @@ function addAttachmentWithComment({ // Remaining: attachment-only actions (no text duplication) for (let i = 1; i < attachments?.length; i += 1) { - addActions({report, notifyReportID, ancestors, timezoneParam: timezone, currentUserAccountID, text: '', file: attachments?.at(i), isInSidePanel, delegateAccountID}); + addActions({ + report, + notifyReportID, + ancestors, + timezoneParam: timezone, + currentUserAccountID, + text: '', + file: attachments?.at(i), + isInSidePanel, + delegateAccountID, + sidePanelContext, + }); } // Play sound once @@ -979,6 +1001,7 @@ function addComment({ currentUserAccountID, shouldPlaySound, isInSidePanel, + sidePanelContext, pregeneratedResponseParams, reportActionID, delegateAccountID, @@ -986,7 +1009,19 @@ function addComment({ if (shouldPlaySound) { playSound(SOUNDS.DONE); } - addActions({report, notifyReportID, ancestors, timezoneParam, currentUserAccountID, text, isInSidePanel, pregeneratedResponseParams, reportActionID, delegateAccountID}); + addActions({ + report, + notifyReportID, + ancestors, + timezoneParam, + currentUserAccountID, + text, + isInSidePanel, + pregeneratedResponseParams, + reportActionID, + delegateAccountID, + sidePanelContext, + }); } function reportActionsExist(reportID: string): boolean { diff --git a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts index b59ce22502fd..7428e041b6b0 100644 --- a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts +++ b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts @@ -26,6 +26,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import {useComposerMeta} from './ComposerContext'; +import useSidePanelContext from './useSidePanelContext'; function useComposerSubmit(reportID: string): (comment: string) => void { const {isOffline} = useNetwork(); @@ -34,6 +35,7 @@ function useComposerSubmit(reportID: string): (comment: string) => void { const personalDetails = usePersonalDetails(); const {availableLoginsList} = useShortMentionsList(); const isInSidePanel = useIsInSidePanel(); + const sidePanelContext = useSidePanelContext(reportID); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE); const delegateAccountID = useDelegateAccountID(); @@ -75,6 +77,7 @@ function useComposerSubmit(reportID: string): (comment: string) => void { shouldPlaySound: true, isInSidePanel, delegateAccountID, + sidePanelContext, }); attachmentFileRef.current = null; return; @@ -145,6 +148,7 @@ function useComposerSubmit(reportID: string): (comment: string) => void { currentUserAccountID: currentUserPersonalDetails.accountID, shouldPlaySound: true, isInSidePanel, + sidePanelContext, reportActionID: optimisticReportActionID, delegateAccountID, }); diff --git a/src/pages/inbox/report/ReportActionCompose/useSidePanelContext.ts b/src/pages/inbox/report/ReportActionCompose/useSidePanelContext.ts new file mode 100644 index 000000000000..5c94d31bdf26 --- /dev/null +++ b/src/pages/inbox/report/ReportActionCompose/useSidePanelContext.ts @@ -0,0 +1,56 @@ +import {useMemo} from 'react'; +import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useCurrentReportIDState} from '@hooks/useCurrentReportID'; +import useIsInSidePanel from '@hooks/useIsInSidePanel'; +import useOnyx from '@hooks/useOnyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +function useSidePanelContext(reportID: string): OnyxTypes.SidePanelContext | undefined { + const isInSidePanel = useIsInSidePanel(); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + const {currentReportID, currentRHPReportID} = useCurrentReportIDState(); + const {currentSearchQueryJSON, selectedTransactionIDs, selectedTransactions, selectedReports} = useSearchStateContext(); + + return useMemo(() => { + if (conciergeReportID !== reportID || !isInSidePanel) { + return undefined; + } + + const contextReportID = currentRHPReportID ?? currentReportID ?? undefined; + + // selectedTransactions (map) is populated from the Search list; selectedTransactionIDs (array) + // is populated from the report table view. The two are mutually exclusive. + const txIDsFromMap = !isEmptyObject(selectedTransactions) + ? Object.entries(selectedTransactions) + .filter(([, info]) => info.isSelected && !!info.transaction) + .map(([id]) => id) + : []; + const allTransactionIDs = txIDsFromMap.length > 0 ? txIDsFromMap : selectedTransactionIDs; + const selectedTransactionIDsForContext = allTransactionIDs.length > 0 ? allTransactionIDs.join(',') : undefined; + + const selectedReportIDsForContext = + selectedReports.length > 0 + ? selectedReports + .map((r) => r.reportID) + .filter((id): id is string => !!id) + .join(',') || undefined + : undefined; + + // This condition is reached when we are either in the global Reports => Reports page, or within a single expense report having multiple transactions. + // If we have selectedReportIDs, that means we're in the Reports page, otherwise we're in the expense report RHP. + if (currentSearchQueryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) { + return selectedReportIDsForContext ? {selectedReportIDs: selectedReportIDsForContext} : {reportID: contextReportID, selectedTransactionIDs: selectedTransactionIDsForContext}; + } + + if (!contextReportID && !selectedTransactionIDsForContext && !selectedReportIDsForContext) { + return undefined; + } + + return {reportID: contextReportID, selectedTransactionIDs: selectedTransactionIDsForContext, selectedReportIDs: selectedReportIDsForContext}; + }, [conciergeReportID, reportID, isInSidePanel, currentSearchQueryJSON?.type, currentRHPReportID, currentReportID, selectedTransactionIDs, selectedTransactions, selectedReports]); +} + +export default useSidePanelContext; diff --git a/src/types/onyx/SidePanel.tsx b/src/types/onyx/SidePanel.tsx index 915e2ed7c48c..2f2bfa26ca9d 100644 --- a/src/types/onyx/SidePanel.tsx +++ b/src/types/onyx/SidePanel.tsx @@ -6,4 +6,11 @@ type SidePanel = { openNarrowScreen: boolean; }; +/** + * Describes the context of what the user was viewing when they sent a message from the Side Panel. + * Sent to the backend so Concierge can tailor its response to the user's current context. + */ +type SidePanelContext = {reportID?: string; selectedTransactionIDs?: string; selectedReportIDs?: string}; + export default SidePanel; +export type {SidePanelContext}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index adf447f942c3..8934f860d3bc 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -156,6 +156,7 @@ import type Session from './Session'; import type ShareBankAccount from './ShareBankAccount'; import type ShareTempFile from './ShareTempFile'; import type SidePanel from './SidePanel'; +import type {SidePanelContext} from './SidePanel'; import type StripeCustomerID from './StripeCustomerID'; import type SupportalPermissionDenied from './SupportalPermissionDenied'; import type Task from './Task'; @@ -362,6 +363,7 @@ export type { DismissedProductTraining, TravelProvisioning, SidePanel, + SidePanelContext, LastPaymentMethodType, ReportAttributesDerivedValue, LastSearchParams, diff --git a/tests/unit/SuggestionMentionTest.tsx b/tests/unit/SuggestionMentionTest.tsx index cc9760d2e52f..afe373edc9e1 100644 --- a/tests/unit/SuggestionMentionTest.tsx +++ b/tests/unit/SuggestionMentionTest.tsx @@ -121,7 +121,7 @@ describe('SuggestionMention', () => { mockUsePersonalDetails.mockImplementation(() => mockPersonalDetails); mockUseArrowKeyFocusManager.mockReturnValue([0, mockSetHighlightedMentionIndex]); - mockUseCurrentReportIDState.mockReturnValue({currentReportID: ''}); + mockUseCurrentReportIDState.mockReturnValue({currentReportID: '', currentRHPReportID: ''}); mockUseCurrentUserPersonalDetails.mockReturnValue({accountID: 1, login: 'current@gmail.com'}); mockUseDebounce.mockImplementation((callback) => { const callbackRef = React.useRef(callback); diff --git a/tests/unit/hooks/useSidePanelContext.test.ts b/tests/unit/hooks/useSidePanelContext.test.ts new file mode 100644 index 000000000000..f4e5c9115458 --- /dev/null +++ b/tests/unit/hooks/useSidePanelContext.test.ts @@ -0,0 +1,190 @@ +import {renderHook} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +// eslint-disable-next-line import/order +import useSidePanelContext from '@pages/inbox/report/ReportActionCompose/useSidePanelContext'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; + +const REPORT_ID = 'report_123'; + +// Mutable state referenced by jest.mock factories below +let mockIsInSidePanel = false; +let mockCurrentReportIDState: {currentReportID: string | undefined; currentRHPReportID: string | undefined} = { + currentReportID: undefined, + currentRHPReportID: undefined, +}; +let mockSearchState: { + currentSearchQueryJSON: {type?: string} | undefined; + selectedTransactionIDs: string[]; + selectedTransactions: Record}>; + selectedReports: Array<{reportID?: string}>; +} = { + currentSearchQueryJSON: undefined, + selectedTransactionIDs: [], + selectedTransactions: {}, + selectedReports: [], +}; + +jest.mock('@hooks/useIsInSidePanel', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: () => mockIsInSidePanel, +})); + +jest.mock('@hooks/useCurrentReportID', () => ({ + useCurrentReportIDState: () => mockCurrentReportIDState, +})); + +jest.mock('@components/Search/SearchContext', () => ({ + useSearchStateContext: () => mockSearchState, +})); + +function resetMocks() { + mockIsInSidePanel = false; + mockCurrentReportIDState = {currentReportID: undefined, currentRHPReportID: undefined}; + mockSearchState = { + currentSearchQueryJSON: undefined, + selectedTransactionIDs: [], + selectedTransactions: {}, + selectedReports: [], + }; +} + +describe('useSidePanelContext', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + resetMocks(); + await Onyx.clear(); + await waitForBatchedUpdates(); + }); + + async function renderWithConciergeReport(reportID = REPORT_ID) { + await Onyx.merge(ONYXKEYS.CONCIERGE_REPORT_ID, REPORT_ID); + await waitForBatchedUpdates(); + return renderHook(() => useSidePanelContext(reportID)); + } + + it('returns undefined when not in the side panel', async () => { + mockIsInSidePanel = false; + const {result} = await renderWithConciergeReport(); + await waitForBatchedUpdates(); + expect(result.current).toBeUndefined(); + }); + + it('returns undefined when reportID does not match conciergeReportID', async () => { + mockIsInSidePanel = true; + const {result} = await renderWithConciergeReport('different_report'); + await waitForBatchedUpdates(); + expect(result.current).toBeUndefined(); + }); + + it('returns undefined when no context is available', async () => { + mockIsInSidePanel = true; + const {result} = await renderWithConciergeReport(); + await waitForBatchedUpdates(); + expect(result.current).toBeUndefined(); + }); + + describe('EXPENSE_REPORT search type', () => { + beforeEach(() => { + mockIsInSidePanel = true; + mockSearchState.currentSearchQueryJSON = {type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT}; + }); + + it('returns only selectedReportIDs when reports are selected', async () => { + mockSearchState.selectedReports = [{reportID: 'report_1'}, {reportID: 'report_2'}]; + const {result} = await renderWithConciergeReport(); + await waitForBatchedUpdates(); + expect(result.current).toEqual({selectedReportIDs: 'report_1,report_2'}); + }); + + it('falls back to contextReportID and selectedTransactionIDs when no reports are selected', async () => { + mockSearchState.selectedReports = []; + mockCurrentReportIDState = {currentRHPReportID: 'rhp_report', currentReportID: undefined}; + mockSearchState.selectedTransactionIDs = ['txn_a']; + const {result} = await renderWithConciergeReport(); + await waitForBatchedUpdates(); + expect(result.current).toEqual({reportID: 'rhp_report', selectedTransactionIDs: 'txn_a'}); + }); + + it('includes child transaction selections when drilling into a report', async () => { + mockSearchState.selectedReports = []; + mockSearchState.selectedTransactionIDs = ['txn_1', 'txn_2']; + const {result} = await renderWithConciergeReport(); + await waitForBatchedUpdates(); + expect(result.current).toMatchObject({selectedTransactionIDs: 'txn_1,txn_2'}); + }); + }); + + describe('contextReportID resolution', () => { + beforeEach(() => { + mockIsInSidePanel = true; + }); + + it('prefers currentRHPReportID over currentReportID', async () => { + mockCurrentReportIDState = {currentRHPReportID: 'rhp_report', currentReportID: 'main_report'}; + const {result} = await renderWithConciergeReport(); + await waitForBatchedUpdates(); + expect(result.current).toMatchObject({reportID: 'rhp_report'}); + }); + + it('falls back to currentReportID when no RHP report is open', async () => { + mockCurrentReportIDState = {currentRHPReportID: undefined, currentReportID: 'main_report'}; + const {result} = await renderWithConciergeReport(); + await waitForBatchedUpdates(); + expect(result.current).toMatchObject({reportID: 'main_report'}); + }); + }); + + describe('transaction ID derivation', () => { + beforeEach(() => { + mockIsInSidePanel = true; + }); + + it('derives IDs from selectedTransactions map (Search list selections), filtering out unselected and non-transaction entries', async () => { + mockSearchState.selectedTransactions = { + txn1: {isSelected: true, transaction: {}}, + txn2: {isSelected: true, transaction: {}}, + txn3: {isSelected: false, transaction: {}}, // not selected + reportKey: {isSelected: true, transaction: undefined}, // empty report row — no transaction + }; + const {result} = await renderWithConciergeReport(); + await waitForBatchedUpdates(); + expect(result.current).toMatchObject({selectedTransactionIDs: 'txn1,txn2'}); + }); + + it('falls back to selectedTransactionIDs array when map is empty (report table view selections)', async () => { + mockSearchState.selectedTransactions = {}; + mockSearchState.selectedTransactionIDs = ['txn_a', 'txn_b']; + const {result} = await renderWithConciergeReport(); + await waitForBatchedUpdates(); + expect(result.current).toMatchObject({selectedTransactionIDs: 'txn_a,txn_b'}); + }); + + it('prefers the map over the array when both are non-empty', async () => { + mockSearchState.selectedTransactions = {txnMap: {isSelected: true, transaction: {}}}; + mockSearchState.selectedTransactionIDs = ['txnArray']; + const {result} = await renderWithConciergeReport(); + await waitForBatchedUpdates(); + expect(result.current).toMatchObject({selectedTransactionIDs: 'txnMap'}); + }); + }); + + it('returns all available context when every field is populated', async () => { + mockIsInSidePanel = true; + mockCurrentReportIDState = {currentRHPReportID: 'rhp_report', currentReportID: undefined}; + mockSearchState.selectedTransactions = {txn1: {isSelected: true, transaction: {}}}; + mockSearchState.selectedReports = [{reportID: 'report_1'}]; + const {result} = await renderWithConciergeReport(); + await waitForBatchedUpdates(); + expect(result.current).toEqual({ + reportID: 'rhp_report', + selectedTransactionIDs: 'txn1', + selectedReportIDs: 'report_1', + }); + }); +});