From 1bb121de0ebda946337fbeeca91bf95e98e53fea Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Thu, 4 Sep 2025 11:19:14 +0300 Subject: [PATCH 01/25] wip --- src/libs/DraftCommentUtils.ts | 28 ++----------------- src/libs/ReportUtils.ts | 4 ++- src/libs/SidebarUtils.ts | 10 ++++++- .../ComposerWithSuggestions.tsx | 3 +- .../ReportActionCompose.tsx | 3 +- tests/unit/SidebarUtilsTest.ts | 7 ++--- 6 files changed, 20 insertions(+), 35 deletions(-) diff --git a/src/libs/DraftCommentUtils.ts b/src/libs/DraftCommentUtils.ts index b3cb32498725e..28269ea94935c 100644 --- a/src/libs/DraftCommentUtils.ts +++ b/src/libs/DraftCommentUtils.ts @@ -1,25 +1,3 @@ -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; - -let draftCommentCollection: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, - callback: (nextVal) => { - draftCommentCollection = nextVal; - }, - waitForCollectionCallback: true, -}); - -/** - * Returns a draft comment from the onyx collection for given reportID. - * Note: You should use the HOCs/hooks to get onyx data, instead of using this directly. - * A valid use-case of this function is outside React components, like in utility functions. - */ -function getDraftComment(reportID: string): OnyxEntry | null | undefined { - return draftCommentCollection?.[ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT + reportID]; -} - /** * Returns true if the report has a valid draft comment. * A valid draft comment is a non-empty string. @@ -31,8 +9,8 @@ function isValidDraftComment(comment?: string | null): boolean { /** * Returns true if the report has a valid draft comment. */ -function hasValidDraftComment(reportID: string): boolean { - return isValidDraftComment(getDraftComment(reportID)); +function hasValidDraftComment(reportID: string, draftComment: string | null): boolean { + return isValidDraftComment(draftComment); } /** @@ -44,4 +22,4 @@ function prepareDraftComment(comment: string | null) { return comment || null; } -export {getDraftComment, isValidDraftComment, hasValidDraftComment, prepareDraftComment}; +export {isValidDraftComment, hasValidDraftComment, prepareDraftComment}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9c1644720c516..d4e48528bdc13 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8348,6 +8348,7 @@ type ShouldReportBeInOptionListParams = { login?: string; includeDomainEmail?: boolean; isReportArchived?: boolean; + draftComment: string | null; }; function reasonForReportToBeInOptionList({ @@ -8358,6 +8359,7 @@ function reasonForReportToBeInOptionList({ betas, excludeEmptyChats, doesReportHaveViolations, + draftComment, includeSelfDM = false, login, includeDomainEmail = false, @@ -8430,7 +8432,7 @@ function reasonForReportToBeInOptionList({ } // Retrieve the draft comment for the report and convert it to a boolean - const hasDraftComment = hasValidDraftComment(report.reportID); + const hasDraftComment = hasValidDraftComment(report.reportID, draftComment); // Include reports that are relevant to the user in any view mode. Criteria include having a draft or having a GBR showing. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 59c701d3adc4d..9ca3f5ddbba95 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -177,6 +177,7 @@ function shouldDisplayReportInLHN( isInFocusMode: boolean, betas: OnyxEntry, transactionViolations: OnyxCollection, + draftComment: string | null, isReportArchived?: boolean, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { @@ -209,7 +210,12 @@ function shouldDisplayReportInLHN( // Check if report should override hidden status const isSystemChat = isSystemChatUtil(report); const shouldOverrideHidden = - hasValidDraftComment(report.reportID) || hasErrorsOtherThanFailedReceipt || isFocused || isSystemChat || !!report.isPinned || reportAttributes?.[report?.reportID]?.requiresAttention; + hasValidDraftComment(report.reportID, draftComment) || + hasErrorsOtherThanFailedReceipt || + isFocused || + isSystemChat || + !!report.isPinned || + reportAttributes?.[report?.reportID]?.requiresAttention; if (isHidden && !shouldOverrideHidden) { return {shouldDisplay: false}; @@ -224,6 +230,7 @@ function shouldDisplayReportInLHN( betas, excludeEmptyChats: true, doesReportHaveViolations, + draftComment, includeSelfDM: true, isReportArchived, }); @@ -313,6 +320,7 @@ function updateReportsToDisplayInLHN( */ function categorizeReportsForLHN( reportsToDisplay: ReportsToDisplayInLHN, + draftComment: string | null, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 3d8805f9f6193..f01115e02635f 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -34,7 +34,6 @@ import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import {forceClearInput} from '@libs/ComponentUtils'; import {canSkipTriggerHotkeys, findCommonSuffixLength, insertText, insertWhiteSpaceAtIndex} from '@libs/ComposerUtils'; import convertToLTRForComposer from '@libs/convertToLTRForComposer'; -import {getDraftComment} from '@libs/DraftCommentUtils'; import {containsOnlyEmojis, extractEmojis, getAddedEmojis, getPreferredSkinToneIndex, replaceAndExtractEmojis} from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import getPlatform from '@libs/getPlatform'; @@ -246,7 +245,7 @@ function ComposerWithSuggestions( const mobileInputScrollPosition = useRef(0); const cursorPositionValue = useSharedValue({x: 0, y: 0}); const tag = useSharedValue(-1); - const draftComment = getDraftComment(reportID) ?? ''; + const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true}); const [value, setValue] = useState(() => { if (draftComment) { emojisPresentBefore.current = extractEmojis(draftComment); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 4fc424a5cbd7f..aec2af5ada342 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -34,7 +34,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import DomUtils from '@libs/DomUtils'; -import {getDraftComment} from '@libs/DraftCommentUtils'; import getModalState from '@libs/getModalState'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Performance from '@libs/Performance'; @@ -145,6 +144,7 @@ function ReportActionCompose({ const [shouldShowComposeInput = true] = useOnyx(ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, {canBeMissing: true}); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true}); const [newParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`, {canBeMissing: true}); + const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true}); /** * Updates the Highlight state of the composer */ @@ -175,7 +175,6 @@ function ReportActionCompose({ }, [debouncedLowerIsScrollLikelyLayoutTriggered]); const [isCommentEmpty, setIsCommentEmpty] = useState(() => { - const draftComment = getDraftComment(reportID); return !draftComment || !!draftComment.match(CONST.REGEX.EMPTY_COMMENT); }); diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index 63c5caac900e7..495204f176c85 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -31,7 +31,6 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; // Mock DraftCommentUtils jest.mock('@libs/DraftCommentUtils', () => ({ hasValidDraftComment: jest.fn(), - getDraftComment: jest.fn(), isValidDraftComment: jest.fn(), prepareDraftComment: jest.fn(), })); @@ -1508,12 +1507,12 @@ describe('SidebarUtils', () => { it('should categorize reports into correct groups', () => { // Given hasValidDraftComment is mocked to return true for report '2' const {hasValidDraftComment} = require('@libs/DraftCommentUtils') as {hasValidDraftComment: jest.Mock}; - hasValidDraftComment.mockImplementation((reportID: string) => reportID === '2'); + hasValidDraftComment.mockImplementation((reportID: string, draftComment: string | null) => reportID === '2' && draftComment === 'test'); const {reports, reportNameValuePairs, reportAttributes} = createSidebarTestData(); // When the reports are categorized - const result = SidebarUtils.categorizeReportsForLHN(reports, reportNameValuePairs, reportAttributes); + const result = SidebarUtils.categorizeReportsForLHN(reports, null, reportNameValuePairs, reportAttributes); // Then the reports are categorized into the correct groups expect(result.pinnedAndGBRReports).toHaveLength(1); @@ -1549,7 +1548,7 @@ describe('SidebarUtils', () => { }; // When the reports are categorized - const result = SidebarUtils.categorizeReportsForLHN(reports, undefined, reportAttributes); + const result = SidebarUtils.categorizeReportsForLHN(reports, null, undefined, reportAttributes); // Then the reports are categorized into the correct groups expect(result.pinnedAndGBRReports).toHaveLength(1); From 29de46ed3869a3d9abbfc39352159cbe520c7c0d Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Mon, 8 Sep 2025 07:48:41 +0300 Subject: [PATCH 02/25] wip --- src/libs/DebugUtils.ts | 3 + src/libs/OptionsListUtils/index.ts | 247 +++++++++++++++-------------- 2 files changed, 127 insertions(+), 123 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index eb97ac8eb56a1..1ff8aec85865a 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -1314,6 +1314,7 @@ function getReasonForShowingRowInLHN({ isReportArchived = false, isInFocusMode = false, betas = undefined, + draftComment, }: { report: OnyxEntry; chatReport: OnyxEntry; @@ -1322,6 +1323,7 @@ function getReasonForShowingRowInLHN({ isReportArchived?: boolean; isInFocusMode?: boolean; betas?: OnyxEntry; + draftComment: string | null; }): TranslationPaths | null { if (!report) { return null; @@ -1338,6 +1340,7 @@ function getReasonForShowingRowInLHN({ doesReportHaveViolations, includeSelfDM: true, isReportArchived, + draftComment, }); if (!([CONST.REPORT_IN_LHN_REASONS.HAS_ADD_WORKSPACE_ROOM_ERRORS, CONST.REPORT_IN_LHN_REASONS.HAS_IOU_VIOLATIONS] as Array).includes(reason) && hasRBR) { diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 8e2f9ecb470ad..6657ce89aba41 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -114,6 +114,7 @@ import { isInvoiceRoom, isMoneyRequest, isPolicyAdmin, + isValidReport, isAdminRoom as reportUtilsIsAdminRoom, isAnnounceRoom as reportUtilsIsAnnounceRoom, isChatReport as reportUtilsIsChatReport, @@ -1469,129 +1470,129 @@ function getUserToInviteContactOption({ return userToInvite; } -function isValidReport(option: SearchOption, config: GetValidReportsConfig): boolean { - const { - betas = [], - includeMultipleParticipantReports = false, - includeOwnedWorkspaceChats = false, - includeThreads = false, - includeTasks = false, - includeMoneyRequests = false, - includeReadOnly = true, - transactionViolations = {}, - includeSelfDM = false, - includeInvoiceRooms = false, - action, - includeP2P = true, - includeDomainEmail = false, - loginsToExclude = {}, - excludeNonAdminWorkspaces, - } = config; - const topmostReportId = Navigation.getTopmostReportId(); - - const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${option.item.chatReportID}`]; - const doesReportHaveViolations = shouldDisplayViolationsRBRInLHN(option.item, transactionViolations); - - const shouldBeInOptionList = shouldReportBeInOptionList({ - report: option.item, - chatReport, - currentReportId: topmostReportId, - betas, - doesReportHaveViolations, - isInFocusMode: false, - excludeEmptyChats: false, - includeSelfDM, - login: option.login, - includeDomainEmail, - isReportArchived: !!option.private_isArchived, - }); - - if (!shouldBeInOptionList) { - return false; - } - - const isThread = option.isThread; - const isTaskReport = option.isTaskReport; - const isPolicyExpenseChat = option.isPolicyExpenseChat; - const isMoneyRequestReport = option.isMoneyRequestReport; - const isSelfDM = option.isSelfDM; - const isChatRoom = option.isChatRoom; - const accountIDs = getParticipantsAccountIDsForDisplay(option.item); - - if (excludeNonAdminWorkspaces && !isPolicyAdmin(option.policyID, policies)) { - return false; - } - - if (isPolicyExpenseChat && !includeOwnedWorkspaceChats) { - return false; - } - // When passing includeP2P false we are trying to hide features from users that are not ready for P2P and limited to expense chats only. - if (!includeP2P && !isPolicyExpenseChat) { - return false; - } - - if (isSelfDM && !includeSelfDM) { - return false; - } - - if (isThread && !includeThreads) { - return false; - } - - if (isTaskReport && !includeTasks) { - return false; - } - - if (isMoneyRequestReport && !includeMoneyRequests) { - return false; - } - - if (!canUserPerformWriteAction(option.item, !!option.private_isArchived) && !includeReadOnly) { - return false; - } - - // In case user needs to add credit bank account, don't allow them to submit an expense from the workspace. - if (includeOwnedWorkspaceChats && hasIOUWaitingOnCurrentUserBankAccount(option.item)) { - return false; - } - - if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) { - return false; - } - - if (option.login === CONST.EMAIL.NOTIFICATIONS) { - return false; - } - const isCurrentUserOwnedPolicyExpenseChatThatCouldShow = - option.isPolicyExpenseChat && option.ownerAccountID === currentUserAccountID && includeOwnedWorkspaceChats && !option.private_isArchived; - - const shouldShowInvoiceRoom = - includeInvoiceRooms && isInvoiceRoom(option.item) && isPolicyAdmin(option.policyID, policies) && !option.private_isArchived && canSendInvoiceFromWorkspace(option.policyID); - - /* - Exclude the report option if it doesn't meet any of the following conditions: - - It is not an owned policy expense chat that could be shown - - Multiple participant reports are not included - - It doesn't have a login - - It is not an invoice room that should be shown - */ - if (!isCurrentUserOwnedPolicyExpenseChatThatCouldShow && !includeMultipleParticipantReports && !option.login && !shouldShowInvoiceRoom) { - return false; - } - - // If we're excluding threads, check the report to see if it has a single participant and if the participant is already selected - if (!includeThreads && ((!!option.login && loginsToExclude[option.login]) || loginsToExclude[option.reportID])) { - return false; - } - - if (action === CONST.IOU.ACTION.CATEGORIZE) { - const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${option.policyID}`]; - if (!reportPolicy?.areCategoriesEnabled) { - return false; - } - } - return true; -} +// function isValidReport(option: SearchOption, config: GetValidReportsConfig): boolean { +// const { +// betas = [], +// includeMultipleParticipantReports = false, +// includeOwnedWorkspaceChats = false, +// includeThreads = false, +// includeTasks = false, +// includeMoneyRequests = false, +// includeReadOnly = true, +// transactionViolations = {}, +// includeSelfDM = false, +// includeInvoiceRooms = false, +// action, +// includeP2P = true, +// includeDomainEmail = false, +// loginsToExclude = {}, +// excludeNonAdminWorkspaces, +// } = config; +// const topmostReportId = Navigation.getTopmostReportId(); + +// const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${option.item.chatReportID}`]; +// const doesReportHaveViolations = shouldDisplayViolationsRBRInLHN(option.item, transactionViolations); + +// const shouldBeInOptionList = shouldReportBeInOptionList({ +// report: option.item, +// chatReport, +// currentReportId: topmostReportId, +// betas, +// doesReportHaveViolations, +// isInFocusMode: false, +// excludeEmptyChats: false, +// includeSelfDM, +// login: option.login, +// includeDomainEmail, +// isReportArchived: !!option.private_isArchived,, +// }); + +// if (!shouldBeInOptionList) { +// return false; +// } + +// const isThread = option.isThread; +// const isTaskReport = option.isTaskReport; +// const isPolicyExpenseChat = option.isPolicyExpenseChat; +// const isMoneyRequestReport = option.isMoneyRequestReport; +// const isSelfDM = option.isSelfDM; +// const isChatRoom = option.isChatRoom; +// const accountIDs = getParticipantsAccountIDsForDisplay(option.item); + +// if (excludeNonAdminWorkspaces && !isPolicyAdmin(option.policyID, policies)) { +// return false; +// } + +// if (isPolicyExpenseChat && !includeOwnedWorkspaceChats) { +// return false; +// } +// // When passing includeP2P false we are trying to hide features from users that are not ready for P2P and limited to expense chats only. +// if (!includeP2P && !isPolicyExpenseChat) { +// return false; +// } + +// if (isSelfDM && !includeSelfDM) { +// return false; +// } + +// if (isThread && !includeThreads) { +// return false; +// } + +// if (isTaskReport && !includeTasks) { +// return false; +// } + +// if (isMoneyRequestReport && !includeMoneyRequests) { +// return false; +// } + +// if (!canUserPerformWriteAction(option.item, !!option.private_isArchived) && !includeReadOnly) { +// return false; +// } + +// // In case user needs to add credit bank account, don't allow them to submit an expense from the workspace. +// if (includeOwnedWorkspaceChats && hasIOUWaitingOnCurrentUserBankAccount(option.item)) { +// return false; +// } + +// if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) { +// return false; +// } + +// if (option.login === CONST.EMAIL.NOTIFICATIONS) { +// return false; +// } +// const isCurrentUserOwnedPolicyExpenseChatThatCouldShow = +// option.isPolicyExpenseChat && option.ownerAccountID === currentUserAccountID && includeOwnedWorkspaceChats && !option.private_isArchived; + +// const shouldShowInvoiceRoom = +// includeInvoiceRooms && isInvoiceRoom(option.item) && isPolicyAdmin(option.policyID, policies) && !option.private_isArchived && canSendInvoiceFromWorkspace(option.policyID); + +// /* +// Exclude the report option if it doesn't meet any of the following conditions: +// - It is not an owned policy expense chat that could be shown +// - Multiple participant reports are not included +// - It doesn't have a login +// - It is not an invoice room that should be shown +// */ +// if (!isCurrentUserOwnedPolicyExpenseChatThatCouldShow && !includeMultipleParticipantReports && !option.login && !shouldShowInvoiceRoom) { +// return false; +// } + +// // If we're excluding threads, check the report to see if it has a single participant and if the participant is already selected +// if (!includeThreads && ((!!option.login && loginsToExclude[option.login]) || loginsToExclude[option.reportID])) { +// return false; +// } + +// if (action === CONST.IOU.ACTION.CATEGORIZE) { +// const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${option.policyID}`]; +// if (!reportPolicy?.areCategoriesEnabled) { +// return false; +// } +// } +// return true; +// } function getValidReports(reports: OptionList['reports'], config: GetValidReportsConfig): GetValidReportsReturnTypeCombined { const { From 9b2273314431daa56eea521590c55c332652e655 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Wed, 10 Sep 2025 10:06:43 +0300 Subject: [PATCH 03/25] wip --- src/libs/SidebarUtils.ts | 42 ++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 2d6a997936024..75826b8fa8436 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -268,18 +268,31 @@ function getReportsToDisplayInLHN( return reportsToDisplay; } -function updateReportsToDisplayInLHN( - displayedReports: ReportsToDisplayInLHN, - reports: OnyxCollection, - updatedReportsKeys: string[], - currentReportId: string | undefined, - isInFocusMode: boolean, - betas: OnyxEntry, - policies: OnyxCollection, - transactionViolations: OnyxCollection, - reportNameValuePairs?: OnyxCollection, - reportAttributes?: ReportAttributesDerivedValue['reports'], -) { +type UpdateReportsToDisplayInLHNProps = { + displayedReports: ReportsToDisplayInLHN; + reports: OnyxCollection; + updatedReportsKeys: string[]; + currentReportId: string | undefined; + isInFocusMode: boolean; + betas: OnyxEntry; + transactionViolations: OnyxCollection; + reportNameValuePairs?: OnyxCollection; + reportAttributes?: ReportAttributesDerivedValue['reports']; + draftComment: string | null; +}; + +function updateReportsToDisplayInLHN({ + displayedReports, + reports, + updatedReportsKeys, + currentReportId, + isInFocusMode, + betas, + transactionViolations, + reportNameValuePairs, + reportAttributes, + draftComment, +}: UpdateReportsToDisplayInLHNProps) { const displayedReportsCopy = {...displayedReports}; updatedReportsKeys.forEach((reportID) => { const report = reports?.[reportID]; @@ -294,7 +307,8 @@ function updateReportsToDisplayInLHN( isInFocusMode, betas, transactionViolations, - isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]), + draftComment, + isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`] ?? {}), reportAttributes, ); @@ -349,7 +363,7 @@ function categorizeReportsForLHN( const isPinned = !!report.isPinned; const requiresAttention = !!reportAttributes?.[reportID]?.requiresAttention; - const hasDraft = reportID ? hasValidDraftComment(reportID) : false; + const hasDraft = reportID ? hasValidDraftComment(reportID, draftComment) : false; const reportNameValuePairsKey = `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`; const rNVPs = reportNameValuePairs?.[reportNameValuePairsKey]; const isArchived = isArchivedNonExpenseReport(report, !!rNVPs?.private_isArchived); From 8db51eb986802c39c9be63e56053f1ec70edf5e8 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Wed, 10 Sep 2025 18:06:52 +0300 Subject: [PATCH 04/25] wip --- src/libs/DraftCommentUtils.ts | 4 +-- src/libs/ReportUtils.ts | 2 +- src/libs/SidebarUtils.ts | 9 ++++--- src/libs/UnreadIndicatorUpdater/index.ts | 3 ++- src/pages/Debug/Report/DebugReportPage.tsx | 2 ++ tests/unit/DebugUtilsTest.ts | 29 +++++++++++++++++---- tests/unit/ReportUtilsTest.ts | 18 +++++++++++++ tests/unit/SidebarUtilsTest.ts | 4 +-- tests/unit/useSidebarOrderedReportsTest.tsx | 4 +-- 9 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/libs/DraftCommentUtils.ts b/src/libs/DraftCommentUtils.ts index 28269ea94935c..779a9f394a02a 100644 --- a/src/libs/DraftCommentUtils.ts +++ b/src/libs/DraftCommentUtils.ts @@ -2,14 +2,14 @@ * Returns true if the report has a valid draft comment. * A valid draft comment is a non-empty string. */ -function isValidDraftComment(comment?: string | null): boolean { +function isValidDraftComment(comment?: string): boolean { return !!comment; } /** * Returns true if the report has a valid draft comment. */ -function hasValidDraftComment(reportID: string, draftComment: string | null): boolean { +function hasValidDraftComment(reportID: string, draftComment: string | undefined): boolean { return isValidDraftComment(draftComment); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 87a6b240b39ce..c30dcc61bf25e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8382,7 +8382,7 @@ type ShouldReportBeInOptionListParams = { login?: string; includeDomainEmail?: boolean; isReportArchived?: boolean; - draftComment: string | null; + draftComment: string | undefined; }; function reasonForReportToBeInOptionList({ diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 75826b8fa8436..5f1eef0519e95 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -169,7 +169,7 @@ function shouldDisplayReportInLHN( isInFocusMode: boolean, betas: OnyxEntry, transactionViolations: OnyxCollection, - draftComment: string | null, + draftComment: string | undefined, isReportArchived?: boolean, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { @@ -236,6 +236,7 @@ function getReportsToDisplayInLHN( betas: OnyxEntry, policies: OnyxCollection, priorityMode: OnyxEntry, + draftComment: string | undefined, transactionViolations: OnyxCollection, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], @@ -256,6 +257,7 @@ function getReportsToDisplayInLHN( isInFocusMode, betas, transactionViolations, + draftComment, isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]), reportAttributes, ); @@ -326,7 +328,7 @@ function updateReportsToDisplayInLHN({ */ function categorizeReportsForLHN( reportsToDisplay: ReportsToDisplayInLHN, - draftComment: string | null, + draftComment: string | undefined, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { @@ -487,6 +489,7 @@ function sortReportsToDisplayInLHN( reportsToDisplay: ReportsToDisplayInLHN, priorityMode: OnyxEntry, localeCompare: LocaleContextProps['localeCompare'], + draftComment: string | undefined, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], ): string[] { @@ -506,7 +509,7 @@ function sortReportsToDisplayInLHN( // - Sorted by reportDisplayName in GSD (focus) view mode // Step 1: Categorize reports - const categories = categorizeReportsForLHN(reportsToDisplay, reportNameValuePairs, reportAttributes); + const categories = categorizeReportsForLHN(reportsToDisplay, draftComment, reportNameValuePairs, reportAttributes); // Step 2: Sort each category const sortedCategories = sortCategorizedReports(categories, isInDefaultMode, localeCompare); diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts index 16c76ba2860ef..0933bdcc2eb37 100644 --- a/src/libs/UnreadIndicatorUpdater/index.ts +++ b/src/libs/UnreadIndicatorUpdater/index.ts @@ -37,7 +37,7 @@ Onyx.connect({ }, }); -function getUnreadReportsForUnreadIndicator(reports: OnyxCollection, currentReportID: string | undefined) { +function getUnreadReportsForUnreadIndicator(reports: OnyxCollection, currentReportID: string | undefined, draftComment: string | undefined) { return Object.values(reports ?? {}).filter((report) => { const notificationPreference = ReportUtils.getReportNotificationPreference(report); const chatReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`]; @@ -56,6 +56,7 @@ function getUnreadReportsForUnreadIndicator(reports: OnyxCollection, cur isInFocusMode: false, excludeEmptyChats: false, isReportArchived, + draftComment, }) && /** * Chats with hidden preference remain invisible in the LHN and are not considered "unread." diff --git a/src/pages/Debug/Report/DebugReportPage.tsx b/src/pages/Debug/Report/DebugReportPage.tsx index fbfb89e6d2d81..e8829b50c5a6e 100644 --- a/src/pages/Debug/Report/DebugReportPage.tsx +++ b/src/pages/Debug/Report/DebugReportPage.tsx @@ -60,6 +60,7 @@ function DebugReportPage({ selector: (attributes) => attributes?.reports?.[reportID], canBeMissing: true, }); + const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true}); const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE, {canBeMissing: true}); const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const transactionID = DebugUtils.getTransactionID(report, reportActions); @@ -95,6 +96,7 @@ function DebugReportPage({ hasRBR, isReportArchived, isInFocusMode: priorityMode === CONST.PRIORITY_MODE.GSD, + draftComment, }); return [ diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index c3346c7269225..340f1830404c1 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -735,12 +735,12 @@ describe('DebugUtils', () => { Onyx.clear(); }); it('returns null when report is not defined', () => { - const reason = DebugUtils.getReasonForShowingRowInLHN({report: undefined, chatReport: chatReportR14932, doesReportHaveViolations: false}); + const reason = DebugUtils.getReasonForShowingRowInLHN({report: undefined, chatReport: chatReportR14932, doesReportHaveViolations: false, draftComment: ''}); expect(reason).toBeNull(); }); it('returns correct reason when report has a valid draft comment', async () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}1`, 'Hello world!'); - const reason = DebugUtils.getReasonForShowingRowInLHN({report: baseReport, chatReport: chatReportR14932, doesReportHaveViolations: false}); + const reason = DebugUtils.getReasonForShowingRowInLHN({report: baseReport, chatReport: chatReportR14932, doesReportHaveViolations: false, draftComment: ''}); expect(reason).toBe('debug.reasonVisibleInLHN.hasDraftComment'); }); it('returns correct reason when report has GBR', () => { @@ -752,6 +752,7 @@ describe('DebugUtils', () => { }, chatReport: chatReportR14932, doesReportHaveViolations: false, + draftComment: '', }); expect(reason).toBe('debug.reasonVisibleInLHN.hasGBR'); }); @@ -763,6 +764,7 @@ describe('DebugUtils', () => { }, chatReport: chatReportR14932, doesReportHaveViolations: false, + draftComment: '', }); expect(reason).toBe('debug.reasonVisibleInLHN.pinnedByUser'); }); @@ -778,6 +780,7 @@ describe('DebugUtils', () => { }, chatReport: chatReportR14932, doesReportHaveViolations: false, + draftComment: '', }); expect(reason).toBe('debug.reasonVisibleInLHN.hasAddWorkspaceRoomErrors'); }); @@ -801,6 +804,7 @@ describe('DebugUtils', () => { chatReport: chatReportR14932, isInFocusMode: true, doesReportHaveViolations: false, + draftComment: '', }); expect(reason).toBe('debug.reasonVisibleInLHN.isUnread'); }); @@ -818,6 +822,7 @@ describe('DebugUtils', () => { hasRBR: false, isReportArchived: isReportArchived.current, doesReportHaveViolations: false, + draftComment: '', }); expect(reason).toBe('debug.reasonVisibleInLHN.isArchived'); }); @@ -829,6 +834,7 @@ describe('DebugUtils', () => { }, chatReport: chatReportR14932, doesReportHaveViolations: false, + draftComment: '', }); expect(reason).toBe('debug.reasonVisibleInLHN.isSelfDM'); }); @@ -837,6 +843,7 @@ describe('DebugUtils', () => { report: baseReport, chatReport: chatReportR14932, doesReportHaveViolations: false, + draftComment: '', }); expect(reason).toBe('debug.reasonVisibleInLHN.isFocused'); }); @@ -889,7 +896,13 @@ describe('DebugUtils', () => { reportID: '1', }, }); - const reason = DebugUtils.getReasonForShowingRowInLHN({report: MOCK_TRANSACTION_REPORT, chatReport: chatReportR14932, hasRBR: true, doesReportHaveViolations: true}); + const reason = DebugUtils.getReasonForShowingRowInLHN({ + report: MOCK_TRANSACTION_REPORT, + chatReport: chatReportR14932, + hasRBR: true, + doesReportHaveViolations: true, + draftComment: '', + }); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); }); it('returns correct reason when report has violations', async () => { @@ -941,11 +954,17 @@ describe('DebugUtils', () => { reportID: '1', }, }); - const reason = DebugUtils.getReasonForShowingRowInLHN({report: MOCK_EXPENSE_REPORT, chatReport: chatReportR14932, hasRBR: true, doesReportHaveViolations: true}); + const reason = DebugUtils.getReasonForShowingRowInLHN({ + report: MOCK_EXPENSE_REPORT, + chatReport: chatReportR14932, + hasRBR: true, + doesReportHaveViolations: true, + draftComment: '', + }); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); }); it('returns correct reason when report has errors', () => { - const reason = DebugUtils.getReasonForShowingRowInLHN({report: baseReport, chatReport: chatReportR14932, hasRBR: true, doesReportHaveViolations: false}); + const reason = DebugUtils.getReasonForShowingRowInLHN({report: baseReport, chatReport: chatReportR14932, hasRBR: true, doesReportHaveViolations: false, draftComment: ''}); expect(reason).toBe('debug.reasonVisibleInLHN.hasRBR'); }); }); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 4f112008695d7..f1ad11d7eb93a 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -2441,6 +2441,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: false, excludeEmptyChats: false, + draftComment: '', }), ).toBeTruthy(); }); @@ -2492,6 +2493,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: true, excludeEmptyChats: false, + draftComment: '', }), ).toBeTruthy(); }); @@ -2513,6 +2515,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: false, excludeEmptyChats: false, + draftComment: '', }), ).toBeTruthy(); }); @@ -2534,6 +2537,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: false, excludeEmptyChats: false, + draftComment: 'fake draft', }), ).toBeTruthy(); }); @@ -2555,6 +2559,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: false, excludeEmptyChats: false, + draftComment: '', }), ).toBeTruthy(); }); @@ -2589,6 +2594,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: false, excludeEmptyChats: false, + draftComment: '', }), ).toBeTruthy(); }); @@ -2619,6 +2625,7 @@ describe('ReportUtils', () => { doesReportHaveViolations: false, excludeEmptyChats: false, isReportArchived: isReportArchived.current, + draftComment: '', }), ).toBeTruthy(); }); @@ -2649,6 +2656,7 @@ describe('ReportUtils', () => { doesReportHaveViolations: false, excludeEmptyChats: false, isReportArchived: isReportArchived.current, + draftComment: '', }), ).toBeFalsy(); }); @@ -2672,6 +2680,7 @@ describe('ReportUtils', () => { doesReportHaveViolations: false, excludeEmptyChats: false, includeSelfDM, + draftComment: '', }), ).toBeTruthy(); }); @@ -2697,6 +2706,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: false, excludeEmptyChats: false, + draftComment: '', }), ).toBeFalsy(); }); @@ -2715,6 +2725,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: false, excludeEmptyChats: false, + draftComment: '', }), ).toBeFalsy(); }); @@ -2736,6 +2747,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: false, excludeEmptyChats: false, + draftComment: '', }), ).toBeFalsy(); }); @@ -2777,6 +2789,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: false, excludeEmptyChats: false, + draftComment: '', }), ).toBeFalsy(); }); @@ -2795,6 +2808,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: false, excludeEmptyChats: true, + draftComment: '', }), ).toBeFalsy(); }); @@ -2815,6 +2829,7 @@ describe('ReportUtils', () => { login: '+@domain.com', excludeEmptyChats: false, includeDomainEmail: false, + draftComment: '', }), ).toBeFalsy(); }); @@ -2859,6 +2874,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: false, excludeEmptyChats: false, + draftComment: '', }), ).toBeFalsy(); }); @@ -2877,6 +2893,7 @@ describe('ReportUtils', () => { betas, doesReportHaveViolations: false, excludeEmptyChats: false, + draftComment: '', }), ).toBeFalsy(); }); @@ -2909,6 +2926,7 @@ describe('ReportUtils', () => { betas: [], doesReportHaveViolations: false, excludeEmptyChats: true, + draftComment: '', }), ).toBeFalsy(); }); diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index 784cb89fb4929..058a3cacb6ffb 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -1588,7 +1588,7 @@ describe('SidebarUtils', () => { }; // When the reports are categorized - const result = SidebarUtils.categorizeReportsForLHN(reports); + const result = SidebarUtils.categorizeReportsForLHN(reports, ''); // Then the reports are categorized into the correct groups expect(result.pinnedAndGBRReports).toHaveLength(0); @@ -1602,7 +1602,7 @@ describe('SidebarUtils', () => { it('should handle empty reports object', () => { // Given the reports are empty - const result = SidebarUtils.categorizeReportsForLHN({}); + const result = SidebarUtils.categorizeReportsForLHN({}, ''); // Then the reports are categorized into the correct groups expect(result.pinnedAndGBRReports).toHaveLength(0); diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index 4f660b3861e0e..bfec0d7bc3860 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -64,7 +64,7 @@ describe('useSidebarOrderedReports', () => { // Default mock implementations mockSidebarUtils.getReportsToDisplayInLHN.mockImplementation(() => ({})); - mockSidebarUtils.updateReportsToDisplayInLHN.mockImplementation((prev) => prev); + mockSidebarUtils.updateReportsToDisplayInLHN.mockImplementation(({displayedReports}) => ({...displayedReports})); mockSidebarUtils.sortReportsToDisplayInLHN.mockReturnValue([]); return waitForBatchedUpdates(); @@ -112,7 +112,7 @@ describe('useSidebarOrderedReports', () => { // When the initial reports are set const initialReports = createMockReports(reportsContent); mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(initialReports); - mockSidebarUtils.updateReportsToDisplayInLHN.mockImplementation((prev) => ({...prev})); + mockSidebarUtils.updateReportsToDisplayInLHN.mockImplementation(({displayedReports}) => ({...displayedReports})); currentReportIDForTestsValue = '1'; // When the hook is rendered From 226af7c382c322f51ed662c5c7b538101bab1d8f Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Wed, 10 Sep 2025 18:38:25 +0300 Subject: [PATCH 05/25] wip --- src/hooks/useSidebarOrderedReports.tsx | 21 +- src/libs/DebugUtils.ts | 2 +- src/libs/OptionsListUtils/index.ts | 273 +++++++++--------- src/libs/SidebarUtils.ts | 2 +- src/libs/UnreadIndicatorUpdater/index.ts | 2 +- .../ComposerWithSuggestions.tsx | 16 +- tests/perf-test/ReportUtils.perf-test.ts | 4 +- tests/perf-test/SidebarUtils.perf-test.ts | 4 +- tests/unit/SidebarUtilsTest.ts | 10 +- tests/unit/UnreadIndicatorUpdaterTest.ts | 6 +- 10 files changed, 174 insertions(+), 166 deletions(-) diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index 1299c9430084e..efadc90a0cdb4 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -72,12 +72,12 @@ function SidebarOrderedReportsContextProvider({ const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: (value) => value?.reports, canBeMissing: true}); const [currentReportsToDisplay, setCurrentReportsToDisplay] = useState({}); - const {shouldUseNarrowLayout} = useResponsiveLayout(); const {accountID} = useCurrentUserPersonalDetails(); const currentReportIDValue = useCurrentReportID(); const derivedCurrentReportID = currentReportIDForTests ?? currentReportIDValue?.currentReportIDFromPath ?? currentReportIDValue?.currentReportID; const prevDerivedCurrentReportID = usePrevious(derivedCurrentReportID); + const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${derivedCurrentReportID}`, {canBeMissing: true}); const policyMemberAccountIDs = useMemo(() => getPolicyEmployeeListByIdWithoutCurrentUser(policies, undefined, accountID), [policies, accountID]); const prevBetas = usePrevious(betas); @@ -146,18 +146,18 @@ function SidebarOrderedReportsContextProvider({ let reportsToDisplay = {}; if (shouldDoIncrementalUpdate) { - reportsToDisplay = SidebarUtils.updateReportsToDisplayInLHN( - currentReportsToDisplay, - chatReports, - updatedReports, - derivedCurrentReportID, - priorityMode === CONST.PRIORITY_MODE.GSD, + reportsToDisplay = SidebarUtils.updateReportsToDisplayInLHN({ + displayedReports: currentReportsToDisplay, + reports: chatReports, + updatedReportsKeys: updatedReports, + currentReportId: derivedCurrentReportID, + isInFocusMode: priorityMode === CONST.PRIORITY_MODE.GSD, betas, - policies, transactionViolations, reportNameValuePairs, reportAttributes, - ); + draftComment, + }); } else { reportsToDisplay = SidebarUtils.getReportsToDisplayInLHN( derivedCurrentReportID, @@ -165,6 +165,7 @@ function SidebarOrderedReportsContextProvider({ betas, policies, priorityMode, + draftComment, transactionViolations, reportNameValuePairs, reportAttributes, @@ -182,7 +183,7 @@ function SidebarOrderedReportsContextProvider({ }, [reportsToDisplayInLHN]); const getOrderedReportIDs = useCallback( - () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReportsToDisplayInLHN ?? {}, priorityMode, localeCompare, reportNameValuePairs, reportAttributes), + () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReportsToDisplayInLHN ?? {}, priorityMode, localeCompare, draftComment, reportNameValuePairs, reportAttributes), // Rule disabled intentionally - reports should be sorted only when the reportsToDisplayInLHN changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps [reportsToDisplayInLHN, localeCompare], diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 1ff8aec85865a..724b77510aa67 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -1323,7 +1323,7 @@ function getReasonForShowingRowInLHN({ isReportArchived?: boolean; isInFocusMode?: boolean; betas?: OnyxEntry; - draftComment: string | null; + draftComment: string | undefined; }): TranslationPaths | null { if (!report) { return null; diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index fc918626a4c80..2abac801f3cf6 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -114,7 +114,6 @@ import { isInvoiceRoom, isMoneyRequest, isPolicyAdmin, - isValidReport, isAdminRoom as reportUtilsIsAdminRoom, isAnnounceRoom as reportUtilsIsAnnounceRoom, isChatReport as reportUtilsIsChatReport, @@ -1489,129 +1488,130 @@ function getUserToInviteContactOption({ return userToInvite; } -// function isValidReport(option: SearchOption, config: GetValidReportsConfig): boolean { -// const { -// betas = [], -// includeMultipleParticipantReports = false, -// includeOwnedWorkspaceChats = false, -// includeThreads = false, -// includeTasks = false, -// includeMoneyRequests = false, -// includeReadOnly = true, -// transactionViolations = {}, -// includeSelfDM = false, -// includeInvoiceRooms = false, -// action, -// includeP2P = true, -// includeDomainEmail = false, -// loginsToExclude = {}, -// excludeNonAdminWorkspaces, -// } = config; -// const topmostReportId = Navigation.getTopmostReportId(); - -// const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${option.item.chatReportID}`]; -// const doesReportHaveViolations = shouldDisplayViolationsRBRInLHN(option.item, transactionViolations); - -// const shouldBeInOptionList = shouldReportBeInOptionList({ -// report: option.item, -// chatReport, -// currentReportId: topmostReportId, -// betas, -// doesReportHaveViolations, -// isInFocusMode: false, -// excludeEmptyChats: false, -// includeSelfDM, -// login: option.login, -// includeDomainEmail, -// isReportArchived: !!option.private_isArchived,, -// }); - -// if (!shouldBeInOptionList) { -// return false; -// } - -// const isThread = option.isThread; -// const isTaskReport = option.isTaskReport; -// const isPolicyExpenseChat = option.isPolicyExpenseChat; -// const isMoneyRequestReport = option.isMoneyRequestReport; -// const isSelfDM = option.isSelfDM; -// const isChatRoom = option.isChatRoom; -// const accountIDs = getParticipantsAccountIDsForDisplay(option.item); - -// if (excludeNonAdminWorkspaces && !isPolicyAdmin(option.policyID, policies)) { -// return false; -// } - -// if (isPolicyExpenseChat && !includeOwnedWorkspaceChats) { -// return false; -// } -// // When passing includeP2P false we are trying to hide features from users that are not ready for P2P and limited to expense chats only. -// if (!includeP2P && !isPolicyExpenseChat) { -// return false; -// } - -// if (isSelfDM && !includeSelfDM) { -// return false; -// } - -// if (isThread && !includeThreads) { -// return false; -// } - -// if (isTaskReport && !includeTasks) { -// return false; -// } - -// if (isMoneyRequestReport && !includeMoneyRequests) { -// return false; -// } - -// if (!canUserPerformWriteAction(option.item, !!option.private_isArchived) && !includeReadOnly) { -// return false; -// } - -// // In case user needs to add credit bank account, don't allow them to submit an expense from the workspace. -// if (includeOwnedWorkspaceChats && hasIOUWaitingOnCurrentUserBankAccount(option.item)) { -// return false; -// } - -// if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) { -// return false; -// } - -// if (option.login === CONST.EMAIL.NOTIFICATIONS) { -// return false; -// } -// const isCurrentUserOwnedPolicyExpenseChatThatCouldShow = -// option.isPolicyExpenseChat && option.ownerAccountID === currentUserAccountID && includeOwnedWorkspaceChats && !option.private_isArchived; - -// const shouldShowInvoiceRoom = -// includeInvoiceRooms && isInvoiceRoom(option.item) && isPolicyAdmin(option.policyID, policies) && !option.private_isArchived && canSendInvoiceFromWorkspace(option.policyID); - -// /* -// Exclude the report option if it doesn't meet any of the following conditions: -// - It is not an owned policy expense chat that could be shown -// - Multiple participant reports are not included -// - It doesn't have a login -// - It is not an invoice room that should be shown -// */ -// if (!isCurrentUserOwnedPolicyExpenseChatThatCouldShow && !includeMultipleParticipantReports && !option.login && !shouldShowInvoiceRoom) { -// return false; -// } - -// // If we're excluding threads, check the report to see if it has a single participant and if the participant is already selected -// if (!includeThreads && ((!!option.login && loginsToExclude[option.login]) || loginsToExclude[option.reportID])) { -// return false; -// } - -// if (action === CONST.IOU.ACTION.CATEGORIZE) { -// const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${option.policyID}`]; -// if (!reportPolicy?.areCategoriesEnabled) { -// return false; -// } -// } -// return true; -// } +function isValidReport(option: SearchOption, config: GetValidReportsConfig, draftComment: string | undefined): boolean { + const { + betas = [], + includeMultipleParticipantReports = false, + includeOwnedWorkspaceChats = false, + includeThreads = false, + includeTasks = false, + includeMoneyRequests = false, + includeReadOnly = true, + transactionViolations = {}, + includeSelfDM = false, + includeInvoiceRooms = false, + action, + includeP2P = true, + includeDomainEmail = false, + loginsToExclude = {}, + excludeNonAdminWorkspaces, + } = config; + const topmostReportId = Navigation.getTopmostReportId(); + + const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${option.item.chatReportID}`]; + const doesReportHaveViolations = shouldDisplayViolationsRBRInLHN(option.item, transactionViolations); + + const shouldBeInOptionList = shouldReportBeInOptionList({ + report: option.item, + chatReport, + currentReportId: topmostReportId, + betas, + doesReportHaveViolations, + isInFocusMode: false, + excludeEmptyChats: false, + includeSelfDM, + login: option.login, + includeDomainEmail, + isReportArchived: !!option.private_isArchived, + draftComment, + }); + + if (!shouldBeInOptionList) { + return false; + } + + const isThread = option.isThread; + const isTaskReport = option.isTaskReport; + const isPolicyExpenseChat = option.isPolicyExpenseChat; + const isMoneyRequestReport = option.isMoneyRequestReport; + const isSelfDM = option.isSelfDM; + const isChatRoom = option.isChatRoom; + const accountIDs = getParticipantsAccountIDsForDisplay(option.item); + + if (excludeNonAdminWorkspaces && !isPolicyAdmin(option.policyID, policies)) { + return false; + } + + if (isPolicyExpenseChat && !includeOwnedWorkspaceChats) { + return false; + } + // When passing includeP2P false we are trying to hide features from users that are not ready for P2P and limited to expense chats only. + if (!includeP2P && !isPolicyExpenseChat) { + return false; + } + + if (isSelfDM && !includeSelfDM) { + return false; + } + + if (isThread && !includeThreads) { + return false; + } + + if (isTaskReport && !includeTasks) { + return false; + } + + if (isMoneyRequestReport && !includeMoneyRequests) { + return false; + } + + if (!canUserPerformWriteAction(option.item, !!option.private_isArchived) && !includeReadOnly) { + return false; + } + + // In case user needs to add credit bank account, don't allow them to submit an expense from the workspace. + if (includeOwnedWorkspaceChats && hasIOUWaitingOnCurrentUserBankAccount(option.item)) { + return false; + } + + if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) { + return false; + } + + if (option.login === CONST.EMAIL.NOTIFICATIONS) { + return false; + } + const isCurrentUserOwnedPolicyExpenseChatThatCouldShow = + option.isPolicyExpenseChat && option.ownerAccountID === currentUserAccountID && includeOwnedWorkspaceChats && !option.private_isArchived; + + const shouldShowInvoiceRoom = + includeInvoiceRooms && isInvoiceRoom(option.item) && isPolicyAdmin(option.policyID, policies) && !option.private_isArchived && canSendInvoiceFromWorkspace(option.policyID); + + /* + Exclude the report option if it doesn't meet any of the following conditions: + - It is not an owned policy expense chat that could be shown + - Multiple participant reports are not included + - It doesn't have a login + - It is not an invoice room that should be shown + */ + if (!isCurrentUserOwnedPolicyExpenseChatThatCouldShow && !includeMultipleParticipantReports && !option.login && !shouldShowInvoiceRoom) { + return false; + } + + // If we're excluding threads, check the report to see if it has a single participant and if the participant is already selected + if (!includeThreads && ((!!option.login && loginsToExclude[option.login]) || loginsToExclude[option.reportID])) { + return false; + } + + if (action === CONST.IOU.ACTION.CATEGORIZE) { + const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${option.policyID}`]; + if (!reportPolicy?.areCategoriesEnabled) { + return false; + } + } + return true; +} function getValidReports(reports: OptionList['reports'], config: GetValidReportsConfig): GetValidReportsReturnTypeCombined { const { @@ -1750,6 +1750,7 @@ function getValidOptions( includeUserToInvite = false, ...config }: GetOptionsConfig = {}, + draftComment: string | undefined = undefined, ): Options { const restrictedLogins = getRestrictedLogins(config, options, canShowManagerMcTest); @@ -1800,16 +1801,20 @@ function getValidOptions( return false; } - return isValidReport(report, { - ...getValidReportsConfig, - includeP2P, - includeDomainEmail, - selectedOptions, - loginsToExclude, - shouldBoldTitleByDefault, - shouldSeparateSelfDMChat, - shouldSeparateWorkspaceChat, - }); + return isValidReport( + report, + { + ...getValidReportsConfig, + includeP2P, + includeDomainEmail, + selectedOptions, + loginsToExclude, + shouldBoldTitleByDefault, + shouldSeparateSelfDMChat, + shouldSeparateWorkspaceChat, + }, + draftComment, + ); }; filteredReports = optionsOrderBy(options.reports, recentReportComparator, maxElements, filteringFunction); diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 5f1eef0519e95..9088a3eab6454 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -280,7 +280,7 @@ type UpdateReportsToDisplayInLHNProps = { transactionViolations: OnyxCollection; reportNameValuePairs?: OnyxCollection; reportAttributes?: ReportAttributesDerivedValue['reports']; - draftComment: string | null; + draftComment: string | undefined; }; function updateReportsToDisplayInLHN({ diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts index 0933bdcc2eb37..e44174b79d480 100644 --- a/src/libs/UnreadIndicatorUpdater/index.ts +++ b/src/libs/UnreadIndicatorUpdater/index.ts @@ -78,7 +78,7 @@ const triggerUnreadUpdate = debounce(() => { const currentReportID = navigationRef?.isReady?.() ? Navigation.getTopmostReportId() : undefined; // We want to keep notification count consistent with what can be accessed from the LHN list - const unreadReports = memoizedGetUnreadReportsForUnreadIndicator(allReports, currentReportID); + const unreadReports = memoizedGetUnreadReportsForUnreadIndicator(allReports, currentReportID, undefined); updateUnread(unreadReports.length); }, CONST.TIMING.UNREAD_UPDATE_DEBOUNCE_TIME); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index f01115e02635f..a101761f6bce3 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -253,7 +253,7 @@ function ComposerWithSuggestions( return draftComment; }); - const commentRef = useRef(value); + const commentRef = useRef(value ?? ''); const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: true}); const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {selector: getPreferredSkinToneIndex, canBeMissing: true}); @@ -272,7 +272,7 @@ function ComposerWithSuggestions( const valueRef = useRef(value); valueRef.current = value; - const [selection, setSelection] = useState(() => ({start: value.length, end: value.length, positionX: 0, positionY: 0})); + const [selection, setSelection] = useState(() => ({start: value?.length ?? 0, end: value?.length ?? 0, positionX: 0, positionY: 0})); const [composerHeight, setComposerHeight] = useState(0); @@ -374,7 +374,7 @@ function ComposerWithSuggestions( const updateComment = useCallback( (commentValue: string, shouldDebounceSaveComment?: boolean) => { raiseIsScrollLikelyLayoutTriggered(); - const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); + const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current ?? '', commentValue); const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && containsOnlyEmojis(diff); const commentWithSpaceInserted = isEmojiInserted ? insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue; const {text: newComment, emojis, cursorPosition} = replaceAndExtractEmojis(commentWithSpaceInserted, preferredSkinTone, preferredLocale); @@ -480,12 +480,12 @@ function ComposerWithSuggestions( // Wales flag has 14 UTF-16 code units. This is the emoji with the largest number of UTF-16 code units we use. const start = Math.max(0, selection.start - 14); - const graphemes = Array.from(splitter.segment(lastTextRef.current.substring(start, selection.start))); + const graphemes = Array.from(splitter.segment(lastTextRef.current?.substring(start, selection.start) ?? '')); const lastGrapheme = graphemes.at(graphemes.length - 1); const lastGraphemeLength = lastGrapheme?.segment.length ?? 0; if (lastGraphemeLength > 1) { event.preventDefault(); - const newText = lastTextRef.current.slice(0, selection.start - lastGraphemeLength) + lastTextRef.current.slice(selection.start); + const newText = (lastTextRef.current?.slice(0, selection.start - lastGraphemeLength) ?? '') + (lastTextRef.current?.slice(selection.start) ?? ''); setSelection((prevSelection) => ({ start: selection.start - lastGraphemeLength, end: selection.start - lastGraphemeLength, @@ -703,7 +703,7 @@ function ComposerWithSuggestions( ); useEffect(() => { - onValueChange(value); + onValueChange(value ?? ''); }, [onValueChange, value]); const onLayout = useCallback( @@ -835,7 +835,7 @@ function ComposerWithSuggestions( isGroupPolicyReport={isGroupPolicyReport} policyID={policyID} // Input - value={value} + value={value ?? ''} selection={selection} setSelection={setSelection} resetKeyboardInput={resetKeyboardInput} @@ -844,7 +844,7 @@ function ComposerWithSuggestions( {isValidReportIDFromPath(reportID) && ( { const betas = [CONST.BETAS.DEFAULT_ROOMS]; await waitForBatchedUpdates(); - await measureFunction(() => shouldReportBeInOptionList({report, chatReport, currentReportId, isInFocusMode, betas, doesReportHaveViolations: false, excludeEmptyChats: false})); + await measureFunction(() => + shouldReportBeInOptionList({report, chatReport, currentReportId, isInFocusMode, betas, doesReportHaveViolations: false, excludeEmptyChats: false, draftComment: undefined}), + ); }); test('[ReportUtils] getWorkspaceIcon on 1k policies', async () => { diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 472b969cee935..bdbe804003df0 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -93,11 +93,11 @@ describe('SidebarUtils', () => { test('[SidebarUtils] getReportsToDisplayInLHN on 15k reports for default priorityMode', async () => { await waitForBatchedUpdates(); - await measureFunction(() => SidebarUtils.getReportsToDisplayInLHN(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.DEFAULT, transactionViolations)); + await measureFunction(() => SidebarUtils.getReportsToDisplayInLHN(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.DEFAULT, undefined, transactionViolations)); }); test('[SidebarUtils] getReportsToDisplayInLHN on 15k reports for GSD priorityMode', async () => { await waitForBatchedUpdates(); - await measureFunction(() => SidebarUtils.getReportsToDisplayInLHN(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.GSD, transactionViolations)); + await measureFunction(() => SidebarUtils.getReportsToDisplayInLHN(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.GSD, undefined, transactionViolations)); }); }); diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index 058a3cacb6ffb..c7adf5bb832a0 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -1525,7 +1525,7 @@ describe('SidebarUtils', () => { const {reports, reportNameValuePairs, reportAttributes} = createSidebarTestData(); // When the reports are categorized - const result = SidebarUtils.categorizeReportsForLHN(reports, null, reportNameValuePairs, reportAttributes); + const result = SidebarUtils.categorizeReportsForLHN(reports, undefined, reportNameValuePairs, reportAttributes); // Then the reports are categorized into the correct groups expect(result.pinnedAndGBRReports).toHaveLength(1); @@ -1561,7 +1561,7 @@ describe('SidebarUtils', () => { }; // When the reports are categorized - const result = SidebarUtils.categorizeReportsForLHN(reports, null, undefined, reportAttributes); + const result = SidebarUtils.categorizeReportsForLHN(reports, undefined, undefined, reportAttributes); // Then the reports are categorized into the correct groups expect(result.pinnedAndGBRReports).toHaveLength(1); @@ -1839,7 +1839,7 @@ describe('SidebarUtils', () => { const priorityMode = CONST.PRIORITY_MODE.DEFAULT; // When the reports are sorted - const result = SidebarUtils.sortReportsToDisplayInLHN(reports, priorityMode, mockLocaleCompare); + const result = SidebarUtils.sortReportsToDisplayInLHN(reports, priorityMode, mockLocaleCompare, undefined); // Then the reports are sorted in the correct order expect(result).toEqual(['0', '1', '2']); // Pinned first, Error second, Normal third @@ -1865,10 +1865,10 @@ describe('SidebarUtils', () => { const mockLocaleCompare = (a: string, b: string) => a.localeCompare(b); // When the reports are sorted in default mode - const defaultResult = SidebarUtils.sortReportsToDisplayInLHN(reports, CONST.PRIORITY_MODE.DEFAULT, mockLocaleCompare); + const defaultResult = SidebarUtils.sortReportsToDisplayInLHN(reports, CONST.PRIORITY_MODE.DEFAULT, mockLocaleCompare, undefined); // When the reports are sorted in GSD mode - const gsdResult = SidebarUtils.sortReportsToDisplayInLHN(reports, CONST.PRIORITY_MODE.GSD, mockLocaleCompare); + const gsdResult = SidebarUtils.sortReportsToDisplayInLHN(reports, CONST.PRIORITY_MODE.GSD, mockLocaleCompare, undefined); // Then the reports are sorted in the correct order expect(defaultResult).toEqual(['1', '0']); // Most recent first (index 1 has later date) diff --git a/tests/unit/UnreadIndicatorUpdaterTest.ts b/tests/unit/UnreadIndicatorUpdaterTest.ts index cf8119f3784af..ad238822b8320 100644 --- a/tests/unit/UnreadIndicatorUpdaterTest.ts +++ b/tests/unit/UnreadIndicatorUpdaterTest.ts @@ -29,7 +29,7 @@ describe('UnreadIndicatorUpdaterTest', () => { 3: {reportID: '3', reportName: 'test', type: CONST.REPORT.TYPE.TASK, lastMessageText: 'test'}, }; TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID).then(() => { - expect(UnreadIndicatorUpdater.getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3').length).toBe(2); + expect(UnreadIndicatorUpdater.getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3', undefined).length).toBe(2); }); }); @@ -39,7 +39,7 @@ describe('UnreadIndicatorUpdaterTest', () => { 2: {reportID: '2', type: CONST.REPORT.TYPE.TASK, lastReadTime: '2023-02-05 09:12:05.000', lastVisibleActionCreated: '2023-02-06 07:15:44.030'}, 3: {reportID: '3', type: CONST.REPORT.TYPE.TASK}, }; - expect(UnreadIndicatorUpdater.getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3').length).toBe(0); + expect(UnreadIndicatorUpdater.getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3', undefined).length).toBe(0); }); it('given notification preference of some reports is hidden', () => { @@ -68,7 +68,7 @@ describe('UnreadIndicatorUpdaterTest', () => { 3: {reportID: '3', reportName: 'test', type: CONST.REPORT.TYPE.TASK, lastMessageText: 'test'}, }; TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID).then(() => { - expect(UnreadIndicatorUpdater.getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3').length).toBe(1); + expect(UnreadIndicatorUpdater.getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3', undefined).length).toBe(1); }); }); }); From 046c9120904fe93d5ce8ba56166c91bbc8d9bd61 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Thu, 11 Sep 2025 17:56:37 +0300 Subject: [PATCH 06/25] fix tests --- src/hooks/useSidebarOrderedReports.tsx | 13 ++++++------- src/libs/SidebarUtils.ts | 20 +++++++++++--------- src/libs/UnreadIndicatorUpdater/index.ts | 2 +- src/pages/Debug/Report/DebugReportPage.tsx | 2 +- tests/unit/DebugUtilsTest.ts | 2 +- tests/unit/SidebarUtilsTest.ts | 7 ++++++- tests/unit/useSidebarOrderedReportsTest.tsx | 6 +++--- 7 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index efadc90a0cdb4..6506b572642f6 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -68,7 +68,7 @@ function SidebarOrderedReportsContextProvider({ const [transactions, {sourceValue: transactionsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: true}); const [transactionViolations, {sourceValue: transactionViolationsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [reportNameValuePairs, {sourceValue: reportNameValuePairsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true}); - const [, {sourceValue: reportsDraftsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); + const [reportsDrafts, {sourceValue: reportsDraftsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: (value) => value?.reports, canBeMissing: true}); const [currentReportsToDisplay, setCurrentReportsToDisplay] = useState({}); @@ -77,7 +77,6 @@ function SidebarOrderedReportsContextProvider({ const currentReportIDValue = useCurrentReportID(); const derivedCurrentReportID = currentReportIDForTests ?? currentReportIDValue?.currentReportIDFromPath ?? currentReportIDValue?.currentReportID; const prevDerivedCurrentReportID = usePrevious(derivedCurrentReportID); - const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${derivedCurrentReportID}`, {canBeMissing: true}); const policyMemberAccountIDs = useMemo(() => getPolicyEmployeeListByIdWithoutCurrentUser(policies, undefined, accountID), [policies, accountID]); const prevBetas = usePrevious(betas); @@ -156,7 +155,7 @@ function SidebarOrderedReportsContextProvider({ transactionViolations, reportNameValuePairs, reportAttributes, - draftComment, + reportsDrafts, }); } else { reportsToDisplay = SidebarUtils.getReportsToDisplayInLHN( @@ -165,7 +164,7 @@ function SidebarOrderedReportsContextProvider({ betas, policies, priorityMode, - draftComment, + reportsDrafts, transactionViolations, reportNameValuePairs, reportAttributes, @@ -174,7 +173,7 @@ function SidebarOrderedReportsContextProvider({ return reportsToDisplay; // Rule disabled intentionally — triggering a re-render on currentReportsToDisplay would cause an infinite loop // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes]); + }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes, reportsDrafts]); const deepComparedReportsToDisplayInLHN = useDeepCompareRef(reportsToDisplayInLHN); @@ -183,10 +182,10 @@ function SidebarOrderedReportsContextProvider({ }, [reportsToDisplayInLHN]); const getOrderedReportIDs = useCallback( - () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReportsToDisplayInLHN ?? {}, priorityMode, localeCompare, draftComment, reportNameValuePairs, reportAttributes), + () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReportsToDisplayInLHN ?? {}, priorityMode, localeCompare, reportsDrafts, reportNameValuePairs, reportAttributes), // Rule disabled intentionally - reports should be sorted only when the reportsToDisplayInLHN changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - [reportsToDisplayInLHN, localeCompare], + [reportsToDisplayInLHN, localeCompare, reportsDrafts], ); const orderedReportIDs = useMemo(() => getOrderedReportIDs(), [getOrderedReportIDs]); diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 9088a3eab6454..29669fab20277 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -169,7 +169,7 @@ function shouldDisplayReportInLHN( isInFocusMode: boolean, betas: OnyxEntry, transactionViolations: OnyxCollection, - draftComment: string | undefined, + reportsDrafts: OnyxCollection | undefined, isReportArchived?: boolean, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { @@ -201,6 +201,7 @@ function shouldDisplayReportInLHN( // Check if report should override hidden status const isSystemChat = isSystemChatUtil(report); + const draftComment = reportsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`]; const shouldOverrideHidden = hasValidDraftComment(report.reportID, draftComment) || hasErrorsOtherThanFailedReceipt || @@ -236,7 +237,7 @@ function getReportsToDisplayInLHN( betas: OnyxEntry, policies: OnyxCollection, priorityMode: OnyxEntry, - draftComment: string | undefined, + reportsDrafts: OnyxCollection | undefined, transactionViolations: OnyxCollection, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], @@ -257,7 +258,7 @@ function getReportsToDisplayInLHN( isInFocusMode, betas, transactionViolations, - draftComment, + reportsDrafts, isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]), reportAttributes, ); @@ -280,7 +281,7 @@ type UpdateReportsToDisplayInLHNProps = { transactionViolations: OnyxCollection; reportNameValuePairs?: OnyxCollection; reportAttributes?: ReportAttributesDerivedValue['reports']; - draftComment: string | undefined; + reportsDrafts: OnyxCollection | undefined; }; function updateReportsToDisplayInLHN({ @@ -293,7 +294,7 @@ function updateReportsToDisplayInLHN({ transactionViolations, reportNameValuePairs, reportAttributes, - draftComment, + reportsDrafts, }: UpdateReportsToDisplayInLHNProps) { const displayedReportsCopy = {...displayedReports}; updatedReportsKeys.forEach((reportID) => { @@ -309,7 +310,7 @@ function updateReportsToDisplayInLHN({ isInFocusMode, betas, transactionViolations, - draftComment, + reportsDrafts, isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`] ?? {}), reportAttributes, ); @@ -328,7 +329,7 @@ function updateReportsToDisplayInLHN({ */ function categorizeReportsForLHN( reportsToDisplay: ReportsToDisplayInLHN, - draftComment: string | undefined, + reportsDrafts: OnyxCollection | undefined, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { @@ -365,6 +366,7 @@ function categorizeReportsForLHN( const isPinned = !!report.isPinned; const requiresAttention = !!reportAttributes?.[reportID]?.requiresAttention; + const draftComment = reportsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]; const hasDraft = reportID ? hasValidDraftComment(reportID, draftComment) : false; const reportNameValuePairsKey = `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`; const rNVPs = reportNameValuePairs?.[reportNameValuePairsKey]; @@ -489,7 +491,7 @@ function sortReportsToDisplayInLHN( reportsToDisplay: ReportsToDisplayInLHN, priorityMode: OnyxEntry, localeCompare: LocaleContextProps['localeCompare'], - draftComment: string | undefined, + reportsDrafts: OnyxCollection | undefined, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], ): string[] { @@ -509,7 +511,7 @@ function sortReportsToDisplayInLHN( // - Sorted by reportDisplayName in GSD (focus) view mode // Step 1: Categorize reports - const categories = categorizeReportsForLHN(reportsToDisplay, draftComment, reportNameValuePairs, reportAttributes); + const categories = categorizeReportsForLHN(reportsToDisplay, reportsDrafts, reportNameValuePairs, reportAttributes); // Step 2: Sort each category const sortedCategories = sortCategorizedReports(categories, isInDefaultMode, localeCompare); diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts index e44174b79d480..cc0259093b260 100644 --- a/src/libs/UnreadIndicatorUpdater/index.ts +++ b/src/libs/UnreadIndicatorUpdater/index.ts @@ -72,7 +72,7 @@ function getUnreadReportsForUnreadIndicator(reports: OnyxCollection, cur }); } -const memoizedGetUnreadReportsForUnreadIndicator = memoize(getUnreadReportsForUnreadIndicator, {maxArgs: 1}); +const memoizedGetUnreadReportsForUnreadIndicator = memoize(getUnreadReportsForUnreadIndicator, {maxArgs: 3}); const triggerUnreadUpdate = debounce(() => { const currentReportID = navigationRef?.isReady?.() ? Navigation.getTopmostReportId() : undefined; diff --git a/src/pages/Debug/Report/DebugReportPage.tsx b/src/pages/Debug/Report/DebugReportPage.tsx index e8829b50c5a6e..fad431b44ea33 100644 --- a/src/pages/Debug/Report/DebugReportPage.tsx +++ b/src/pages/Debug/Report/DebugReportPage.tsx @@ -96,7 +96,7 @@ function DebugReportPage({ hasRBR, isReportArchived, isInFocusMode: priorityMode === CONST.PRIORITY_MODE.GSD, - draftComment, + draftComment: draftComment, }); return [ diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index 340f1830404c1..a5d74f049d784 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -740,7 +740,7 @@ describe('DebugUtils', () => { }); it('returns correct reason when report has a valid draft comment', async () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}1`, 'Hello world!'); - const reason = DebugUtils.getReasonForShowingRowInLHN({report: baseReport, chatReport: chatReportR14932, doesReportHaveViolations: false, draftComment: ''}); + const reason = DebugUtils.getReasonForShowingRowInLHN({report: baseReport, chatReport: chatReportR14932, doesReportHaveViolations: false, draftComment: 'Hello world!'}); expect(reason).toBe('debug.reasonVisibleInLHN.hasDraftComment'); }); it('returns correct reason when report has GBR', () => { diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index c7adf5bb832a0..1360213516ff5 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -1524,8 +1524,13 @@ describe('SidebarUtils', () => { const {reports, reportNameValuePairs, reportAttributes} = createSidebarTestData(); + // Given reportsDrafts contains a draft comment for report '2' + const reportsDrafts = { + [`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}2`]: 'test', + }; + // When the reports are categorized - const result = SidebarUtils.categorizeReportsForLHN(reports, undefined, reportNameValuePairs, reportAttributes); + const result = SidebarUtils.categorizeReportsForLHN(reports, reportsDrafts, reportNameValuePairs, reportAttributes); // Then the reports are categorized into the correct groups expect(result.pinnedAndGBRReports).toHaveLength(1); diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index bfec0d7bc3860..36caaec31608e 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -58,6 +58,7 @@ describe('useSidebarOrderedReports', () => { [ONYXKEYS.COLLECTION.TRANSACTION]: {}, [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: {}, [ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS]: {}, + [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: {}, [ONYXKEYS.BETAS]: [], [ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: {reports: {}}, } as unknown as OnyxMultiSetInput); @@ -181,6 +182,7 @@ describe('useSidebarOrderedReports', () => { updatedReports, expect.any(String), // priorityMode expect.any(Function), // localeCompare + expect.any(Object), // reportsDrafts expect.any(Object), // reportNameValuePairs expect.any(Object), // reportAttributes ); @@ -232,16 +234,14 @@ describe('useSidebarOrderedReports', () => { mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(newReportsWithSameContent); rerender({}); - currentReportIDForTestsValue = '2'; // When the mock is updated const thirdReportsWithSameContent = createMockReports(reportsContent); mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(thirdReportsWithSameContent); rerender({}); - currentReportIDForTestsValue = '3'; - // Then sortReportsToDisplayInLHN should be called only once (initial render) + // Then sortReportsToDisplayInLHN should be called only once (initial render) since the report content is the same expect(mockSidebarUtils.sortReportsToDisplayInLHN).toHaveBeenCalledTimes(1); }); From fd11513ccf807e626dff497fae5505b112c06421 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Mon, 15 Sep 2025 08:59:16 +0300 Subject: [PATCH 07/25] ts and lint fixes --- src/pages/Debug/Report/DebugReportPage.tsx | 4 ++-- tests/unit/SidebarUtilsTest.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Debug/Report/DebugReportPage.tsx b/src/pages/Debug/Report/DebugReportPage.tsx index fad431b44ea33..208be5520a393 100644 --- a/src/pages/Debug/Report/DebugReportPage.tsx +++ b/src/pages/Debug/Report/DebugReportPage.tsx @@ -96,7 +96,7 @@ function DebugReportPage({ hasRBR, isReportArchived, isInFocusMode: priorityMode === CONST.PRIORITY_MODE.GSD, - draftComment: draftComment, + draftComment, }); return [ @@ -142,7 +142,7 @@ function DebugReportPage({ : undefined, }, ]; - }, [report, transactionViolations, reportID, isReportArchived, chatReport, reportActions, transactions, reportAttributes?.reportErrors, betas, priorityMode, translate]); + }, [report, transactionViolations, reportID, isReportArchived, chatReport, reportActions, transactions, reportAttributes?.reportErrors, betas, priorityMode, draftComment, translate]); const DebugDetailsTab = useCallback( () => ( diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index 1360213516ff5..2118f8a7cb480 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -1593,7 +1593,7 @@ describe('SidebarUtils', () => { }; // When the reports are categorized - const result = SidebarUtils.categorizeReportsForLHN(reports, ''); + const result = SidebarUtils.categorizeReportsForLHN(reports, {}); // Then the reports are categorized into the correct groups expect(result.pinnedAndGBRReports).toHaveLength(0); @@ -1607,7 +1607,7 @@ describe('SidebarUtils', () => { it('should handle empty reports object', () => { // Given the reports are empty - const result = SidebarUtils.categorizeReportsForLHN({}, ''); + const result = SidebarUtils.categorizeReportsForLHN({}, {}); // Then the reports are categorized into the correct groups expect(result.pinnedAndGBRReports).toHaveLength(0); From 73a33c8485c4b3b00290f74a3d880bd83f5fd8b1 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Wed, 17 Sep 2025 08:36:19 +0300 Subject: [PATCH 08/25] resolving comments --- .../LHNOptionsList/LHNOptionsList.tsx | 3 +- .../FilterDropdowns/UserSelectPopup.tsx | 4 +- .../Search/SearchAutocompleteList.tsx | 17 +++- .../SearchFiltersParticipantsSelector.tsx | 2 + src/libs/DraftCommentUtils.ts | 25 ----- src/libs/OptionsListUtils/index.ts | 97 +++++++++++-------- src/libs/ReportUtils.ts | 3 +- src/libs/SidebarUtils.ts | 10 +- src/libs/actions/Report.ts | 4 +- .../TaskShareDestinationSelectorModal.tsx | 5 +- tests/unit/OptionsListUtilsTest.tsx | 36 +++---- tests/unit/SidebarUtilsTest.ts | 11 --- 12 files changed, 101 insertions(+), 116 deletions(-) delete mode 100644 src/libs/DraftCommentUtils.ts diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 7708f32a9a2b9..e2ac199b7a690 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -19,7 +19,6 @@ import useRootNavigationState from '@hooks/useRootNavigationState'; import useScrollEventEmitter from '@hooks/useScrollEventEmitter'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isValidDraftComment} from '@libs/DraftCommentUtils'; import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; @@ -190,7 +189,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio ? (getOriginalMessage(itemParentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID; const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - const hasDraftComment = isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]); + const hasDraftComment = !!draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]; const isReportArchived = !!itemReportNameValuePairs?.private_isArchived; const canUserPerformWrite = canUserPerformWriteAction(item, isReportArchived); diff --git a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx index ef18babc2dbe2..f357dbcc9acda 100644 --- a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx @@ -56,6 +56,7 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const [searchTerm, setSearchTerm] = useState(''); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const initialSelectedOptions = useMemo(() => { return value.reduce((acc, id) => { const participant = personalDetails?.[id]; @@ -86,12 +87,13 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) reports: options.reports, personalDetails: options.personalDetails, }, + draftComments, { excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, includeCurrentUser: true, }, ); - }, [options.reports, options.personalDetails]); + }, [options.reports, options.personalDetails, draftComments]); const filteredOptions = useMemo(() => { return filterAndOrderOptions(optionsList, cleanSearchTerm, countryCode, { diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 9be712b0a2c61..f7725c2738aae 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -176,6 +176,7 @@ function SearchAutocompleteList( const {shouldUseNarrowLayout} = useResponsiveLayout(); const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES, {canBeMissing: true}); const taxRates = getAllTaxRates(); @@ -184,8 +185,20 @@ function SearchAutocompleteList( if (!areOptionsInitialized) { return defaultListOptions; } - return getSearchOptions(options, betas ?? [], true, true, autocompleteQueryValue, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS, true, true, false, true); - }, [areOptionsInitialized, betas, options, autocompleteQueryValue]); + return getSearchOptions({ + options, + draftComments, + betas: betas ?? [], + isUsedInChatFinder: true, + includeReadOnly: true, + searchQuery: autocompleteQueryValue, + maxResults: CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS, + includeUserToInvite: true, + includeRecentReports: true, + includeCurrentUser: true, + shouldShowGBR: false, + }); + }, [areOptionsInitialized, options, draftComments, betas, autocompleteQueryValue]); const [isInitialRender, setIsInitialRender] = useState(true); diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index 20b0a0efa4a4c..735b6cfb25ce4 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -51,6 +51,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: const [selectedOptions, setSelectedOptions] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const cleanSearchTerm = useMemo(() => searchTerm.trim().toLowerCase(), [searchTerm]); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const defaultOptions = useMemo(() => { if (!areOptionsInitialized) { @@ -62,6 +63,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: reports: options.reports, personalDetails: options.personalDetails, }, + draftComments, { excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, includeCurrentUser: true, diff --git a/src/libs/DraftCommentUtils.ts b/src/libs/DraftCommentUtils.ts deleted file mode 100644 index 779a9f394a02a..0000000000000 --- a/src/libs/DraftCommentUtils.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Returns true if the report has a valid draft comment. - * A valid draft comment is a non-empty string. - */ -function isValidDraftComment(comment?: string): boolean { - return !!comment; -} - -/** - * Returns true if the report has a valid draft comment. - */ -function hasValidDraftComment(reportID: string, draftComment: string | undefined): boolean { - return isValidDraftComment(draftComment); -} - -/** - * Prepares a draft comment by returning null if it's empty. - */ -function prepareDraftComment(comment: string | null) { - // logical OR is used to convert empty string to null - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return comment || null; -} - -export {isValidDraftComment, hasValidDraftComment, prepareDraftComment}; diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index ca7700030d2f1..9fe3ee7009e66 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -1741,6 +1741,7 @@ function getRestrictedLogins(config: GetOptionsConfig, options: OptionList, canS */ function getValidOptions( options: OptionList, + draftComments: OnyxCollection | undefined, { excludeLogins = {}, includeSelectedOptions = false, @@ -1756,7 +1757,6 @@ function getValidOptions( includeUserToInvite = false, ...config }: GetOptionsConfig = {}, - draftComment: string | undefined = undefined, ): Options { const restrictedLogins = getRestrictedLogins(config, options, canShowManagerMcTest); @@ -1792,7 +1792,7 @@ function getValidOptions( const filteringFunction = (report: SearchOption) => { let searchText = `${report.text ?? ''}${report.login ?? ''}`; - + const draftComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`]; if (report.isThread) { searchText += report.alternateText ?? ''; } else if (report.isChatRoom) { @@ -1922,24 +1922,39 @@ function getValidOptions( }; } +type SearchOptionsConfig = { + options: OptionList; + draftComments: OnyxCollection; + betas?: Beta[]; + isUsedInChatFinder?: boolean; + includeReadOnly?: boolean; + searchQuery?: string; + maxResults?: number; + includeUserToInvite?: boolean; + includeRecentReports?: boolean; + includeCurrentUser?: boolean; + shouldShowGBR?: boolean; +}; + /** * Build the options for the Search view */ -function getSearchOptions( - options: OptionList, - betas: Beta[] = [], +function getSearchOptions({ + options, + draftComments, + betas, isUsedInChatFinder = true, includeReadOnly = true, searchQuery = '', - maxResults?: number, - includeUserToInvite?: boolean, + maxResults, + includeUserToInvite, includeRecentReports = true, includeCurrentUser = false, shouldShowGBR = false, -): Options { +}: SearchOptionsConfig): Options { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); - const optionList = getValidOptions(options, { + const optionList = getValidOptions(options, draftComments, { betas, includeRecentReports, includeMultipleParticipantReports: true, @@ -1966,8 +1981,8 @@ function getSearchOptions( return optionList; } -function getShareLogOptions(options: OptionList, betas: Beta[] = []): Options { - return getValidOptions(options, { +function getShareLogOptions(options: OptionList, draftComments: OnyxCollection, betas: Beta[] = []): Options { + return getValidOptions(options, draftComments, { betas, includeMultipleParticipantReports: true, includeP2P: true, @@ -2009,6 +2024,7 @@ function getAttendeeOptions( betas: OnyxEntry, attendees: Attendee[], recentAttendees: Attendee[], + draftComments: OnyxCollection, includeOwnedWorkspaceChats = false, includeP2P = true, includeInvoiceRooms = false, @@ -2042,22 +2058,19 @@ function getAttendeeOptions( })) .map((attendee) => getParticipantsOption(attendee, personalDetailList as never)); - return getValidOptions( - {reports, personalDetails}, - { - betas, - selectedOptions: attendees.map((attendee) => ({...attendee, login: attendee.email})), - excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, - includeOwnedWorkspaceChats, - includeRecentReports: false, - includeP2P, - includeSelectedOptions: false, - includeSelfDM: false, - includeInvoiceRooms, - action, - recentAttendees: filteredRecentAttendees, - }, - ); + return getValidOptions({reports, personalDetails}, draftComments, { + betas, + selectedOptions: attendees.map((attendee) => ({...attendee, login: attendee.email})), + excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, + includeOwnedWorkspaceChats, + includeRecentReports: false, + includeP2P, + includeSelectedOptions: false, + includeSelfDM: false, + includeInvoiceRooms, + action, + recentAttendees: filteredRecentAttendees, + }); } /** @@ -2071,23 +2084,21 @@ function getShareDestinationOptions( selectedOptions: Array> = [], excludeLogins: Record = {}, includeOwnedWorkspaceChats = true, + draftComments: OnyxCollection = {}, ) { - return getValidOptions( - {reports, personalDetails}, - { - betas, - selectedOptions, - includeMultipleParticipantReports: true, - showChatPreviewLine: true, - forcePolicyNamePreview: true, - includeThreads: true, - includeMoneyRequests: true, - includeTasks: true, - excludeLogins, - includeOwnedWorkspaceChats, - includeSelfDM: true, - }, - ); + return getValidOptions({reports, personalDetails}, draftComments, { + betas, + selectedOptions, + includeMultipleParticipantReports: true, + showChatPreviewLine: true, + forcePolicyNamePreview: true, + includeThreads: true, + includeMoneyRequests: true, + includeTasks: true, + excludeLogins, + includeOwnedWorkspaceChats, + includeSelfDM: true, + }); } /** diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 481084f77f91d..2c73b73cd9168 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -83,7 +83,6 @@ import type {OnboardingCompanySize, OnboardingMessage, OnboardingPurpose, Onboar import type {AddCommentOrAttachmentParams} from './API/parameters'; import {convertToDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; -import {hasValidDraftComment} from './DraftCommentUtils'; import {getEnvironment, getEnvironmentURL} from './Environment/Environment'; import type EnvironmentType from './Environment/getEnvironment/types'; import {getMicroSecondOnyxErrorWithTranslationKey, isReceiptError} from './ErrorUtils'; @@ -8500,7 +8499,7 @@ function reasonForReportToBeInOptionList({ } // Retrieve the draft comment for the report and convert it to a boolean - const hasDraftComment = hasValidDraftComment(report.reportID, draftComment); + const hasDraftComment = !!draftComment; // Include reports that are relevant to the user in any view mode. Criteria include having a draft or having a GBR showing. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 29669fab20277..b123bb8286860 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -14,7 +14,6 @@ import type Policy from '@src/types/onyx/Policy'; import type PriorityMode from '@src/types/onyx/PriorityMode'; import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; -import {hasValidDraftComment} from './DraftCommentUtils'; import {translateLocal} from './Localize'; import {getLastActorDisplayName, getLastMessageTextForReport, getPersonalDetailsForAccountIDs, shouldShowLastActorDisplayName} from './OptionsListUtils'; import Parser from './Parser'; @@ -203,12 +202,7 @@ function shouldDisplayReportInLHN( const isSystemChat = isSystemChatUtil(report); const draftComment = reportsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`]; const shouldOverrideHidden = - hasValidDraftComment(report.reportID, draftComment) || - hasErrorsOtherThanFailedReceipt || - isFocused || - isSystemChat || - !!report.isPinned || - reportAttributes?.[report?.reportID]?.requiresAttention; + !!draftComment || hasErrorsOtherThanFailedReceipt || isFocused || isSystemChat || !!report.isPinned || reportAttributes?.[report?.reportID]?.requiresAttention; if (isHidden && !shouldOverrideHidden) { return {shouldDisplay: false}; @@ -367,7 +361,7 @@ function categorizeReportsForLHN( const isPinned = !!report.isPinned; const requiresAttention = !!reportAttributes?.[reportID]?.requiresAttention; const draftComment = reportsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]; - const hasDraft = reportID ? hasValidDraftComment(reportID, draftComment) : false; + const hasDraft = !!draftComment; const reportNameValuePairsKey = `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`; const rNVPs = reportNameValuePairs?.[reportNameValuePairsKey]; const isArchived = isArchivedNonExpenseReport(report, !!rNVPs?.private_isArchived); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 6ecef3df56ba8..85522cbc1eb19 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -62,7 +62,6 @@ import * as ApiUtils from '@libs/ApiUtils'; import * as CollectionUtils from '@libs/CollectionUtils'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import DateUtils from '@libs/DateUtils'; -import {prepareDraftComment} from '@libs/DraftCommentUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as Environment from '@libs/Environment/Environment'; import {getOldDotURLFromEnvironment} from '@libs/Environment/Environment'; @@ -1785,7 +1784,8 @@ function saveReportDraft(reportID: string, report: Report) { * When empty string or null is passed, it will delete the draft comment from Onyx store. */ function saveReportDraftComment(reportID: string, comment: string | null, callback: () => void = () => {}) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, prepareDraftComment(comment)).then(callback); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if comment was empty string ?? wont work + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, comment || null).then(callback); } /** Broadcasts whether or not a user is typing on a report over the report's private pusher channel. */ diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index 5d368bbf755b2..ac4fc835b0c00 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -76,6 +76,7 @@ function TaskShareDestinationSelectorModal() { return ids; }, }); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const textInputHint = useMemo(() => (isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''), [isOffline, translate]); @@ -90,7 +91,7 @@ function TaskShareDestinationSelectorModal() { }; } const filteredReports = reportFilter(optionList.reports, archivedReportsIdSet); - const {recentReports} = getShareDestinationOptions(filteredReports, optionList.personalDetails, [], [], {}, true); + const {recentReports} = getShareDestinationOptions(filteredReports, optionList.personalDetails, [], [], {}, true, draftComments); const header = getHeaderMessage(recentReports && recentReports.length !== 0, false, ''); return { recentReports, @@ -99,7 +100,7 @@ function TaskShareDestinationSelectorModal() { currentUserOption: null, header, }; - }, [areOptionsInitialized, optionList.personalDetails, optionList.reports, archivedReportsIdSet]); + }, [areOptionsInitialized, optionList.reports, optionList.personalDetails, archivedReportsIdSet, draftComments]); const options = useMemo(() => { if (debouncedSearchValue.trim() === '') { diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index b527efa642e39..9b5b97c1868d4 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -1332,7 +1332,7 @@ describe('OptionsListUtils', () => { it('should not return any results if the search value is on an excluded logins list', () => { const searchText = 'admin@expensify.com'; // Given a set of options with excluded logins list - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails},{}, {excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT}); // When we call filterAndOrderOptions with a search value and excluded logins list const filterOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE, {excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT}); @@ -1343,7 +1343,7 @@ describe('OptionsListUtils', () => { it('should return the user to invite when the search value is a valid, non-existent email and the user is not excluded', () => { const searchText = 'test@email.com'; // Given a set of options - const options = getSearchOptions(OPTIONS); + const options = getSearchOptions({options: OPTIONS, draftComments: {}}); // When we call filterAndOrderOptions with a search value and excludeLogins const filteredOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE, {excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT}); @@ -1354,7 +1354,7 @@ describe('OptionsListUtils', () => { it('should return limited amount of recent reports if the limit is set', () => { const searchText = ''; // Given a set of options - const options = getSearchOptions(OPTIONS); + const options = getSearchOptions({options: OPTIONS, draftComments: {}}); // When we call filterAndOrderOptions with a search value and maxRecentReportsToShow set to 2 const filteredOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE, {maxRecentReportsToShow: 2}); @@ -1372,7 +1372,7 @@ describe('OptionsListUtils', () => { it('should not return any user to invite if email exists on the personal details list', () => { const searchText = 'natasharomanoff@expensify.com'; // Given a set of options with all betas - const options = getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); + const options = getSearchOptions({options: OPTIONS, draftComments: {}, betas: [CONST.BETAS.ALL]}); // When we call filterAndOrderOptions with a search value const filteredOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE); @@ -1464,7 +1464,7 @@ describe('OptionsListUtils', () => { it('should show the option from personal details when searching for personal detail with no existing report', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, , personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value that matches a personal detail with no existing report const filteredOptions = filterAndOrderOptions(options, 'hulk', COUNTRY_CODE); @@ -1478,7 +1478,7 @@ describe('OptionsListUtils', () => { it('should not return any options or user to invite if there are no search results and the string does not match a potential email or phone', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports const filteredOptions = filterAndOrderOptions(options, 'marc@expensify', COUNTRY_CODE); @@ -1491,7 +1491,7 @@ describe('OptionsListUtils', () => { it('should not return any options but should return an user to invite if no matching options exist and the search value is a potential email', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports const filteredOptions = filterAndOrderOptions(options, 'marc@expensify.com', COUNTRY_CODE); @@ -1504,7 +1504,7 @@ describe('OptionsListUtils', () => { it('should return user to invite when search term has a period with options for it that do not contain the period', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports but matches user to invite const filteredOptions = filterAndOrderOptions(options, 'peter.parker@expensify.com', COUNTRY_CODE); @@ -1516,7 +1516,7 @@ describe('OptionsListUtils', () => { it('should return user which has displayName with accent mark when search value without accent mark', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); // When we call filterAndOrderOptions with a search value without accent mark const filteredOptions = filterAndOrderOptions(options, 'Timothee', COUNTRY_CODE); @@ -1526,7 +1526,7 @@ describe('OptionsListUtils', () => { it('should not return options but should return an user to invite if no matching options exist and the search value is a potential phone number', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports but matches user to invite const filteredOptions = filterAndOrderOptions(options, '5005550006', COUNTRY_CODE); @@ -1541,7 +1541,7 @@ describe('OptionsListUtils', () => { it('should not return options but should return an user to invite if no matching options exist and the search value is a potential phone number with country code added', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports but matches user to invite const filteredOptions = filterAndOrderOptions(options, '+15005550006', COUNTRY_CODE); @@ -1556,7 +1556,7 @@ describe('OptionsListUtils', () => { it('should not return options but should return an user to invite if no matching options exist and the search value is a potential phone number with special characters added', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports but matches user to invite const filteredOptions = filterAndOrderOptions(options, '+1 (800)324-3233', COUNTRY_CODE); @@ -1571,7 +1571,7 @@ describe('OptionsListUtils', () => { it('should not return any options or user to invite if contact number contains alphabet characters', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports const filteredOptions = filterAndOrderOptions(options, '998243aaaa', COUNTRY_CODE); @@ -1584,7 +1584,7 @@ describe('OptionsListUtils', () => { it('should not return any options if search value does not match any personal details', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); // When we call filterAndOrderOptions with a search value that does not match any personal details const filteredOptions = filterAndOrderOptions(options, 'magneto', COUNTRY_CODE); @@ -1594,7 +1594,7 @@ describe('OptionsListUtils', () => { it('should return one recent report and no personal details if a search value provides an email', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); // When we call filterAndOrderOptions with a search value that matches an email const filteredOptions = filterAndOrderOptions(options, 'peterparker@expensify.com', COUNTRY_CODE, {sortByReportTypeInSearch: true}); @@ -1608,7 +1608,7 @@ describe('OptionsListUtils', () => { it('should return all matching reports and personal details', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); // When we call filterAndOrderOptions with a search value that matches both reports and personal details and maxRecentReportsToShow param const filteredOptions = filterAndOrderOptions(options, '.com', COUNTRY_CODE, {maxRecentReportsToShow: 5}); @@ -1625,7 +1625,7 @@ describe('OptionsListUtils', () => { it('should return matching option when searching (getSearchOptions)', () => { // Given a set of options - const options = getSearchOptions(OPTIONS); + const options = getSearchOptions({options: OPTIONS, draftComments: {}}); // When we call filterAndOrderOptions with a search value that matches a personal detail const filteredOptions = filterAndOrderOptions(options, 'spider', COUNTRY_CODE); @@ -1637,7 +1637,7 @@ describe('OptionsListUtils', () => { it('should return latest lastVisibleActionCreated item on top when search value matches multiple items (getSearchOptions)', () => { // Given a set of options - const options = getSearchOptions(OPTIONS); + const options = getSearchOptions({options: OPTIONS, draftComments: {}}); // When we call filterAndOrderOptions with a search value that matches multiple items const filteredOptions = filterAndOrderOptions(options, 'fantastic', COUNTRY_CODE); diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index 2118f8a7cb480..7f89214959b3e 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -28,13 +28,6 @@ import * as LHNTestUtils from '../utils/LHNTestUtils'; import {localeCompare} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -// Mock DraftCommentUtils -jest.mock('@libs/DraftCommentUtils', () => ({ - hasValidDraftComment: jest.fn(), - isValidDraftComment: jest.fn(), - prepareDraftComment: jest.fn(), -})); - // Mock PolicyUtils jest.mock('@libs/PolicyUtils', () => ({ ...jest.requireActual('@libs/PolicyUtils'), @@ -1518,10 +1511,6 @@ describe('SidebarUtils', () => { describe('sortReportsToDisplayInLHN', () => { describe('categorizeReportsForLHN', () => { it('should categorize reports into correct groups', () => { - // Given hasValidDraftComment is mocked to return true for report '2' - const {hasValidDraftComment} = require('@libs/DraftCommentUtils') as {hasValidDraftComment: jest.Mock}; - hasValidDraftComment.mockImplementation((reportID: string, draftComment: string | null) => reportID === '2' && draftComment === 'test'); - const {reports, reportNameValuePairs, reportAttributes} = createSidebarTestData(); // Given reportsDrafts contains a draft comment for report '2' From edb95247113612af4ab9b6a43864b97fc194f112 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Wed, 17 Sep 2025 11:21:05 +0300 Subject: [PATCH 09/25] Refactor wip --- tests/perf-test/OptionsListUtils.perf-test.ts | 8 +- tests/unit/OptionsListUtilsTest.tsx | 141 ++++++++++++------ 2 files changed, 97 insertions(+), 52 deletions(-) diff --git a/tests/perf-test/OptionsListUtils.perf-test.ts b/tests/perf-test/OptionsListUtils.perf-test.ts index 108408156aa17..15d8339e97fe6 100644 --- a/tests/perf-test/OptionsListUtils.perf-test.ts +++ b/tests/perf-test/OptionsListUtils.perf-test.ts @@ -107,26 +107,26 @@ describe('OptionsListUtils', () => { /* Testing getSearchOptions */ test('[OptionsListUtils] getSearchOptions', async () => { await waitForBatchedUpdates(); - await measureFunction(() => getSearchOptions(options, mockedBetas)); + await measureFunction(() => getSearchOptions({options, betas: mockedBetas, draftComments: {}})); }); /* Testing getShareLogOptions */ test('[OptionsListUtils] getShareLogOptions', async () => { await waitForBatchedUpdates(); - await measureFunction(() => getShareLogOptions(options, mockedBetas)); + await measureFunction(() => getShareLogOptions(options, {}, mockedBetas)); }); /* Testing getFilteredOptions */ test('[OptionsListUtils] getFilteredOptions with search value', async () => { await waitForBatchedUpdates(); - const formattedOptions = getValidOptions({reports: options.reports, personalDetails: options.personalDetails}, ValidOptionsConfig); + const formattedOptions = getValidOptions({reports: options.reports, personalDetails: options.personalDetails}, {}, ValidOptionsConfig); await measureFunction(() => { filterAndOrderOptions(formattedOptions, SEARCH_VALUE, COUNTRY_CODE); }); }); test('[OptionsListUtils] getFilteredOptions with empty search value', async () => { await waitForBatchedUpdates(); - const formattedOptions = getValidOptions({reports: options.reports, personalDetails: options.personalDetails}, ValidOptionsConfig); + const formattedOptions = getValidOptions({reports: options.reports, personalDetails: options.personalDetails}, {}, ValidOptionsConfig); await measureFunction(() => { filterAndOrderOptions(formattedOptions, '', COUNTRY_CODE); }); diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 9b5b97c1868d4..71ea4126aba10 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -617,7 +617,7 @@ describe('OptionsListUtils', () => { it('should return all options when no search value is provided', () => { // Given a set of options // When we call getSearchOptions with all betas - const results = getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); + const results = getSearchOptions({options: OPTIONS, draftComments: {}, betas: [CONST.BETAS.ALL]}); // Then all personal details (including those that have reports) should be returned expect(results.personalDetails.length).toBe(10); @@ -629,7 +629,17 @@ describe('OptionsListUtils', () => { it('should include current user when includeCurrentUser is true for type:chat from suggestions', () => { // Given a set of options where the current user is Iron Man (accountID: 2) // When we call getSearchOptions with includeCurrentUser set to true - const results = getSearchOptions(OPTIONS, [CONST.BETAS.ALL], true, true, '', undefined, false, true, true); + const results = getSearchOptions({ + options: OPTIONS, + draftComments: {}, + betas: [CONST.BETAS.ALL], + isUsedInChatFinder: true, + includeReadOnly: true, + searchQuery: '', + maxResults: undefined, + includeUserToInvite: false, + includeRecentReports: true, + }); // Then the current user should be included in personalDetails const currentUserOption = results.personalDetails.find((option) => option.login === 'tonystark@expensify.com'); @@ -644,7 +654,17 @@ describe('OptionsListUtils', () => { it('should exclude current user when includeCurrentUser is false', () => { // Given a set of options where the current user is Iron Man (accountID: 2) // When we call getSearchOptions with includeCurrentUser set to false (default behavior) - const results = getSearchOptions(OPTIONS, [CONST.BETAS.ALL], true, true, '', undefined, false, true, false); + const results = getSearchOptions({ + options: OPTIONS, + draftComments: {}, + betas: [CONST.BETAS.ALL], + isUsedInChatFinder: true, + includeReadOnly: true, + searchQuery: '', + maxResults: undefined, + includeUserToInvite: false, + includeRecentReports: true, + }); // Then the current user should not be included in personalDetails const currentUserOption = results.personalDetails.find((option) => option.login === 'tonystark@expensify.com'); @@ -659,10 +679,13 @@ describe('OptionsListUtils', () => { it('should sort options alphabetically and preserves reportID for personal details with existing reports', () => { // Given a set of reports and personalDetails // When we call getValidOptions() - let results: Pick = getValidOptions({ - reports: OPTIONS.reports, - personalDetails: OPTIONS.personalDetails, - }); + let results: Pick = getValidOptions( + { + reports: OPTIONS.reports, + personalDetails: OPTIONS.personalDetails, + }, + {}, + ); // When we call orderOptions() results = orderOptions(results); @@ -693,7 +716,7 @@ describe('OptionsListUtils', () => { it('should sort personal details options alphabetically when only personal details are provided', () => { // Given a set of personalDetails and an empty reports array - let results: Pick = getValidOptions({personalDetails: OPTIONS.personalDetails, reports: []}); + let results: Pick = getValidOptions({personalDetails: OPTIONS.personalDetails, reports: []}, {}); // When we call orderOptions() results = orderOptions(results); @@ -720,7 +743,7 @@ describe('OptionsListUtils', () => { it('should return empty options when no reports or personal details are provided', () => { // Given empty arrays of reports and personalDetails // When we call getValidOptions() - const results = getValidOptions({reports: [], personalDetails: []}); + const results = getValidOptions({reports: [], personalDetails: []}, {}); // Then the result should be empty expect(results.personalDetails).toEqual([]); @@ -734,7 +757,7 @@ describe('OptionsListUtils', () => { it('should include Concierge by default in results', () => { // Given a set of reports and personalDetails that includes Concierge // When we call getValidOptions() - const results = getValidOptions({reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails}); + const results = getValidOptions({reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails}, {}); // Then the result should include all personalDetails except the currently logged in user expect(results.personalDetails.length).toBe(Object.values(OPTIONS_WITH_CONCIERGE.personalDetails).length - 1); @@ -750,6 +773,7 @@ describe('OptionsListUtils', () => { reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails, }, + {}, { excludeLogins: {[CONST.EMAIL.CONCIERGE]: true}, }, @@ -764,7 +788,11 @@ describe('OptionsListUtils', () => { it('should exclude Chronos when excludedLogins is specified', () => { // Given a set of reports and personalDetails that includes Chronos and a config object that excludes Chronos // When we call getValidOptions() - const results = getValidOptions({reports: OPTIONS_WITH_CHRONOS.reports, personalDetails: OPTIONS_WITH_CHRONOS.personalDetails}, {excludeLogins: {[CONST.EMAIL.CHRONOS]: true}}); + const results = getValidOptions( + {reports: OPTIONS_WITH_CHRONOS.reports, personalDetails: OPTIONS_WITH_CHRONOS.personalDetails}, + {}, + {excludeLogins: {[CONST.EMAIL.CHRONOS]: true}}, + ); // Then the result should include all personalDetails except the currently logged in user and Chronos expect(results.personalDetails.length).toBe(Object.values(OPTIONS_WITH_CHRONOS.personalDetails).length - 2); @@ -780,6 +808,7 @@ describe('OptionsListUtils', () => { reports: OPTIONS_WITH_RECEIPTS.reports, personalDetails: OPTIONS_WITH_RECEIPTS.personalDetails, }, + {}, { excludeLogins: {[CONST.EMAIL.RECEIPTS]: true}, }, @@ -796,6 +825,7 @@ describe('OptionsListUtils', () => { // When we call getValidOptions() const result = getValidOptions( {reports: OPTIONS_WITH_MANAGER_MCTEST.reports, personalDetails: OPTIONS_WITH_MANAGER_MCTEST.personalDetails}, + {}, {includeP2P: true, canShowManagerMcTest: true, betas: [CONST.BETAS.NEWDOT_MANAGER_MCTEST]}, ); @@ -810,6 +840,7 @@ describe('OptionsListUtils', () => { // When we call getValidOptions() const result = getValidOptions( {reports: OPTIONS_WITH_MANAGER_MCTEST.reports, personalDetails: OPTIONS_WITH_MANAGER_MCTEST.personalDetails}, + {}, {includeP2P: true, canShowManagerMcTest: false, betas: [CONST.BETAS.NEWDOT_MANAGER_MCTEST]}, ); @@ -833,6 +864,7 @@ describe('OptionsListUtils', () => { // When we call getValidOptions() const optionsWhenUserAlreadySubmittedExpense = getValidOptions( {reports: OPTIONS_WITH_MANAGER_MCTEST.reports, personalDetails: OPTIONS_WITH_MANAGER_MCTEST.personalDetails}, + {}, {includeP2P: true, canShowManagerMcTest: true, betas: [CONST.BETAS.NEWDOT_MANAGER_MCTEST]}, ); @@ -880,6 +912,7 @@ describe('OptionsListUtils', () => { // When we call getValidOptions with includeMultipleParticipantReports set to true const results = getValidOptions( {reports: [adminRoom], personalDetails: OPTIONS.personalDetails}, + {}, { includeMultipleParticipantReports: true, }, @@ -928,6 +961,7 @@ describe('OptionsListUtils', () => { }; const results = getValidOptions( {reports: [workspaceChat], personalDetails: []}, + {}, { includeMultipleParticipantReports: true, showRBR: true, @@ -974,6 +1008,7 @@ describe('OptionsListUtils', () => { }; const results = getValidOptions( {reports: [workspaceChat], personalDetails: []}, + {}, { includeMultipleParticipantReports: true, showRBR: false, @@ -987,12 +1022,16 @@ describe('OptionsListUtils', () => { it('should include all reports by default', () => { // Given a set of reports and personalDetails that includes workspace rooms // When we call getValidOptions() - const results = getValidOptions(OPTIONS_WITH_WORKSPACE_ROOM, { - includeRecentReports: true, - includeMultipleParticipantReports: true, - includeP2P: true, - includeOwnedWorkspaceChats: true, - }); + const results = getValidOptions( + OPTIONS_WITH_WORKSPACE_ROOM, + {}, + { + includeRecentReports: true, + includeMultipleParticipantReports: true, + includeP2P: true, + includeOwnedWorkspaceChats: true, + }, + ); // Then the result should include all reports except the currently logged in user expect(results.recentReports.length).toBe(OPTIONS_WITH_WORKSPACE_ROOM.reports.length - 1); @@ -1004,7 +1043,7 @@ describe('OptionsListUtils', () => { it('should exclude users with recent reports from personalDetails', () => { // Given a set of reports and personalDetails // When we call getValidOptions with no search value - const results = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); + const results = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); const reportLogins = results.recentReports.map((reportOption) => reportOption.login); const personalDetailsOverlapWithReports = results.personalDetails.every((personalDetailOption) => reportLogins.includes(personalDetailOption.login)); @@ -1017,7 +1056,7 @@ describe('OptionsListUtils', () => { it('should exclude selected options', () => { // Given a set of reports and personalDetails // When we call getValidOptions with excludeLogins param - const results = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {excludeLogins: {'peterparker@expensify.com': true}}); + const results = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}, {excludeLogins: {'peterparker@expensify.com': true}}); // Then the option should not appear anywhere in either list expect(results.recentReports.every((option) => option.login !== 'peterparker@expensify.com')).toBe(true); @@ -1027,7 +1066,7 @@ describe('OptionsListUtils', () => { it('should include Concierge in the results by default', () => { // Given a set of report and personalDetails that include Concierge // When we call getValidOptions() - const results = getValidOptions({reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails}); + const results = getValidOptions({reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails}, {}); // Then the result should include all personalDetails except the currently logged in user expect(results.personalDetails.length).toBe(Object.values(OPTIONS_WITH_CONCIERGE.personalDetails).length - 1); @@ -1043,6 +1082,7 @@ describe('OptionsListUtils', () => { reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails, }, + {}, { excludeLogins: {[CONST.EMAIL.CONCIERGE]: true}, }, @@ -1058,7 +1098,11 @@ describe('OptionsListUtils', () => { it('should exclude Chronos from the results when it is specified in excludedLogins', () => { // given a set of reports and personalDetails that includes Chronos // When we call getValidOptions() with excludeLogins param - const results = getValidOptions({reports: OPTIONS_WITH_CHRONOS.reports, personalDetails: OPTIONS_WITH_CHRONOS.personalDetails}, {excludeLogins: {[CONST.EMAIL.CHRONOS]: true}}); + const results = getValidOptions( + {reports: OPTIONS_WITH_CHRONOS.reports, personalDetails: OPTIONS_WITH_CHRONOS.personalDetails}, + {}, + {excludeLogins: {[CONST.EMAIL.CHRONOS]: true}}, + ); // Then the result should include all personalDetails except the currently logged in user and Chronos expect(results.personalDetails.length).toBe(Object.values(OPTIONS_WITH_CHRONOS.personalDetails).length - 2); @@ -1075,6 +1119,7 @@ describe('OptionsListUtils', () => { reports: OPTIONS_WITH_RECEIPTS.reports, personalDetails: OPTIONS_WITH_RECEIPTS.personalDetails, }, + {}, { excludeLogins: {[CONST.EMAIL.RECEIPTS]: true}, }, @@ -1129,7 +1174,7 @@ describe('OptionsListUtils', () => { it('should not include read-only report', () => { // Given a list of 11 report options with reportID of 10 is archived // When we call getShareLogOptions - const results = getShareLogOptions(OPTIONS, []); + const results = getShareLogOptions(OPTIONS, {}, []); // Then the report with reportID of 10 should not be included on the list expect(results.recentReports.length).toBe(10); @@ -1189,7 +1234,7 @@ describe('OptionsListUtils', () => { it('should return all options when search is empty', () => { // Given a set of options // When we call getSearchOptions with all betas - const options = getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); + const options = getSearchOptions({options: OPTIONS, draftComments: {}, betas: [CONST.BETAS.ALL]}); // When we pass the returned options to filterAndOrderOptions with an empty search value const filteredOptions = filterAndOrderOptions(options, '', COUNTRY_CODE); @@ -1201,7 +1246,7 @@ describe('OptionsListUtils', () => { const searchText = 'man'; // Given a set of options // When we call getSearchOptions with all betas - const options = getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); + const options = getSearchOptions({options: OPTIONS, draftComments: {}, betas: [CONST.BETAS.ALL]}); // When we pass the returned options to filterAndOrderOptions with a search value and sortByReportTypeInSearch param const filteredOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE, {sortByReportTypeInSearch: true}); @@ -1220,7 +1265,7 @@ describe('OptionsListUtils', () => { const searchText = 'mistersinister@marauders.com'; // Given a set of options // When we call getSearchOptions with all betas - const options = getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); + const options = getSearchOptions({options: OPTIONS, draftComments: {}, betas: [CONST.BETAS.ALL]}); // When we pass the returned options to filterAndOrderOptions with a search value const filteredOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE); @@ -1234,7 +1279,7 @@ describe('OptionsListUtils', () => { const searchText = 'Archived'; // Given a set of options // When we call getSearchOptions with all betas - const options = getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); + const options = getSearchOptions({options: OPTIONS, draftComments: {}, betas: [CONST.BETAS.ALL]}); // When we pass the returned options to filterAndOrderOptions with a search value const filteredOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE); @@ -1250,7 +1295,7 @@ describe('OptionsListUtils', () => { // Given a set of options created from PERSONAL_DETAILS_WITH_PERIODS const OPTIONS_WITH_PERIODS = createOptionList(PERSONAL_DETAILS_WITH_PERIODS, REPORTS); // When we call getSearchOptions with all betas - const options = getSearchOptions(OPTIONS_WITH_PERIODS, [CONST.BETAS.ALL]); + const options = getSearchOptions({options: OPTIONS_WITH_PERIODS, draftComments: {}, betas: [CONST.BETAS.ALL]}); // When we pass the returned options to filterAndOrderOptions with a search value and sortByReportTypeInSearch param const filteredOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE, {sortByReportTypeInSearch: true}); @@ -1264,7 +1309,7 @@ describe('OptionsListUtils', () => { const searchText = 'avengers'; // Given a set of options with workspace rooms // When we call getSearchOptions with all betas - const options = getSearchOptions(OPTIONS_WITH_WORKSPACE_ROOM, [CONST.BETAS.ALL]); + const options = getSearchOptions({options: OPTIONS_WITH_WORKSPACE_ROOM, draftComments: {}, betas: [CONST.BETAS.ALL]}); // When we pass the returned options to filterAndOrderOptions with a search value const filteredOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE); @@ -1277,7 +1322,7 @@ describe('OptionsListUtils', () => { it('should put exact match by login on the top of the list', () => { const searchText = 'reedrichards@expensify.com'; // Given a set of options with all betas - const options = getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); + const options = getSearchOptions({options: OPTIONS, draftComments: {}, betas: [CONST.BETAS.ALL]}); // When we pass the returned options to filterAndOrderOptions with a search value const filteredOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE); @@ -1292,7 +1337,7 @@ describe('OptionsListUtils', () => { // Given a set of options with chat rooms const OPTIONS_WITH_CHAT_ROOMS = createOptionList(PERSONAL_DETAILS, REPORTS_WITH_CHAT_ROOM); // When we call getSearchOptions with all betas - const options = getSearchOptions(OPTIONS_WITH_CHAT_ROOMS, [CONST.BETAS.ALL]); + const options = getSearchOptions({options: OPTIONS_WITH_CHAT_ROOMS, draftComments: {}, betas: [CONST.BETAS.ALL]}); // When we pass the returned options to filterAndOrderOptions with a search value const filterOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE); @@ -1306,7 +1351,7 @@ describe('OptionsListUtils', () => { renderLocaleContextProvider(); const searchText = 'fantastic'; // Given a set of options - const options = getSearchOptions(OPTIONS); + const options = getSearchOptions({options: OPTIONS, draftComments: {}}); // When we call filterAndOrderOptions with a search value const filteredOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE); @@ -1321,7 +1366,7 @@ describe('OptionsListUtils', () => { it('should return the user to invite when the search value is a valid, non-existent email', () => { const searchText = 'test@email.com'; // Given a set of options - const options = getSearchOptions(OPTIONS); + const options = getSearchOptions({options: OPTIONS, draftComments: {}}); // When we call filterAndOrderOptions with a search value const filteredOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE); @@ -1332,7 +1377,7 @@ describe('OptionsListUtils', () => { it('should not return any results if the search value is on an excluded logins list', () => { const searchText = 'admin@expensify.com'; // Given a set of options with excluded logins list - const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails},{}, {excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}, {excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT}); // When we call filterAndOrderOptions with a search value and excluded logins list const filterOptions = filterAndOrderOptions(options, searchText, COUNTRY_CODE, {excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT}); @@ -1464,7 +1509,7 @@ describe('OptionsListUtils', () => { it('should show the option from personal details when searching for personal detail with no existing report', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, , personalDetails: OPTIONS.personalDetails}, {}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value that matches a personal detail with no existing report const filteredOptions = filterAndOrderOptions(options, 'hulk', COUNTRY_CODE); @@ -1478,7 +1523,7 @@ describe('OptionsListUtils', () => { it('should not return any options or user to invite if there are no search results and the string does not match a potential email or phone', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports const filteredOptions = filterAndOrderOptions(options, 'marc@expensify', COUNTRY_CODE); @@ -1491,7 +1536,7 @@ describe('OptionsListUtils', () => { it('should not return any options but should return an user to invite if no matching options exist and the search value is a potential email', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports const filteredOptions = filterAndOrderOptions(options, 'marc@expensify.com', COUNTRY_CODE); @@ -1504,7 +1549,7 @@ describe('OptionsListUtils', () => { it('should return user to invite when search term has a period with options for it that do not contain the period', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports but matches user to invite const filteredOptions = filterAndOrderOptions(options, 'peter.parker@expensify.com', COUNTRY_CODE); @@ -1516,7 +1561,7 @@ describe('OptionsListUtils', () => { it('should return user which has displayName with accent mark when search value without accent mark', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value without accent mark const filteredOptions = filterAndOrderOptions(options, 'Timothee', COUNTRY_CODE); @@ -1526,7 +1571,7 @@ describe('OptionsListUtils', () => { it('should not return options but should return an user to invite if no matching options exist and the search value is a potential phone number', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports but matches user to invite const filteredOptions = filterAndOrderOptions(options, '5005550006', COUNTRY_CODE); @@ -1541,7 +1586,7 @@ describe('OptionsListUtils', () => { it('should not return options but should return an user to invite if no matching options exist and the search value is a potential phone number with country code added', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports but matches user to invite const filteredOptions = filterAndOrderOptions(options, '+15005550006', COUNTRY_CODE); @@ -1556,7 +1601,7 @@ describe('OptionsListUtils', () => { it('should not return options but should return an user to invite if no matching options exist and the search value is a potential phone number with special characters added', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports but matches user to invite const filteredOptions = filterAndOrderOptions(options, '+1 (800)324-3233', COUNTRY_CODE); @@ -1571,7 +1616,7 @@ describe('OptionsListUtils', () => { it('should not return any options or user to invite if contact number contains alphabet characters', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value that does not match any personal details or reports const filteredOptions = filterAndOrderOptions(options, '998243aaaa', COUNTRY_CODE); @@ -1584,7 +1629,7 @@ describe('OptionsListUtils', () => { it('should not return any options if search value does not match any personal details', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value that does not match any personal details const filteredOptions = filterAndOrderOptions(options, 'magneto', COUNTRY_CODE); @@ -1594,7 +1639,7 @@ describe('OptionsListUtils', () => { it('should return one recent report and no personal details if a search value provides an email', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value that matches an email const filteredOptions = filterAndOrderOptions(options, 'peterparker@expensify.com', COUNTRY_CODE, {sortByReportTypeInSearch: true}); @@ -1608,7 +1653,7 @@ describe('OptionsListUtils', () => { it('should return all matching reports and personal details', () => { // Given a set of options - const options = getValidOptions({reports: OPTIONS.reports, draftComments: {}, personalDetails: OPTIONS.personalDetails}); + const options = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); // When we call filterAndOrderOptions with a search value that matches both reports and personal details and maxRecentReportsToShow param const filteredOptions = filterAndOrderOptions(options, '.com', COUNTRY_CODE, {maxRecentReportsToShow: 5}); @@ -1654,7 +1699,7 @@ describe('OptionsListUtils', () => { // Given a set of options with periods const OPTIONS_WITH_PERIODS = createOptionList(PERSONAL_DETAILS_WITH_PERIODS, REPORTS); // When we call getSearchOptions - const results = getSearchOptions(OPTIONS_WITH_PERIODS); + const results = getSearchOptions({options: OPTIONS_WITH_PERIODS, draftComments: {}}); // When we pass the returned options to filterAndOrderOptions with a search value const filteredResults = filterAndOrderOptions(results, 'barry.allen@expensify.com', COUNTRY_CODE, {sortByReportTypeInSearch: true}); @@ -1672,7 +1717,7 @@ describe('OptionsListUtils', () => { OPTIONS.personalDetails = OPTIONS.personalDetails.flatMap((obj) => [obj, {...obj}]); // Given a set of options - const options = getSearchOptions(OPTIONS, [CONST.BETAS.ALL]); + const options = getSearchOptions({options: OPTIONS, draftComments: {}, betas: [CONST.BETAS.ALL]}); // When we call filterAndOrderOptions with a an empty search value const filteredOptions = filterAndOrderOptions(options, '', COUNTRY_CODE); const matchingEntries = filteredOptions.personalDetails.filter((detail) => detail.login === login); @@ -1688,7 +1733,7 @@ describe('OptionsListUtils', () => { const OPTIONS_WITH_SELF_DM = createOptionList(PERSONAL_DETAILS, REPORTS_WITH_SELF_DM); // Given a set of options with self dm and all betas - const options = getSearchOptions(OPTIONS_WITH_SELF_DM, [CONST.BETAS.ALL]); + const options = getSearchOptions({options: OPTIONS_WITH_SELF_DM, draftComments: {}, betas: [CONST.BETAS.ALL]}); // When we call filterAndOrderOptions with a search value const filteredOptions = filterAndOrderOptions(options, searchTerm, COUNTRY_CODE); From 6e47d7927eb8a7e9883ade661bdd351f426cc432 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Wed, 17 Sep 2025 11:43:09 +0300 Subject: [PATCH 10/25] fixing typecheck --- .../Search/SearchAutocompleteList.tsx | 33 ++++++++++++++++--- .../Search/SearchFiltersChatsSelector.tsx | 5 +-- src/hooks/useSearchSelector.base.ts | 30 ++++++++++++++--- .../MoneyRequestAccountantSelector.tsx | 4 ++- 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 4855da14f6218..4b5668314a782 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -377,9 +377,19 @@ function SearchAutocompleteList( case CONST.SEARCH.SYNTAX_FILTER_KEYS.PAYER: case CONST.SEARCH.SYNTAX_FILTER_KEYS.ATTENDEE: case CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPORTER: { - const participants = getSearchOptions(options, betas ?? [], true, true, autocompleteValue, 10, false, false, true, true).personalDetails.filter( - (participant) => participant.text && !alreadyAutocompletedKeys.includes(participant.text.toLowerCase()), - ); + const participants = getSearchOptions({ + options, + draftComments, + betas: betas ?? [], + isUsedInChatFinder: true, + includeReadOnly: true, + searchQuery: autocompleteValue, + maxResults: 10, + includeUserToInvite: false, + includeRecentReports: false, + includeCurrentUser: true, + shouldShowGBR: true, + }).personalDetails.filter((participant) => participant.text && !alreadyAutocompletedKeys.includes(participant.text.toLowerCase())); return participants.map((participant) => ({ filterKey: autocompleteKey, @@ -389,7 +399,19 @@ function SearchAutocompleteList( })); } case CONST.SEARCH.SYNTAX_FILTER_KEYS.IN: { - const filteredReports = getSearchOptions(options, betas ?? [], true, true, autocompleteValue, 10, false, true, false, true).recentReports; + const filteredReports = getSearchOptions({ + options, + draftComments, + betas: betas ?? [], + isUsedInChatFinder: true, + includeReadOnly: true, + searchQuery: autocompleteValue, + maxResults: 10, + includeUserToInvite: false, + includeRecentReports: true, + includeCurrentUser: false, + shouldShowGBR: true, + }).recentReports; return filteredReports.map((chat) => ({ filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN, @@ -539,7 +561,9 @@ function SearchAutocompleteList( recentCurrencyAutocompleteList, taxAutocompleteList, options, + draftComments, betas, + currentUserLogin, typeAutocompleteList, groupByAutocompleteList, statusAutocompleteList, @@ -549,7 +573,6 @@ function SearchAutocompleteList( cardAutocompleteList, booleanTypes, workspaceList, - currentUserLogin, ]); const sortedRecentSearches = useMemo(() => { diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index fe47b185cf6c6..910d6a12b231c 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -52,6 +52,7 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen const [selectedReportIDs, setSelectedReportIDs] = useState(initialReportIDs); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const cleanSearchTerm = useMemo(() => searchTerm.trim().toLowerCase(), [searchTerm]); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const selectedOptions = useMemo(() => { return selectedReportIDs.map((id) => { @@ -65,8 +66,8 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen if (!areOptionsInitialized || !isScreenTransitionEnd) { return defaultListOptions; } - return getSearchOptions(options, undefined, false); - }, [areOptionsInitialized, isScreenTransitionEnd, options]); + return getSearchOptions({options, draftComments, betas: undefined, isUsedInChatFinder: false}); + }, [areOptionsInitialized, draftComments, isScreenTransitionEnd, options]); const chatOptions = useMemo(() => { return filterAndOrderOptions(defaultOptions, cleanSearchTerm, countryCode, { diff --git a/src/hooks/useSearchSelector.base.ts b/src/hooks/useSearchSelector.base.ts index f6b436e4e0dfe..bc24e803281f2 100644 --- a/src/hooks/useSearchSelector.base.ts +++ b/src/hooks/useSearchSelector.base.ts @@ -146,6 +146,7 @@ function useSearchSelectorBase({ const [selectedOptions, setSelectedOptions] = useState(initialSelected ?? []); const [maxResults, setMaxResults] = useState(maxResultsPerPage); const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const onListEndReached = useCallback(() => { setMaxResults((previous) => previous + maxResultsPerPage); @@ -162,9 +163,18 @@ function useSearchSelectorBase({ switch (searchContext) { case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_SEARCH: - return getSearchOptions(optionsWithContacts, betas ?? [], true, true, computedSearchTerm, maxResults, includeUserToInvite); + return getSearchOptions({ + options: optionsWithContacts, + draftComments, + betas: betas ?? [], + isUsedInChatFinder: true, + includeReadOnly: true, + searchQuery: computedSearchTerm, + maxResults, + includeUserToInvite, + }); case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE: - return getValidOptions(optionsWithContacts, { + return getValidOptions(optionsWithContacts, draftComments, { betas: betas ?? [], includeP2P: true, includeSelectedOptions: false, @@ -174,7 +184,7 @@ function useSearchSelectorBase({ searchString: computedSearchTerm, }); case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL: - return getValidOptions(optionsWithContacts, { + return getValidOptions(optionsWithContacts, draftComments, { ...getValidOptionsConfig, betas: betas ?? [], searchString: computedSearchTerm, @@ -185,7 +195,19 @@ function useSearchSelectorBase({ default: return getEmptyOptions(); } - }, [areOptionsInitialized, optionsWithContacts, betas, computedSearchTerm, maxResults, searchContext, includeUserToInvite, excludeLogins, includeRecentReports, getValidOptionsConfig]); + }, [ + areOptionsInitialized, + searchContext, + optionsWithContacts, + draftComments, + betas, + computedSearchTerm, + maxResults, + includeUserToInvite, + excludeLogins, + includeRecentReports, + getValidOptionsConfig, + ]); const isOptionSelected = useMemo(() => { return (option: OptionData) => diff --git a/src/pages/iou/request/MoneyRequestAccountantSelector.tsx b/src/pages/iou/request/MoneyRequestAccountantSelector.tsx index 10cbfeabb6ed7..fd76b0ac0fbea 100644 --- a/src/pages/iou/request/MoneyRequestAccountantSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAccountantSelector.tsx @@ -61,6 +61,7 @@ function MoneyRequestAccountantSelector({onFinish, onAccountantSelected, iouType }); const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: (val) => val?.reports}); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); useEffect(() => { searchInServer(debouncedSearchTerm.trim()); @@ -76,6 +77,7 @@ function MoneyRequestAccountantSelector({onFinish, onAccountantSelected, iouType reports: options.reports, personalDetails: options.personalDetails, }, + draftComments, { betas, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, @@ -89,7 +91,7 @@ function MoneyRequestAccountantSelector({onFinish, onAccountantSelected, iouType ...optionList, ...orderedOptions, }; - }, [action, areOptionsInitialized, betas, didScreenTransitionEnd, options.personalDetails, options.reports]); + }, [action, areOptionsInitialized, betas, didScreenTransitionEnd, draftComments, options.personalDetails, options.reports]); const chatOptions = useMemo(() => { if (!areOptionsInitialized) { From 59dc622de9035bbe24e9a513a582cc5737c81b9e Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Wed, 17 Sep 2025 12:40:02 +0300 Subject: [PATCH 11/25] wip --- src/pages/NewChatPage.tsx | 4 ++- src/pages/Share/ShareTab.tsx | 14 +++++++-- .../request/MoneyRequestAttendeeSelector.tsx | 29 +++++++++++++++++-- .../MoneyRequestParticipantsSelector.tsx | 2 ++ .../ShareLogList/BaseShareLogList.tsx | 5 ++-- .../CustomStatus/VacationDelegatePage.tsx | 2 ++ .../Security/AddDelegate/AddDelegatePage.tsx | 2 ++ src/pages/tasks/TaskAssigneeSelectorModal.tsx | 2 ++ tests/unit/OptionsListUtilsTest.tsx | 1 + 9 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index df42f9516f51b..dc3b999491810 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -68,6 +68,7 @@ function useOptions() { const {options: listOptions, areOptionsInitialized} = useOptionsList({ shouldInitialize: didScreenTransitionEnd, }); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const defaultOptions = useMemo(() => { const filteredOptions = memoizedGetValidOptions( @@ -75,13 +76,14 @@ function useOptions() { reports: listOptions.reports ?? [], personalDetails: (listOptions.personalDetails ?? []).concat(contacts), }, + draftComments, { betas: betas ?? [], includeSelfDM: true, }, ); return filteredOptions; - }, [betas, listOptions.personalDetails, listOptions.reports, contacts]); + }, [listOptions.reports, listOptions.personalDetails, contacts, draftComments, betas]); const unselectedOptions = useMemo(() => filterSelectedOptions(defaultOptions, new Set(selectedOptions.map(({accountID}) => accountID))), [defaultOptions, selectedOptions]); diff --git a/src/pages/Share/ShareTab.tsx b/src/pages/Share/ShareTab.tsx index ed28b95e9de7c..6274ebab4720d 100644 --- a/src/pages/Share/ShareTab.tsx +++ b/src/pages/Share/ShareTab.tsx @@ -38,6 +38,7 @@ function ShareTab(_: unknown, ref: React.Ref) { const [textInputValue, debouncedTextInputValue, setTextInputValue] = useDebouncedState(''); const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const selectionListRef = useRef(null); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); useImperativeHandle(ref, () => ({ focus: selectionListRef.current?.focusTextInput, @@ -54,8 +55,17 @@ function ShareTab(_: unknown, ref: React.Ref) { if (!areOptionsInitialized) { return defaultListOptions; } - return getSearchOptions(options, betas ?? [], false, false, textInputValue, 20, true); - }, [areOptionsInitialized, betas, options, textInputValue]); + return getSearchOptions({ + options, + draftComments, + betas: betas ?? [], + isUsedInChatFinder: false, + includeReadOnly: false, + searchQuery: textInputValue, + maxResults: 20, + includeUserToInvite: true, + }); + }, [areOptionsInitialized, betas, draftComments, options, textInputValue]); const recentReportsOptions = useMemo(() => { if (textInputValue.trim() === '') { diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index 88e2dd9d9ec47..85c1140d589c3 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -67,6 +67,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: false}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [recentAttendees] = useOnyx(ONYXKEYS.NVP_RECENT_ATTENDEES, {canBeMissing: true}); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const policy = usePolicy(activePolicyID); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); const {options, areOptionsInitialized} = useOptionsList({ @@ -86,7 +87,18 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde if (!areOptionsInitialized || !didScreenTransitionEnd) { getEmptyOptions(); } - const optionList = getAttendeeOptions(options.reports, options.personalDetails, betas, attendees, recentAttendees ?? [], iouType === CONST.IOU.TYPE.SUBMIT, true, false, action); + const optionList = getAttendeeOptions( + options.reports, + options.personalDetails, + betas, + attendees, + recentAttendees ?? [], + draftComments, + iouType === CONST.IOU.TYPE.SUBMIT, + true, + false, + action, + ); if (isPaidGroupPolicy) { const orderedOptions = orderOptions(optionList, searchTerm, { preferChatRoomsOverThreads: true, @@ -97,7 +109,20 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde optionList.personalDetails = orderedOptions.personalDetails; } return optionList; - }, [areOptionsInitialized, didScreenTransitionEnd, options.reports, options.personalDetails, betas, attendees, recentAttendees, iouType, action, isPaidGroupPolicy, searchTerm]); + }, [ + areOptionsInitialized, + didScreenTransitionEnd, + options.reports, + options.personalDetails, + betas, + attendees, + recentAttendees, + draftComments, + iouType, + action, + isPaidGroupPolicy, + searchTerm, + ]); const chatOptions = useMemo(() => { if (!areOptionsInitialized) { diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 5067161994028..02599779a1ba5 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -119,6 +119,7 @@ function MoneyRequestParticipantsSelector( const {options, areOptionsInitialized, initializeOptions} = useOptionsList({ shouldInitialize: didScreenTransitionEnd, }); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: (val) => val?.reports}); const [textInputAutoFocus, setTextInputAutoFocus] = useState(!isNative); @@ -159,6 +160,7 @@ function MoneyRequestParticipantsSelector( reports: options.reports, personalDetails: options.personalDetails.concat(contacts), }, + draftComments, { betas, selectedOptions: participants as Participant[], diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 8566ab3792ea2..c13c58fae3df6 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -27,6 +27,7 @@ function BaseShareLogList({onAttachLogToReport}: BaseShareLogListProps) { const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); const {options, areOptionsInitialized} = useOptionsList(); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const defaultOptions = useMemo(() => { if (!areOptionsInitialized) { @@ -38,7 +39,7 @@ function BaseShareLogList({onAttachLogToReport}: BaseShareLogListProps) { headerMessage: '', }; } - const shareLogOptions = getShareLogOptions(options, betas ?? []); + const shareLogOptions = getShareLogOptions(options, draftComments, betas ?? []); const header = getHeaderMessage((shareLogOptions.recentReports.length || 0) + (shareLogOptions.personalDetails.length || 0) !== 0, !!shareLogOptions.userToInvite, ''); @@ -46,7 +47,7 @@ function BaseShareLogList({onAttachLogToReport}: BaseShareLogListProps) { ...shareLogOptions, headerMessage: header, }; - }, [areOptionsInitialized, options, betas]); + }, [areOptionsInitialized, options, draftComments, betas]); const searchOptions = useMemo(() => { if (debouncedSearchValue.trim() === '') { diff --git a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx index f6e22358be106..6b590a16bb928 100644 --- a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx +++ b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx @@ -32,6 +32,7 @@ function useOptions() { const [vacationDelegate] = useOnyx(ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, {canBeMissing: true}); const currentVacationDelegate = vacationDelegate?.delegate; const delegatePersonalDetails = getPersonalDetailByEmail(currentVacationDelegate ?? ''); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const excludeLogins = useMemo( () => ({ @@ -47,6 +48,7 @@ function useOptions() { reports: optionsList.reports, personalDetails: optionsList.personalDetails, }, + draftComments, { betas, excludeLogins, diff --git a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx index 6a434d1899c8c..9092088d79c19 100644 --- a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx @@ -29,6 +29,7 @@ function useOptions() { const {options: optionsList, areOptionsInitialized} = useOptionsList(); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const existingDelegates = useMemo( () => account?.delegatedAccess?.delegates?.reduce( @@ -48,6 +49,7 @@ function useOptions() { reports: optionsList.reports, personalDetails: optionsList.personalDetails, }, + draftComments, { betas, excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 9cb534b63914e..5dffaa561730e 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -44,6 +44,7 @@ function useOptions() { const {options: optionsList, areOptionsInitialized} = useOptionsList(); const session = useSession(); const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const defaultOptions = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions( @@ -51,6 +52,7 @@ function useOptions() { reports: optionsList.reports, personalDetails: optionsList.personalDetails, }, + draftComments, { betas, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 15167c094ff64..7410e81a08428 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -640,6 +640,7 @@ describe('OptionsListUtils', () => { maxResults: undefined, includeUserToInvite: false, includeRecentReports: true, + includeCurrentUser: true, }); // Then the current user should be included in personalDetails From e05275fa987be171d45074595223b52ec1fdc36f Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Wed, 17 Sep 2025 13:33:10 +0300 Subject: [PATCH 12/25] resolving comments --- src/libs/UnreadIndicatorUpdater/index.ts | 13 +++++++++++-- .../ComposerWithSuggestions.tsx | 18 +++++++++--------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts index cc0259093b260..570c4b322ce03 100644 --- a/src/libs/UnreadIndicatorUpdater/index.ts +++ b/src/libs/UnreadIndicatorUpdater/index.ts @@ -37,6 +37,15 @@ Onyx.connect({ }, }); +let allDraftComments: OnyxCollection = {}; +Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, + waitForCollectionCallback: true, + callback: (value) => { + allDraftComments = value; + }, +}); + function getUnreadReportsForUnreadIndicator(reports: OnyxCollection, currentReportID: string | undefined, draftComment: string | undefined) { return Object.values(reports ?? {}).filter((report) => { const notificationPreference = ReportUtils.getReportNotificationPreference(report); @@ -76,9 +85,9 @@ const memoizedGetUnreadReportsForUnreadIndicator = memoize(getUnreadReportsForUn const triggerUnreadUpdate = debounce(() => { const currentReportID = navigationRef?.isReady?.() ? Navigation.getTopmostReportId() : undefined; - + const draftComment = allDraftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${currentReportID}`]; // We want to keep notification count consistent with what can be accessed from the LHN list - const unreadReports = memoizedGetUnreadReportsForUnreadIndicator(allReports, currentReportID, undefined); + const unreadReports = memoizedGetUnreadReportsForUnreadIndicator(allReports, currentReportID, draftComment); updateUnread(unreadReports.length); }, CONST.TIMING.UNREAD_UPDATE_DEBOUNCE_TIME); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index a101761f6bce3..aab07d03166b3 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -245,7 +245,7 @@ function ComposerWithSuggestions( const mobileInputScrollPosition = useRef(0); const cursorPositionValue = useSharedValue({x: 0, y: 0}); const tag = useSharedValue(-1); - const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true}); + const [draftComment = ''] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, {canBeMissing: true}); const [value, setValue] = useState(() => { if (draftComment) { emojisPresentBefore.current = extractEmojis(draftComment); @@ -253,7 +253,7 @@ function ComposerWithSuggestions( return draftComment; }); - const commentRef = useRef(value ?? ''); + const commentRef = useRef(value); const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: true}); const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {selector: getPreferredSkinToneIndex, canBeMissing: true}); @@ -272,7 +272,7 @@ function ComposerWithSuggestions( const valueRef = useRef(value); valueRef.current = value; - const [selection, setSelection] = useState(() => ({start: value?.length ?? 0, end: value?.length ?? 0, positionX: 0, positionY: 0})); + const [selection, setSelection] = useState(() => ({start: value.length ?? 0, end: value.length ?? 0, positionX: 0, positionY: 0})); const [composerHeight, setComposerHeight] = useState(0); @@ -374,7 +374,7 @@ function ComposerWithSuggestions( const updateComment = useCallback( (commentValue: string, shouldDebounceSaveComment?: boolean) => { raiseIsScrollLikelyLayoutTriggered(); - const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current ?? '', commentValue); + const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && containsOnlyEmojis(diff); const commentWithSpaceInserted = isEmojiInserted ? insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue; const {text: newComment, emojis, cursorPosition} = replaceAndExtractEmojis(commentWithSpaceInserted, preferredSkinTone, preferredLocale); @@ -480,12 +480,12 @@ function ComposerWithSuggestions( // Wales flag has 14 UTF-16 code units. This is the emoji with the largest number of UTF-16 code units we use. const start = Math.max(0, selection.start - 14); - const graphemes = Array.from(splitter.segment(lastTextRef.current?.substring(start, selection.start) ?? '')); + const graphemes = Array.from(splitter.segment(lastTextRef.current.substring(start, selection.start))); const lastGrapheme = graphemes.at(graphemes.length - 1); const lastGraphemeLength = lastGrapheme?.segment.length ?? 0; if (lastGraphemeLength > 1) { event.preventDefault(); - const newText = (lastTextRef.current?.slice(0, selection.start - lastGraphemeLength) ?? '') + (lastTextRef.current?.slice(selection.start) ?? ''); + const newText = lastTextRef.current.slice(0, selection.start - lastGraphemeLength) + lastTextRef.current.slice(selection.start); setSelection((prevSelection) => ({ start: selection.start - lastGraphemeLength, end: selection.start - lastGraphemeLength, @@ -703,7 +703,7 @@ function ComposerWithSuggestions( ); useEffect(() => { - onValueChange(value ?? ''); + onValueChange(value); }, [onValueChange, value]); const onLayout = useCallback( @@ -835,7 +835,7 @@ function ComposerWithSuggestions( isGroupPolicyReport={isGroupPolicyReport} policyID={policyID} // Input - value={value ?? ''} + value={value} selection={selection} setSelection={setSelection} resetKeyboardInput={resetKeyboardInput} @@ -844,7 +844,7 @@ function ComposerWithSuggestions( {isValidReportIDFromPath(reportID) && ( Date: Wed, 17 Sep 2025 13:48:08 +0300 Subject: [PATCH 13/25] resolve lint issues --- src/pages/Search/SearchTypeMenu.tsx | 1 + .../request/MoneyRequestParticipantsSelector.tsx | 13 +++++++------ .../Profile/CustomStatus/VacationDelegatePage.tsx | 2 +- .../Security/AddDelegate/AddDelegatePage.tsx | 2 +- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 2 +- src/pages/workspace/WorkspaceInvitePage.tsx | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 1fd3b6bd0aa5f..f70399f083d36 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -141,6 +141,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { allCards, allFeeds, allPolicies, + currentUserAccountID, ], ); diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 02599779a1ba5..91ff6b8863b0c 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -193,16 +193,17 @@ function MoneyRequestParticipantsSelector( ...orderedOptions, }; }, [ - action, - contacts, areOptionsInitialized, - betas, didScreenTransitionEnd, - iouType, - isCategorizeOrShareAction, - options.personalDetails, options.reports, + options.personalDetails, + contacts, + draftComments, + betas, participants, + iouType, + action, + isCategorizeOrShareAction, isPerDiemRequest, canShowManagerMcTest, ]); diff --git a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx index 6b590a16bb928..c4a6f61bcfc7b 100644 --- a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx +++ b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx @@ -64,7 +64,7 @@ function useOptions() { currentUserOption, headerMessage, }; - }, [optionsList.reports, optionsList.personalDetails, betas, excludeLogins]); + }, [optionsList.reports, optionsList.personalDetails, draftComments, betas, excludeLogins]); const options = useMemo(() => { const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), countryCode, { diff --git a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx index 9092088d79c19..c61e6f616995b 100644 --- a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx @@ -70,7 +70,7 @@ function useOptions() { currentUserOption, headerMessage, }; - }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); + }, [optionsList.reports, optionsList.personalDetails, draftComments, betas, existingDelegates, isLoading]); const options = useMemo(() => { const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), countryCode, { diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 5dffaa561730e..eeb9e00954f7d 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -74,7 +74,7 @@ function useOptions() { currentUserOption, headerMessage, }; - }, [optionsList.reports, optionsList.personalDetails, betas, isLoading]); + }, [optionsList.reports, optionsList.personalDetails, draftComments, betas, isLoading]); const optionsWithoutCurrentUser = useMemo(() => { if (!session?.accountID) { diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 956e90bf67c49..0354f1af8a39b 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -96,7 +96,7 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { const inviteOptions = getMemberInviteOptions(options.personalDetails, draftComments, betas ?? [], excludedUsers, true); return {...inviteOptions, recentReports: [], currentUserOption: null}; - }, [areOptionsInitialized, betas, excludedUsers, options.personalDetails]); + }, [areOptionsInitialized, betas, draftComments, excludedUsers, options.personalDetails]); const inviteOptions = useMemo( () => filterAndOrderOptions(defaultOptions, debouncedSearchTerm, countryCode, {excludeLogins: excludedUsers}), From 104f226f1b20e491275e7041481546a8e49a5c8d Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Wed, 17 Sep 2025 13:53:08 +0300 Subject: [PATCH 14/25] fix lint --- src/libs/actions/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 97abffac36d38..135d5dd93af6c 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1784,7 +1784,7 @@ function saveReportDraft(reportID: string, report: Report) { * When empty string or null is passed, it will delete the draft comment from Onyx store. */ function saveReportDraftComment(reportID: string, comment: string | null, callback: () => void = () => {}) { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if comment was empty string ?? wont work + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, comment || null).then(callback); } From a71b87f960917f039593e7998c856b5d497729d2 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Thu, 18 Sep 2025 18:53:46 +0300 Subject: [PATCH 15/25] fix comments --- .../Search/SearchFiltersParticipantsSelector.tsx | 2 +- src/hooks/useSidebarOrderedReports.tsx | 7 ++++--- src/libs/ReportUtils.ts | 5 +---- src/libs/SidebarUtils.ts | 13 ++++++------- .../ComposerWithSuggestions.tsx | 2 +- tests/unit/useSidebarOrderedReportsTest.tsx | 3 ++- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index 735b6cfb25ce4..77ad09cca16f1 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -69,7 +69,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: includeCurrentUser: true, }, ); - }, [areOptionsInitialized, options.personalDetails, options.reports]); + }, [areOptionsInitialized, draftComments, options.personalDetails, options.reports]); const unselectedOptions = useMemo(() => { return filterSelectedOptions(defaultOptions, new Set(selectedOptions.map((option) => option.accountID))); diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index 6506b572642f6..f2fb8796261be 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -77,6 +77,7 @@ function SidebarOrderedReportsContextProvider({ const currentReportIDValue = useCurrentReportID(); const derivedCurrentReportID = currentReportIDForTests ?? currentReportIDValue?.currentReportIDFromPath ?? currentReportIDValue?.currentReportID; const prevDerivedCurrentReportID = usePrevious(derivedCurrentReportID); + const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${derivedCurrentReportID}`, {canBeMissing: true}); const policyMemberAccountIDs = useMemo(() => getPolicyEmployeeListByIdWithoutCurrentUser(policies, undefined, accountID), [policies, accountID]); const prevBetas = usePrevious(betas); @@ -155,7 +156,7 @@ function SidebarOrderedReportsContextProvider({ transactionViolations, reportNameValuePairs, reportAttributes, - reportsDrafts, + reportDraftComment: draftComment, }); } else { reportsToDisplay = SidebarUtils.getReportsToDisplayInLHN( @@ -164,7 +165,7 @@ function SidebarOrderedReportsContextProvider({ betas, policies, priorityMode, - reportsDrafts, + draftComment, transactionViolations, reportNameValuePairs, reportAttributes, @@ -173,7 +174,7 @@ function SidebarOrderedReportsContextProvider({ return reportsToDisplay; // Rule disabled intentionally — triggering a re-render on currentReportsToDisplay would cause an infinite loop // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes, reportsDrafts]); + }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes, draftComment]); const deepComparedReportsToDisplayInLHN = useDeepCompareRef(reportsToDisplayInLHN); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index aa14deb202347..4ff7fc7ef96e6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8575,12 +8575,9 @@ function reasonForReportToBeInOptionList({ return CONST.REPORT_IN_LHN_REASONS.IS_FOCUSED; } - // Retrieve the draft comment for the report and convert it to a boolean - const hasDraftComment = !!draftComment; - // Include reports that are relevant to the user in any view mode. Criteria include having a draft or having a GBR showing. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (hasDraftComment) { + if (!!draftComment) { return CONST.REPORT_IN_LHN_REASONS.HAS_DRAFT_COMMENT; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 03a41be3d53de..61ea57b1134ce 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -168,7 +168,7 @@ function shouldDisplayReportInLHN( isInFocusMode: boolean, betas: OnyxEntry, transactionViolations: OnyxCollection, - reportsDrafts: OnyxCollection | undefined, + draftComment: OnyxEntry, isReportArchived?: boolean, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { @@ -200,7 +200,6 @@ function shouldDisplayReportInLHN( // Check if report should override hidden status const isSystemChat = isSystemChatUtil(report); - const draftComment = reportsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`]; const shouldOverrideHidden = !!draftComment || hasErrorsOtherThanFailedReceipt || isFocused || isSystemChat || !!report.isPinned || reportAttributes?.[report?.reportID]?.requiresAttention; @@ -231,7 +230,7 @@ function getReportsToDisplayInLHN( betas: OnyxEntry, policies: OnyxCollection, priorityMode: OnyxEntry, - reportsDrafts: OnyxCollection | undefined, + reportDraft: OnyxEntry, transactionViolations: OnyxCollection, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], @@ -252,7 +251,7 @@ function getReportsToDisplayInLHN( isInFocusMode, betas, transactionViolations, - reportsDrafts, + reportDraft, isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]), reportAttributes, ); @@ -275,7 +274,7 @@ type UpdateReportsToDisplayInLHNProps = { transactionViolations: OnyxCollection; reportNameValuePairs?: OnyxCollection; reportAttributes?: ReportAttributesDerivedValue['reports']; - reportsDrafts: OnyxCollection | undefined; + reportDraftComment: OnyxEntry; }; function updateReportsToDisplayInLHN({ @@ -288,7 +287,7 @@ function updateReportsToDisplayInLHN({ transactionViolations, reportNameValuePairs, reportAttributes, - reportsDrafts, + reportDraftComment, }: UpdateReportsToDisplayInLHNProps) { const displayedReportsCopy = {...displayedReports}; updatedReportsKeys.forEach((reportID) => { @@ -304,7 +303,7 @@ function updateReportsToDisplayInLHN({ isInFocusMode, betas, transactionViolations, - reportsDrafts, + reportDraftComment, isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`] ?? {}), reportAttributes, ); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index aab07d03166b3..3c210eb9cc409 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -272,7 +272,7 @@ function ComposerWithSuggestions( const valueRef = useRef(value); valueRef.current = value; - const [selection, setSelection] = useState(() => ({start: value.length ?? 0, end: value.length ?? 0, positionX: 0, positionY: 0})); + const [selection, setSelection] = useState(() => ({start: value.length, end: value.length, positionX: 0, positionY: 0})); const [composerHeight, setComposerHeight] = useState(0); diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index 36caaec31608e..59d7be7800212 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -129,7 +129,7 @@ describe('useSidebarOrderedReports', () => { mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(newReportsWithSameContent); rerender({}); - + currentReportIDForTestsValue = '2'; // Then sortReportsToDisplayInLHN should not be called again since deep comparison shows no change expect(mockSidebarUtils.sortReportsToDisplayInLHN).not.toHaveBeenCalled(); }); @@ -240,6 +240,7 @@ describe('useSidebarOrderedReports', () => { mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(thirdReportsWithSameContent); rerender({}); + currentReportIDForTestsValue = '3'; // Then sortReportsToDisplayInLHN should be called only once (initial render) since the report content is the same expect(mockSidebarUtils.sortReportsToDisplayInLHN).toHaveBeenCalledTimes(1); From 53cab1626daa28f18fb727b1e921b233d9976ef6 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Mon, 29 Sep 2025 13:37:04 +0200 Subject: [PATCH 16/25] fix lint problems and tests --- src/hooks/useSidebarOrderedReports.tsx | 9 ++++----- src/libs/ReportUtils.ts | 2 +- src/libs/SidebarUtils.ts | 14 ++++++++++---- src/pages/Search/SearchTypeMenu.tsx | 1 - tests/perf-test/SidebarUtils.perf-test.ts | 4 ++-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index d97246c98f556..f88333fca56d2 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -80,7 +80,6 @@ function SidebarOrderedReportsContextProvider({ const currentReportIDValue = useCurrentReportID(); const derivedCurrentReportID = currentReportIDForTests ?? currentReportIDValue?.currentReportIDFromPath ?? currentReportIDValue?.currentReportID; const prevDerivedCurrentReportID = usePrevious(derivedCurrentReportID); - const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${derivedCurrentReportID}`, {canBeMissing: true}); const policyMemberAccountIDs = useMemo(() => getPolicyEmployeeListByIdWithoutCurrentUser(policies, undefined, accountID), [policies, accountID]); const prevBetas = usePrevious(betas); @@ -162,7 +161,7 @@ function SidebarOrderedReportsContextProvider({ transactionViolations, reportNameValuePairs, reportAttributes, - reportDraftComment: draftComment, + draftComments: reportsDrafts, }); } else { reportsToDisplay = SidebarUtils.getReportsToDisplayInLHN( @@ -171,7 +170,7 @@ function SidebarOrderedReportsContextProvider({ betas, policies, priorityMode, - draftComment, + reportsDrafts, transactionViolations, reportNameValuePairs, reportAttributes, @@ -181,7 +180,7 @@ function SidebarOrderedReportsContextProvider({ return reportsToDisplay; // Rule disabled intentionally — triggering a re-render on currentReportsToDisplay would cause an infinite loop // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes, draftComment]); + }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes]); const deepComparedReportsToDisplayInLHN = useDeepCompareRef(reportsToDisplayInLHN); @@ -193,7 +192,7 @@ function SidebarOrderedReportsContextProvider({ () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReportsToDisplayInLHN ?? {}, priorityMode, localeCompare, reportsDrafts, reportNameValuePairs, reportAttributes), // Rule disabled intentionally - reports should be sorted only when the reportsToDisplayInLHN changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - [reportsToDisplayInLHN, localeCompare, reportsDrafts], + [reportsToDisplayInLHN, localeCompare], ); const orderedReportIDs = useMemo(() => getOrderedReportIDs(), [getOrderedReportIDs]); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index abb5d44acdd49..7628aa7ed46e5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8611,7 +8611,7 @@ function reasonForReportToBeInOptionList({ // Include reports that are relevant to the user in any view mode. Criteria include having a draft or having a GBR showing. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (!!draftComment) { + if (draftComment) { return CONST.REPORT_IN_LHN_REASONS.HAS_DRAFT_COMMENT; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 391d963b81c70..cbb0d8bfb6923 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -222,7 +222,7 @@ function getReportsToDisplayInLHN( betas: OnyxEntry, policies: OnyxCollection, priorityMode: OnyxEntry, - reportDraft: OnyxEntry, + draftComments: OnyxCollection, transactionViolations: OnyxCollection, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], @@ -236,6 +236,8 @@ function getReportsToDisplayInLHN( return; } + const reportDraftComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`]; + const {shouldDisplay, hasErrorsOtherThanFailedReceipt} = shouldDisplayReportInLHN( report, reports, @@ -243,7 +245,7 @@ function getReportsToDisplayInLHN( isInFocusMode, betas, transactionViolations, - reportDraft, + reportDraftComment, isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]), reportAttributes, ); @@ -266,7 +268,7 @@ type UpdateReportsToDisplayInLHNProps = { transactionViolations: OnyxCollection; reportNameValuePairs?: OnyxCollection; reportAttributes?: ReportAttributesDerivedValue['reports']; - reportDraftComment: OnyxEntry; + draftComments: OnyxCollection; }; function updateReportsToDisplayInLHN({ @@ -279,7 +281,7 @@ function updateReportsToDisplayInLHN({ transactionViolations, reportNameValuePairs, reportAttributes, - reportDraftComment, + draftComments, }: UpdateReportsToDisplayInLHNProps) { const displayedReportsCopy = {...displayedReports}; updatedReportsKeys.forEach((reportID) => { @@ -288,6 +290,10 @@ function updateReportsToDisplayInLHN({ return; } + // Get the specific draft comment for this report instead of using a single draft comment for all reports + // This fixes the issue where the current report's draft comment was incorrectly used to filter all reports + const reportDraftComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`]; + const {shouldDisplay, hasErrorsOtherThanFailedReceipt} = shouldDisplayReportInLHN( report, reports, diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index f3d4bc7e9496d..c70e5e314efdd 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -137,7 +137,6 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { allFeeds, currentUserAccountID, allPolicies, - currentUserAccountID, ], ); diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 6c903f8332f34..3951852237435 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -94,11 +94,11 @@ describe('SidebarUtils', () => { test('[SidebarUtils] getReportsToDisplayInLHN on 15k reports for default priorityMode', async () => { await waitForBatchedUpdates(); - await measureFunction(() => SidebarUtils.getReportsToDisplayInLHN(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.DEFAULT, undefined, transactionViolations)); + await measureFunction(() => SidebarUtils.getReportsToDisplayInLHN(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.DEFAULT, {}, transactionViolations)); }); test('[SidebarUtils] getReportsToDisplayInLHN on 15k reports for GSD priorityMode', async () => { await waitForBatchedUpdates(); - await measureFunction(() => SidebarUtils.getReportsToDisplayInLHN(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.GSD, undefined, transactionViolations)); + await measureFunction(() => SidebarUtils.getReportsToDisplayInLHN(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.GSD, {}, transactionViolations)); }); }); From 00dc392f10f38df1e2acc784d1421970dd1fa1d1 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Thu, 2 Oct 2025 12:55:55 +0200 Subject: [PATCH 17/25] resolve comments --- src/pages/RoomInvitePage.tsx | 5 ++--- tests/unit/useSidebarOrderedReportsTest.tsx | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 2c2984b559ec3..7b415be0fc9dc 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -62,7 +62,6 @@ function RoomInvitePage({ const [selectedOptions, setSelectedOptions] = useState([]); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); const isReportArchived = useReportIsArchived(report.reportID); - const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const {options, areOptionsInitialized} = useOptionsList(); @@ -87,7 +86,7 @@ function RoomInvitePage({ return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null}; } - const inviteOptions = getMemberInviteOptions(options.personalDetails, draftComments, betas ?? [], excludedUsers); + const inviteOptions = getMemberInviteOptions(options.personalDetails, undefined, betas ?? [], excludedUsers); // Update selectedOptions with the latest personalDetails information const detailsMap: Record = {}; inviteOptions.personalDetails.forEach((detail) => { @@ -108,7 +107,7 @@ function RoomInvitePage({ recentReports: [], currentUserOption: null, }; - }, [areOptionsInitialized, betas, draftComments, excludedUsers, options.personalDetails, selectedOptions]); + }, [areOptionsInitialized, betas, excludedUsers, options.personalDetails, selectedOptions]); const inviteOptions = useMemo(() => { if (debouncedSearchTerm.trim() === '') { diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index 59d7be7800212..70c82b32592d4 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -129,7 +129,7 @@ describe('useSidebarOrderedReports', () => { mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(newReportsWithSameContent); rerender({}); - currentReportIDForTestsValue = '2'; + // Then sortReportsToDisplayInLHN should not be called again since deep comparison shows no change expect(mockSidebarUtils.sortReportsToDisplayInLHN).not.toHaveBeenCalled(); }); From 2f521e64a38f07e1a05c26797fc254e875cea5c0 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Fri, 3 Oct 2025 08:57:22 +0200 Subject: [PATCH 18/25] resolve comment --- tests/unit/useSidebarOrderedReportsTest.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index 70c82b32592d4..6a9d591fc42cb 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -129,6 +129,7 @@ describe('useSidebarOrderedReports', () => { mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(newReportsWithSameContent); rerender({}); + currentReportIDForTestsValue = '2'; // Then sortReportsToDisplayInLHN should not be called again since deep comparison shows no change expect(mockSidebarUtils.sortReportsToDisplayInLHN).not.toHaveBeenCalled(); @@ -234,6 +235,7 @@ describe('useSidebarOrderedReports', () => { mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(newReportsWithSameContent); rerender({}); + currentReportIDForTestsValue = '2'; // When the mock is updated const thirdReportsWithSameContent = createMockReports(reportsContent); @@ -265,7 +267,6 @@ describe('useSidebarOrderedReports', () => { // Then the mock calls are cleared mockSidebarUtils.sortReportsToDisplayInLHN.mockClear(); - currentReportIDForTestsValue = '2'; // When the priority mode is changed await Onyx.set(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); From 6db7638ef96a4e9f3f4c32f0f8fa5e12fdf40b58 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Fri, 3 Oct 2025 09:54:17 +0200 Subject: [PATCH 19/25] fix test --- tests/unit/useSidebarOrderedReportsTest.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index 6a9d591fc42cb..419d44fc7e191 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -270,6 +270,7 @@ describe('useSidebarOrderedReports', () => { // When the priority mode is changed await Onyx.set(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); + currentReportIDForTestsValue = '2'; rerender({}); From bcc85e50ef1e4db4318632c7ccee92416a4858ff Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Fri, 3 Oct 2025 10:17:31 +0200 Subject: [PATCH 20/25] fix test --- tests/unit/useSidebarOrderedReportsTest.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index 419d44fc7e191..3e5bf2875a8f6 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -129,7 +129,6 @@ describe('useSidebarOrderedReports', () => { mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(newReportsWithSameContent); rerender({}); - currentReportIDForTestsValue = '2'; // Then sortReportsToDisplayInLHN should not be called again since deep comparison shows no change expect(mockSidebarUtils.sortReportsToDisplayInLHN).not.toHaveBeenCalled(); @@ -244,7 +243,7 @@ describe('useSidebarOrderedReports', () => { rerender({}); currentReportIDForTestsValue = '3'; - // Then sortReportsToDisplayInLHN should be called only once (initial render) since the report content is the same + // Then sortReportsToDisplayInLHN should be called only once (initial render) expect(mockSidebarUtils.sortReportsToDisplayInLHN).toHaveBeenCalledTimes(1); }); @@ -267,10 +266,10 @@ describe('useSidebarOrderedReports', () => { // Then the mock calls are cleared mockSidebarUtils.sortReportsToDisplayInLHN.mockClear(); + currentReportIDForTestsValue = '2'; // When the priority mode is changed await Onyx.set(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); - currentReportIDForTestsValue = '2'; rerender({}); From ad8bb86179a0803b2c3c4065441e7eb7da61d300 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Fri, 3 Oct 2025 10:50:03 +0200 Subject: [PATCH 21/25] fix test finnally --- src/hooks/useSidebarOrderedReports.tsx | 2 +- src/libs/OptionsListUtils/index.ts | 13 ++++--------- .../BaseOnboardingWorkspaceInvite.tsx | 5 ++--- src/pages/RoomInvitePage.tsx | 2 +- src/pages/workspace/WorkspaceInvitePage.tsx | 5 ++--- tests/perf-test/OptionsListUtils.perf-test.ts | 2 +- tests/unit/OptionsListUtilsTest.tsx | 6 +++--- 7 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index f88333fca56d2..d31af37ed2a12 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -180,7 +180,7 @@ function SidebarOrderedReportsContextProvider({ return reportsToDisplay; // Rule disabled intentionally — triggering a re-render on currentReportsToDisplay would cause an infinite loop // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes]); + }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes, reportsDrafts]); const deepComparedReportsToDisplayInLHN = useDeepCompareRef(reportsToDisplayInLHN); diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 375b31c4a727b..d0ae9937c860b 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -2138,23 +2138,18 @@ function formatMemberForList(member: SearchOptionData): MemberForList { */ function getMemberInviteOptions( personalDetails: Array>, - draftComments: OnyxCollection, betas: Beta[] = [], excludeLogins: Record = {}, includeSelectedOptions = false, - reports: Array> = [], - includeRecentReports = false, - searchString = '', - maxElements?: number, ): Options { - return getValidOptions({reports, personalDetails}, draftComments, { + return getValidOptions({personalDetails, reports: []}, undefined, { betas, includeP2P: true, excludeLogins, includeSelectedOptions, - includeRecentReports, - searchString, - maxElements, + includeRecentReports: false, + searchString: '', + maxElements: undefined, }); } diff --git a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx index fbe509bc90578..2f11adbd48de3 100644 --- a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx +++ b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx @@ -65,7 +65,6 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo const {options, areOptionsInitialized} = useOptionsList({ shouldInitialize: didScreenTransitionEnd, }); - const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const welcomeNoteSubject = useMemo( () => `# ${currentUserPersonalDetails?.displayName ?? ''} invited you to ${policy?.name ?? 'a workspace'}`, @@ -90,10 +89,10 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null}; } - const inviteOptions = getMemberInviteOptions(options.personalDetails, draftComments, betas ?? [], excludedUsers, true); + const inviteOptions = getMemberInviteOptions(options.personalDetails, betas ?? [], excludedUsers, true); return {...inviteOptions, recentReports: [], currentUserOption: null}; - }, [areOptionsInitialized, betas, draftComments, excludedUsers, options.personalDetails]); + }, [areOptionsInitialized, betas, excludedUsers, options.personalDetails]); const inviteOptions = useMemo( () => filterAndOrderOptions(defaultOptions, debouncedSearchTerm, countryCode, {excludeLogins: excludedUsers}), diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 7b415be0fc9dc..5a02302729191 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -86,7 +86,7 @@ function RoomInvitePage({ return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null}; } - const inviteOptions = getMemberInviteOptions(options.personalDetails, undefined, betas ?? [], excludedUsers); + const inviteOptions = getMemberInviteOptions(options.personalDetails, betas ?? [], excludedUsers); // Update selectedOptions with the latest personalDetails information const detailsMap: Record = {}; inviteOptions.personalDetails.forEach((detail) => { diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 523efa1a6a2fb..6d1b6a259c4cd 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -59,7 +59,6 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { const firstRenderRef = useRef(true); const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: false}); const [invitedEmailsToAccountIDsDraft] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, {canBeMissing: true}); - const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList); @@ -93,10 +92,10 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null}; } - const inviteOptions = getMemberInviteOptions(options.personalDetails, draftComments, betas ?? [], excludedUsers, true); + const inviteOptions = getMemberInviteOptions(options.personalDetails, betas ?? [], excludedUsers, true); return {...inviteOptions, recentReports: [], currentUserOption: null}; - }, [areOptionsInitialized, betas, draftComments, excludedUsers, options.personalDetails]); + }, [areOptionsInitialized, betas, excludedUsers, options.personalDetails]); const inviteOptions = useMemo( () => filterAndOrderOptions(defaultOptions, debouncedSearchTerm, countryCode, {excludeLogins: excludedUsers}), diff --git a/tests/perf-test/OptionsListUtils.perf-test.ts b/tests/perf-test/OptionsListUtils.perf-test.ts index fd5e98731a0f0..15d8339e97fe6 100644 --- a/tests/perf-test/OptionsListUtils.perf-test.ts +++ b/tests/perf-test/OptionsListUtils.perf-test.ts @@ -141,7 +141,7 @@ describe('OptionsListUtils', () => { /* Testing getMemberInviteOptions */ test('[OptionsListUtils] getMemberInviteOptions', async () => { await waitForBatchedUpdates(); - await measureFunction(() => getMemberInviteOptions(options.personalDetails, {}, mockedBetas)); + await measureFunction(() => getMemberInviteOptions(options.personalDetails, mockedBetas)); }); test('[OptionsListUtils] worst case scenario with a search term that matches a subset of selectedOptions, filteredRecentReports, and filteredPersonalDetails', async () => { diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 7410e81a08428..dc1ead8d0281d 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -1188,7 +1188,7 @@ describe('OptionsListUtils', () => { it('should sort personal details alphabetically', () => { // Given a set of personalDetails // When we call getMemberInviteOptions - const results = getMemberInviteOptions(OPTIONS.personalDetails, {}, []); + const results = getMemberInviteOptions(OPTIONS.personalDetails, []); // Then personal details should be sorted alphabetically expect(results.personalDetails.at(0)?.text).toBe('Black Panther'); @@ -1431,7 +1431,7 @@ describe('OptionsListUtils', () => { it('should not return any options if search value does not match any personal details (getMemberInviteOptions)', () => { // Given a set of options - const options = getMemberInviteOptions(OPTIONS.personalDetails, {}, []); + const options = getMemberInviteOptions(OPTIONS.personalDetails, []); // When we call filterAndOrderOptions with a search value that does not match any personal details const filteredOptions = filterAndOrderOptions(options, 'magneto', COUNTRY_CODE); @@ -1441,7 +1441,7 @@ describe('OptionsListUtils', () => { it('should return one personal detail if search value matches an email (getMemberInviteOptions)', () => { // Given a set of options - const options = getMemberInviteOptions(OPTIONS.personalDetails, {}, []); + const options = getMemberInviteOptions(OPTIONS.personalDetails, []); // When we call filterAndOrderOptions with a search value that matches an email const filteredOptions = filterAndOrderOptions(options, 'peterparker@expensify.com', COUNTRY_CODE); From a1489ec6406f744e26a47452bcf6bb3b88df8e93 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Fri, 3 Oct 2025 15:44:53 +0200 Subject: [PATCH 22/25] fix test --- src/hooks/useSidebarOrderedReports.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index d31af37ed2a12..c0d2f4e76e801 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -192,7 +192,7 @@ function SidebarOrderedReportsContextProvider({ () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReportsToDisplayInLHN ?? {}, priorityMode, localeCompare, reportsDrafts, reportNameValuePairs, reportAttributes), // Rule disabled intentionally - reports should be sorted only when the reportsToDisplayInLHN changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - [reportsToDisplayInLHN, localeCompare], + [deepComparedReportsToDisplayInLHN, localeCompare], ); const orderedReportIDs = useMemo(() => getOrderedReportIDs(), [getOrderedReportIDs]); From c8e5abdeb73e98fa0c1946569cdcec8336ff0afe Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Mon, 6 Oct 2025 14:52:47 +0200 Subject: [PATCH 23/25] fix test --- src/hooks/useSidebarOrderedReports.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index c0d2f4e76e801..b139457b0eaac 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -183,16 +183,17 @@ function SidebarOrderedReportsContextProvider({ }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes, reportsDrafts]); const deepComparedReportsToDisplayInLHN = useDeepCompareRef(reportsToDisplayInLHN); + const deepComparedReportsDrafts = useDeepCompareRef(reportsDrafts); useEffect(() => { setCurrentReportsToDisplay(reportsToDisplayInLHN); }, [reportsToDisplayInLHN]); const getOrderedReportIDs = useCallback( - () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReportsToDisplayInLHN ?? {}, priorityMode, localeCompare, reportsDrafts, reportNameValuePairs, reportAttributes), + () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReportsToDisplayInLHN ?? {}, priorityMode, localeCompare, deepComparedReportsDrafts, reportNameValuePairs, reportAttributes), // Rule disabled intentionally - reports should be sorted only when the reportsToDisplayInLHN changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - [deepComparedReportsToDisplayInLHN, localeCompare], + [deepComparedReportsToDisplayInLHN, localeCompare, deepComparedReportsDrafts], ); const orderedReportIDs = useMemo(() => getOrderedReportIDs(), [getOrderedReportIDs]); From 4162b831182a43da307799ec2ac7e428d5826fc7 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Thu, 9 Oct 2025 08:26:22 +0200 Subject: [PATCH 24/25] fix ts problems --- tests/unit/OptionsListUtilsTest.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 20bdc75487b2a..727c0f7ac6075 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -1139,7 +1139,7 @@ describe('OptionsListUtils', () => { // Given a set of reports and personalDetails with multiple reports // When we call getValidOptions with maxRecentReportElements set to 2 const maxRecentReports = 2; - const results = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {maxRecentReportElements: maxRecentReports}); + const results = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}, {maxRecentReportElements: maxRecentReports}); // Then the recent reports should be limited to the specified number expect(results.recentReports.length).toBeLessThanOrEqual(maxRecentReports); @@ -1148,8 +1148,8 @@ describe('OptionsListUtils', () => { it('should show all reports when maxRecentReportElements is not specified', () => { // Given a set of reports and personalDetails // When we call getValidOptions without maxRecentReportElements - const resultsWithoutLimit = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); - const resultsWithLimit = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {maxRecentReportElements: 2}); + const resultsWithoutLimit = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); + const resultsWithLimit = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}, {maxRecentReportElements: 2}); // Then the results without limit should have more or equal reports expect(resultsWithoutLimit.recentReports.length).toBeGreaterThanOrEqual(resultsWithLimit.recentReports.length); @@ -1158,8 +1158,8 @@ describe('OptionsListUtils', () => { it('should not affect personalDetails count when maxRecentReportElements is specified', () => { // Given a set of reports and personalDetails // When we call getValidOptions with and without maxRecentReportElements - const resultsWithoutLimit = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}); - const resultsWithLimit = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {maxRecentReportElements: 2}); + const resultsWithoutLimit = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}); + const resultsWithLimit = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {}, {maxRecentReportElements: 2}); // Then personalDetails should remain the same regardless of maxRecentReportElements expect(resultsWithLimit.personalDetails.length).toBe(resultsWithoutLimit.personalDetails.length); @@ -1170,7 +1170,11 @@ describe('OptionsListUtils', () => { // When we call getValidOptions with both maxElements and maxRecentReportElements const maxRecentReports = 3; const maxTotalElements = 10; - const results = getValidOptions({reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, {maxElements: maxTotalElements, maxRecentReportElements: maxRecentReports}); + const results = getValidOptions( + {reports: OPTIONS.reports, personalDetails: OPTIONS.personalDetails}, + {}, + {maxElements: maxTotalElements, maxRecentReportElements: maxRecentReports}, + ); // Then recent reports should be limited by maxRecentReportElements expect(results.recentReports.length).toBeLessThanOrEqual(maxRecentReports); From ff5d3cc4c3a9811014a94483ae9bbf6cf35cfb05 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Thu, 9 Oct 2025 08:55:28 +0200 Subject: [PATCH 25/25] fix ts and lint --- src/hooks/useSearchSelector.base.ts | 2 +- tests/perf-test/OptionsListUtils.perf-test.ts | 1 + tests/unit/OptionsListUtilsTest.tsx | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSearchSelector.base.ts b/src/hooks/useSearchSelector.base.ts index 05658f868bc1b..64294892e548c 100644 --- a/src/hooks/useSearchSelector.base.ts +++ b/src/hooks/useSearchSelector.base.ts @@ -203,7 +203,7 @@ function useSearchSelectorBase({ loginsToExclude: excludeLogins, }); case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_SHARE_DESTINATION: - return getValidOptions(optionsWithContacts, { + return getValidOptions(optionsWithContacts, draftComments, { betas, selectedOptions, includeMultipleParticipantReports: true, diff --git a/tests/perf-test/OptionsListUtils.perf-test.ts b/tests/perf-test/OptionsListUtils.perf-test.ts index 9ad6daf92ae65..345b11ecd6ff1 100644 --- a/tests/perf-test/OptionsListUtils.perf-test.ts +++ b/tests/perf-test/OptionsListUtils.perf-test.ts @@ -138,6 +138,7 @@ describe('OptionsListUtils', () => { await measureFunction(() => getValidOptions( {reports: options.reports, personalDetails: options.personalDetails}, + {}, { betas: mockedBetas, includeMultipleParticipantReports: true, diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 578efe9fd6339..f430bc19e4523 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -1196,6 +1196,7 @@ describe('OptionsListUtils', () => { // When we call getValidOptions for share destination with an empty search value const results = getValidOptions( {reports: filteredReports, personalDetails: OPTIONS.personalDetails}, + {}, { betas: [], includeMultipleParticipantReports: true, @@ -1230,6 +1231,7 @@ describe('OptionsListUtils', () => { // When we call getValidOptions for share destination with an empty search value const results = getValidOptions( {reports: filteredReportsWithWorkspaceRooms, personalDetails: OPTIONS.personalDetails}, + {}, { betas: [], includeMultipleParticipantReports: true, @@ -1542,6 +1544,7 @@ describe('OptionsListUtils', () => { // When we call getValidOptions for share destination with the filteredReports const options = getValidOptions( {reports: filteredReports, personalDetails: OPTIONS.personalDetails}, + {}, { betas: [], includeMultipleParticipantReports: true, @@ -1578,6 +1581,7 @@ describe('OptionsListUtils', () => { // When we call getValidOptions for share destination with the filteredReports const options = getValidOptions( {reports: filteredReportsWithWorkspaceRooms, personalDetails: OPTIONS.personalDetails}, + {}, { betas: [], includeMultipleParticipantReports: true, @@ -1614,6 +1618,7 @@ describe('OptionsListUtils', () => { // When we call getValidOptions for share destination with the filteredReports const options = getValidOptions( {reports: filteredReportsWithWorkspaceRooms, personalDetails: OPTIONS.personalDetails}, + {}, { betas: [], includeMultipleParticipantReports: true,