From e389f173d3ca7445af2ea73e206e7242ca4743e6 Mon Sep 17 00:00:00 2001 From: "I.K." <54219858+M00rish@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:57:01 +0000 Subject: [PATCH 01/18] fix header --- src/pages/InviteReportParticipantsPage.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/InviteReportParticipantsPage.tsx b/src/pages/InviteReportParticipantsPage.tsx index af169f3fa875..04b863e00046 100644 --- a/src/pages/InviteReportParticipantsPage.tsx +++ b/src/pages/InviteReportParticipantsPage.tsx @@ -10,6 +10,7 @@ import type {WithNavigationTransitionEndProps} from '@components/withNavigationT import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useReportAttributes from '@hooks/useReportAttributes'; import useSearchSelector from '@hooks/useSearchSelector'; import useThemeStyles from '@hooks/useThemeStyles'; import {inviteToGroupChat, searchUserInServer} from '@libs/actions/Report'; @@ -22,9 +23,10 @@ import type {ParticipantsNavigatorParamList} from '@libs/Navigation/types'; import {getHeaderMessage} from '@libs/OptionsListUtils'; import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; -import {getGroupChatName} from '@libs/ReportNameUtils'; +import {getGroupChatName, getReportName} from '@libs/ReportNameUtils'; import type {OptionData} from '@libs/ReportUtils'; import {getParticipantsAccountIDsForDisplay} from '@libs/ReportUtils'; +import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -43,6 +45,7 @@ function InviteReportParticipantsPage({report}: InviteReportParticipantsPageProp const {translate, formatPhoneNumber} = useLocalize(); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const reportAttributes = useReportAttributes(); // Any existing participants and Expensify emails should not be eligible for invitation const excludedUsers: Record = { @@ -113,7 +116,7 @@ function InviteReportParticipantsPage({report}: InviteReportParticipantsPageProp }; const reportID = report.reportID; - const reportName = getGroupChatName(formatPhoneNumber, undefined, true, report); + const reportName = StringUtils.lineBreaksToSpaces(getReportName(report, reportAttributes)); const goBack = () => { Navigation.goBack(ROUTES.REPORT_PARTICIPANTS.getRoute(reportID, route.params.backTo)); From fbfb102e6ac797a89841a4da445f0d84bfce86ae Mon Sep 17 00:00:00 2001 From: "I.K." <54219858+M00rish@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:06:20 +0000 Subject: [PATCH 02/18] re-add fix --- src/pages/InviteReportParticipantsPage.tsx | 2 +- src/pages/ReportParticipantsPage.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/InviteReportParticipantsPage.tsx b/src/pages/InviteReportParticipantsPage.tsx index 04b863e00046..47fabfa32c8a 100644 --- a/src/pages/InviteReportParticipantsPage.tsx +++ b/src/pages/InviteReportParticipantsPage.tsx @@ -23,7 +23,7 @@ import type {ParticipantsNavigatorParamList} from '@libs/Navigation/types'; import {getHeaderMessage} from '@libs/OptionsListUtils'; import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; -import {getGroupChatName, getReportName} from '@libs/ReportNameUtils'; +import {getReportName} from '@libs/ReportNameUtils'; import type {OptionData} from '@libs/ReportUtils'; import {getParticipantsAccountIDsForDisplay} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index d752e8ff52da..b7e6034ebfb8 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -85,6 +85,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { const currentUserAccountID = Number(session?.accountID); const isCurrentUserAdmin = isGroupChatAdmin(report, currentUserAccountID); const isGroupChat = isGroupChatUtils(report); + const shouldShowInviteButton = isGroupChat || isMoneyRequestReport(report); const isCurrentUserGroupChatAdmin = isGroupChat && isCurrentUserAdmin; const isFocused = useIsFocused(); const {isOffline} = useNetwork(); @@ -341,7 +342,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { subtitle={StringUtils.lineBreaksToSpaces(getReportName(report, reportAttributes))} /> - {isGroupChat && ( + {shouldShowInviteButton && ( {(isSmallScreenWidth ? canSelectMultiple : selectedMembers.length > 0) ? ( @@ -368,7 +369,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { )} - + Date: Thu, 30 Apr 2026 23:39:30 +0000 Subject: [PATCH 03/18] updates picked --- src/libs/actions/RequestConflictUtils.ts | 7 +++- src/pages/InviteReportParticipantsPage.tsx | 24 +++++------ .../InviteReportParticipantsPageUtils.ts | 23 +++++++++++ src/pages/ReportParticipantsPage.tsx | 3 +- .../unit/InviteReportParticipantsPageTest.ts | 40 +++++++++++++++++++ tests/unit/RequestConflictUtilsTest.ts | 29 ++++++++++++++ 6 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 src/pages/InviteReportParticipantsPageUtils.ts create mode 100644 tests/unit/InviteReportParticipantsPageTest.ts diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index 001e76d95063..678cb4fc5948 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -75,7 +75,12 @@ function resolveDuplicationConflictAction(persistedRequests: AnyRequest[], reque function resolveOpenReportDuplicationConflictAction(persistedRequests: Array>, parameters: OpenReportParams): ConflictActionData { for (let index = 0; index < persistedRequests.length; index++) { const request = persistedRequests.at(index); - if (request?.command === WRITE_COMMANDS.OPEN_REPORT && request.data?.reportID === parameters.reportID && request.data?.emailList === parameters.emailList) { + if ( + request?.command === WRITE_COMMANDS.OPEN_REPORT && + request.data?.reportID === parameters.reportID && + (request.data?.emailList ?? '') === (parameters.emailList ?? '') && + (request.data?.accountIDList ?? '') === (parameters.accountIDList ?? '') + ) { // If the previous request had guided setup data, we can safely ignore the new request if (request.data.guidedSetupData) { return { diff --git a/src/pages/InviteReportParticipantsPage.tsx b/src/pages/InviteReportParticipantsPage.tsx index 47fabfa32c8a..f2d2c67e8ee4 100644 --- a/src/pages/InviteReportParticipantsPage.tsx +++ b/src/pages/InviteReportParticipantsPage.tsx @@ -1,5 +1,5 @@ import {useRoute} from '@react-navigation/native'; -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -10,7 +10,6 @@ import type {WithNavigationTransitionEndProps} from '@components/withNavigationT import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useReportAttributes from '@hooks/useReportAttributes'; import useSearchSelector from '@hooks/useSearchSelector'; import useThemeStyles from '@hooks/useThemeStyles'; import {inviteToGroupChat, searchUserInServer} from '@libs/actions/Report'; @@ -31,9 +30,10 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {InvitedEmailsToAccountIDs} from '@src/types/onyx'; +import {reportByIDsSelector} from '@src/selectors/Attributes'; import type {WithReportOrNotFoundProps} from './inbox/report/withReportOrNotFound'; import withReportOrNotFound from './inbox/report/withReportOrNotFound'; +import getInvitedEmailsToAccountIDs from './InviteReportParticipantsPageUtils'; type InviteReportParticipantsPageProps = WithReportOrNotFoundProps & WithNavigationTransitionEndProps; @@ -45,7 +45,11 @@ function InviteReportParticipantsPage({report}: InviteReportParticipantsPageProp const {translate, formatPhoneNumber} = useLocalize(); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const reportAttributes = useReportAttributes(); + const reportID = report.reportID; + const reportAttributesSelector = useMemo(() => reportByIDsSelector([reportID]), [reportID]); + const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, { + selector: reportAttributesSelector, + }); // Any existing participants and Expensify emails should not be eligible for invitation const excludedUsers: Record = { @@ -115,7 +119,6 @@ function InviteReportParticipantsPage({report}: InviteReportParticipantsPageProp toggleSelection(option); }; - const reportID = report.reportID; const reportName = StringUtils.lineBreaksToSpaces(getReportName(report, reportAttributes)); const goBack = () => { @@ -126,14 +129,9 @@ function InviteReportParticipantsPage({report}: InviteReportParticipantsPageProp if (selectedOptions.length === 0) { return; } - const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = {}; - for (const option of selectedOptions) { - const login = option.login ?? ''; - const accountID = option.accountID; - if (!login.toLowerCase().trim() || !accountID) { - continue; - } - invitedEmailsToAccountIDs[login] = accountID; + const invitedEmailsToAccountIDs = getInvitedEmailsToAccountIDs(selectedOptions); + if (Object.keys(invitedEmailsToAccountIDs).length === 0) { + return; } inviteToGroupChat(report, invitedEmailsToAccountIDs, formatPhoneNumber); goBack(); diff --git a/src/pages/InviteReportParticipantsPageUtils.ts b/src/pages/InviteReportParticipantsPageUtils.ts new file mode 100644 index 000000000000..bbd0df6b3346 --- /dev/null +++ b/src/pages/InviteReportParticipantsPageUtils.ts @@ -0,0 +1,23 @@ +import type {InvitedEmailsToAccountIDs} from '@src/types/onyx'; + +type InviteOption = { + accountID?: number; + login?: string; +}; + +function getInvitedEmailsToAccountIDs(selectedOptions: InviteOption[]): InvitedEmailsToAccountIDs { + const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = {}; + + for (const option of selectedOptions) { + const login = option.login ?? ''; + const accountID = option.accountID; + if (!login.trim() || accountID === undefined) { + continue; + } + invitedEmailsToAccountIDs[login] = accountID; + } + + return invitedEmailsToAccountIDs; +} + +export default getInvitedEmailsToAccountIDs; diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index b7e6034ebfb8..0a2180c9cbb3 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -106,6 +106,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { }; const [selectedMembers, setSelectedMembers] = useFilteredSelection(personalDetailsParticipants, filterParticipants); + const shouldShowBulkActionsDropdown = isGroupChat && (isSmallScreenWidth ? canSelectMultiple : selectedMembers.length > 0); // Get the active chat members by filtering out the pending members with delete action const activeParticipants = participantsForDisplay.filter((participant) => isOffline || !participant.isPendingDelete); @@ -344,7 +345,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { {shouldShowInviteButton && ( - {(isSmallScreenWidth ? canSelectMultiple : selectedMembers.length > 0) ? ( + {shouldShowBulkActionsDropdown ? ( shouldAlwaysShowDropdownMenu pressOnEnter diff --git a/tests/unit/InviteReportParticipantsPageTest.ts b/tests/unit/InviteReportParticipantsPageTest.ts new file mode 100644 index 000000000000..e4371d495bcd --- /dev/null +++ b/tests/unit/InviteReportParticipantsPageTest.ts @@ -0,0 +1,40 @@ +import getInvitedEmailsToAccountIDs from '@pages/InviteReportParticipantsPageUtils'; + +describe('InviteReportParticipantsPage', () => { + describe('getInvitedEmailsToAccountIDs', () => { + it('preserves optimistic account IDs for new invitees', () => { + // Given a typed invitee that does not have a real account yet + const login = 'new-user@example.com'; + const selectedOptions = [ + { + login, + accountID: 0, + }, + ]; + + // When building the invite map + const invitedEmailsToAccountIDs = getInvitedEmailsToAccountIDs(selectedOptions); + + // Then the optimistic accountID is preserved for the API layer + expect(Object.entries(invitedEmailsToAccountIDs)).toStrictEqual([[login, 0]]); + }); + + it('skips malformed selected options', () => { + // Given selected options that cannot be invited + const selectedOptions = [ + { + accountID: 123, + }, + { + login: 'missing-account-id@example.com', + }, + ]; + + // When building the invite map + const invitedEmailsToAccountIDs = getInvitedEmailsToAccountIDs(selectedOptions); + + // Then no invalid invitee is sent to the API layer + expect(invitedEmailsToAccountIDs).toStrictEqual({}); + }); + }); +}); diff --git a/tests/unit/RequestConflictUtilsTest.ts b/tests/unit/RequestConflictUtilsTest.ts index 74cc4e7992d0..d3c0c57aa035 100644 --- a/tests/unit/RequestConflictUtilsTest.ts +++ b/tests/unit/RequestConflictUtilsTest.ts @@ -6,6 +6,7 @@ import { resolveDuplicationConflictAction, resolveEditCommentWithNewAddCommentRequest, resolveEnableFeatureConflicts, + resolveOpenReportDuplicationConflictAction, } from '@libs/actions/RequestConflictUtils'; import {WRITE_COMMANDS} from '@libs/API/types'; import type {WriteCommand} from '@libs/API/types'; @@ -42,6 +43,34 @@ describe('RequestConflictUtils', () => { expect(result).toEqual({conflictAction: {type: 'replace', index: 2}}); }); + it.each([ + ['push when accountIDList differs', {emailList: '', accountIDList: '483'}, {emailList: '', accountIDList: ''}, {type: 'push'}], + ['replace when accountIDList matches', {emailList: '', accountIDList: '483'}, {emailList: '', accountIDList: '483'}, {type: 'replace', index: 0}], + ['replace when optional participant lists are empty', {}, {emailList: '', accountIDList: ''}, {type: 'replace', index: 0}], + ])('resolveOpenReportDuplicationConflictAction should %s', (_description, queuedData, nextData, expectedConflictAction) => { + const reportID = '8071514414018373'; + + // Given an existing OpenReport request + const persistedRequests = [ + { + command: WRITE_COMMANDS.OPEN_REPORT, + data: { + reportID, + ...queuedData, + }, + }, + ]; + + // When another OpenReport is queued for the same report + const result = resolveOpenReportDuplicationConflictAction(persistedRequests, { + reportID, + ...nextData, + }); + + // Then the request should only replace when participant identifiers match + expect(result).toEqual({conflictAction: expectedConflictAction}); + }); + it('resolveCommentDeletionConflicts should return push when no special comments are found', () => { const persistedRequests = [{command: 'OpenReport'}, {command: 'AddComment', data: {reportActionID: 2}}, {command: 'CloseAccount'}]; const reportActionID = '1'; From 3ccece246faed3e81732b2ceced59d56f5d316e9 Mon Sep 17 00:00:00 2001 From: "I.K." <54219858+M00rish@users.noreply.github.com> Date: Mon, 4 May 2026 04:34:06 +0000 Subject: [PATCH 04/18] lint --- src/pages/InviteReportParticipantsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/InviteReportParticipantsPage.tsx b/src/pages/InviteReportParticipantsPage.tsx index f2d2c67e8ee4..35d82458e0e1 100644 --- a/src/pages/InviteReportParticipantsPage.tsx +++ b/src/pages/InviteReportParticipantsPage.tsx @@ -30,7 +30,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import {reportByIDsSelector} from '@src/selectors/Attributes'; +import reportByIDsSelector from '@src/selectors/Attributes'; import type {WithReportOrNotFoundProps} from './inbox/report/withReportOrNotFound'; import withReportOrNotFound from './inbox/report/withReportOrNotFound'; import getInvitedEmailsToAccountIDs from './InviteReportParticipantsPageUtils'; From d0ef64b4a23bd7ff67e97ea0a2efc11502f52934 Mon Sep 17 00:00:00 2001 From: "I.K." <54219858+M00rish@users.noreply.github.com> Date: Thu, 18 Jun 2026 10:39:38 +0000 Subject: [PATCH 05/18] gate invite page --- src/pages/DynamicReportParticipantsInvitePage.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/DynamicReportParticipantsInvitePage.tsx b/src/pages/DynamicReportParticipantsInvitePage.tsx index 47cdb4249083..16ba3b133e05 100644 --- a/src/pages/DynamicReportParticipantsInvitePage.tsx +++ b/src/pages/DynamicReportParticipantsInvitePage.tsx @@ -1,4 +1,5 @@ import React, {useEffect, useMemo, useState} from 'react'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -22,7 +23,7 @@ import type {OptionData} from '@libs/PersonalDetailOptionsListUtils'; import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; import {getReportName} from '@libs/ReportNameUtils'; -import {getParticipantsAccountIDsForDisplay} from '@libs/ReportUtils'; +import {getParticipantsAccountIDsForDisplay, isGroupChat, isMoneyRequestReport, isOpenExpenseReport} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -47,6 +48,7 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants selector: reportAttributesSelector, }); const backPath = useDynamicBackPath(DYNAMIC_ROUTES.REPORT_PARTICIPANTS_INVITE.path); + const shouldShowInvitePage = isGroupChat(report) || (isMoneyRequestReport(report) && isOpenExpenseReport(report)); // Any existing participants and Expensify emails should not be eligible for invitation const excludedUsers: Record = { @@ -132,6 +134,10 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants goBack(); }; + if (!shouldShowInvitePage) { + return ; + } + const getHeaderMessageText = () => { if (sections.length > 0) { return ''; From 3ff45ccfc3c8ef5ecb6dcfd6399dc3f15d52cdd2 Mon Sep 17 00:00:00 2001 From: "I.K." <54219858+M00rish@users.noreply.github.com> Date: Fri, 19 Jun 2026 05:38:52 +0000 Subject: [PATCH 06/18] fix tests --- src/libs/actions/RequestConflictUtils.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index a18ed413f196..f3ffa04056b9 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -75,12 +75,21 @@ function resolveDuplicationConflictAction(persistedRequests: AnyRequest[], reque function resolveOpenReportDuplicationConflictAction(persistedRequests: Array>, parameters: OpenReportParams): ConflictActionData { for (let index = 0; index < persistedRequests.length; index++) { const request = persistedRequests.at(index); - if ( - request?.command === WRITE_COMMANDS.OPEN_REPORT && - request.data?.reportID === parameters.reportID && - (request.data?.emailList ?? '') === (parameters.emailList ?? '') && - (request.data?.accountIDList ?? '') === (parameters.accountIDList ?? '') - ) { + + // Skip irrelevant requests immediately + if (request?.command !== WRITE_COMMANDS.OPEN_REPORT || request.data?.reportID !== parameters.reportID) { + continue; + } + + const queuedHasParticipants = !!(request.data?.emailList ?? request.data?.accountIDList); + const newHasParticipants = !!(parameters.emailList ?? parameters.accountIDList); + + const isExactParticipantMatch = (request.data?.emailList ?? '') === (parameters.emailList ?? '') && (request.data?.accountIDList ?? '') === (parameters.accountIDList ?? ''); + + // True only if the caller completely omitted the participant keys (e.g., a simple follow-up request) + const isNewParticipantsUndefined = parameters.emailList === undefined && parameters.accountIDList === undefined; + + if (isExactParticipantMatch || (!queuedHasParticipants && newHasParticipants) || (queuedHasParticipants && isNewParticipantsUndefined)) { // If the previous request had guided setup data, we can safely ignore the new request if (request.data.guidedSetupData) { return { From 295809c09c700f8b3fd8292e1c0eaee14a581310 Mon Sep 17 00:00:00 2001 From: "I.K." <54219858+M00rish@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:11:13 +0000 Subject: [PATCH 07/18] remove duplicate consts --- src/libs/actions/RequestConflictUtils.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index f3ffa04056b9..362f7ce93dad 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -81,8 +81,10 @@ function resolveOpenReportDuplicationConflictAction(persis continue; } - const queuedHasParticipants = !!(request.data?.emailList ?? request.data?.accountIDList); - const newHasParticipants = !!(parameters.emailList ?? parameters.accountIDList); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const queuedHasParticipants = !!(request.data?.emailList || request.data?.accountIDList); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const newHasParticipants = !!(parameters.emailList || parameters.accountIDList); const isExactParticipantMatch = (request.data?.emailList ?? '') === (parameters.emailList ?? '') && (request.data?.accountIDList ?? '') === (parameters.accountIDList ?? ''); @@ -104,10 +106,6 @@ function resolveOpenReportDuplicationConflictAction(persis // ReportFetchHandler when the screen mounts has no participants. Replacing would drop the // accountIDList, leaving the server with no way to resolve the optimistic reportID — Auth // returns NIL reportSummary and PHP throws "Report not found" (da7984df). - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const queuedHasParticipants = !!(request.data?.emailList || request.data?.accountIDList); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const newHasParticipants = !!(parameters.emailList || parameters.accountIDList); if (queuedHasParticipants && !newHasParticipants) { return { conflictAction: { From 4c18664f300339ab0c63cd4db5e8a992a9586ba1 Mon Sep 17 00:00:00 2001 From: "I.K." <54219858+M00rish@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:13:52 +0000 Subject: [PATCH 08/18] review fix --- src/libs/actions/RequestConflictUtils.ts | 5 +---- tests/unit/RequestConflictUtilsTest.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index 362f7ce93dad..a0f223f5fb11 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -88,10 +88,7 @@ function resolveOpenReportDuplicationConflictAction(persis const isExactParticipantMatch = (request.data?.emailList ?? '') === (parameters.emailList ?? '') && (request.data?.accountIDList ?? '') === (parameters.accountIDList ?? ''); - // True only if the caller completely omitted the participant keys (e.g., a simple follow-up request) - const isNewParticipantsUndefined = parameters.emailList === undefined && parameters.accountIDList === undefined; - - if (isExactParticipantMatch || (!queuedHasParticipants && newHasParticipants) || (queuedHasParticipants && isNewParticipantsUndefined)) { + if (isExactParticipantMatch || (!queuedHasParticipants && newHasParticipants) || (queuedHasParticipants && !newHasParticipants)) { // If the previous request had guided setup data, we can safely ignore the new request if (request.data.guidedSetupData) { return { diff --git a/tests/unit/RequestConflictUtilsTest.ts b/tests/unit/RequestConflictUtilsTest.ts index 35ad44817434..8a679197f4a4 100644 --- a/tests/unit/RequestConflictUtilsTest.ts +++ b/tests/unit/RequestConflictUtilsTest.ts @@ -44,7 +44,7 @@ describe('RequestConflictUtils', () => { }); it.each([ - ['push when accountIDList differs', {emailList: '', accountIDList: '483'}, {emailList: '', accountIDList: ''}, {type: 'push'}], + ['push when accountIDList differs', {emailList: '', accountIDList: '483'}, {emailList: '', accountIDList: ''}, {type: 'noAction'}], ['replace when accountIDList matches', {emailList: '', accountIDList: '483'}, {emailList: '', accountIDList: '483'}, {type: 'replace', index: 0}], ['replace when optional participant lists are empty', {}, {emailList: '', accountIDList: ''}, {type: 'replace', index: 0}], ])('resolveOpenReportDuplicationConflictAction should %s', (_description, queuedData, nextData, expectedConflictAction) => { @@ -228,6 +228,18 @@ describe('RequestConflictUtils', () => { const result = resolveOpenReportDuplicationConflictAction(persistedRequests, {reportID: '1', accountIDList: '10,20'} as never); expect(result).toEqual({conflictAction: {type: 'replace', index: 0}}); }); + + it('should return noAction when queued request has participants but new follow-up request has empty participant strings', () => { + const persistedRequests = [{command: WRITE_COMMANDS.OPEN_REPORT, data: {reportID: '1', accountIDList: '483'}}]; + const result = resolveOpenReportDuplicationConflictAction(persistedRequests, {reportID: '1', emailList: '', accountIDList: ''} as never); + expect(result).toEqual({conflictAction: {type: 'noAction'}}); + }); + + it('should push when accountIDList genuinely differs (distinct active lists)', () => { + const persistedRequests = [{command: WRITE_COMMANDS.OPEN_REPORT, data: {reportID: '1', accountIDList: '483'}}]; + const result = resolveOpenReportDuplicationConflictAction(persistedRequests, {reportID: '1', accountIDList: '484'} as never); + expect(result).toEqual({conflictAction: {type: 'push'}}); + }); }); describe('resolveDetachReceiptConflicts', () => { From 6e49efa6e7c1b5720f80b28f672601594ab51f7a Mon Sep 17 00:00:00 2001 From: "I.K." <54219858+M00rish@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:07:21 +0000 Subject: [PATCH 09/18] limit invite to submitter and admin --- src/pages/DynamicReportParticipantsPage.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pages/DynamicReportParticipantsPage.tsx b/src/pages/DynamicReportParticipantsPage.tsx index 734e9cc6f733..0928e80533b0 100755 --- a/src/pages/DynamicReportParticipantsPage.tsx +++ b/src/pages/DynamicReportParticipantsPage.tsx @@ -17,6 +17,7 @@ import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; import useReportAttributes from '@hooks/useReportAttributes'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -30,6 +31,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ParticipantsNavigatorParamList} from '@libs/Navigation/types'; import {temporaryGetDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; +import {isPolicyAdmin} from '@libs/PolicyUtils'; import {getReportName} from '@libs/ReportNameUtils'; import { getReportPersonalDetailsParticipants, @@ -42,7 +44,6 @@ import { isGroupChat as isGroupChatUtils, isMoneyRequestReport, isOpenExpenseReport, - isPolicyAdmin, isPolicyExpenseChat, isSelfDM, isTaskReport, @@ -89,6 +90,7 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr const currentUserAccountID = Number(session?.accountID); const isCurrentUserAdmin = isGroupChatAdmin(report, currentUserAccountID); const isGroupChat = isGroupChatUtils(report); + const policy = usePolicy(report.policyID); const isReportSubmitterOrAdmin = isCurrentUserSubmitter(report) || isPolicyAdmin(policy); const shouldShowInviteButton = isReportSubmitterOrAdmin && (isGroupChat || (isMoneyRequestReport(report) && isOpenExpenseReport(report))); const isCurrentUserGroupChatAdmin = isGroupChat && isCurrentUserAdmin; @@ -248,10 +250,6 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr ? translate('common.members') : translate('common.details'); - const memberNotFoundMessage = shouldShowInviteButton - ? `${translate('roomMembersPage.memberNotFound')} ${translate('roomMembersPage.useInviteButton')}` - : translate('roomMembersPage.memberNotFound'); - return ( Date: Thu, 9 Jul 2026 12:43:32 +0000 Subject: [PATCH 10/18] eslint --- config/eslint/eslint.seatbelt.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index ce14a7781e80..a8c3fec9b513 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -2045,7 +2045,7 @@ "../../tests/unit/ReportUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 19 "../../tests/unit/ReportUtilsTest.ts" "no-restricted-imports" 1 "../../tests/unit/ReportWelcomeTextTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 -"../../tests/unit/RequestConflictUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 11 +"../../tests/unit/RequestConflictUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 13 "../../tests/unit/RequestTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../tests/unit/RequireA11yDisableJustificationRuleTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../tests/unit/RequireLiveRegionForStatusUpdatesRuleTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 From 07790da86be48e483c0575f27593af365096a8b5 Mon Sep 17 00:00:00 2001 From: "I.K." <54219858+M00rish@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:03:44 +0000 Subject: [PATCH 11/18] gate for chat admins too --- src/pages/DynamicReportParticipantsInvitePage.tsx | 9 +++++++-- src/pages/DynamicReportParticipantsPage.tsx | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pages/DynamicReportParticipantsInvitePage.tsx b/src/pages/DynamicReportParticipantsInvitePage.tsx index c365404cfdec..94b0ebf6f20f 100644 --- a/src/pages/DynamicReportParticipantsInvitePage.tsx +++ b/src/pages/DynamicReportParticipantsInvitePage.tsx @@ -12,6 +12,7 @@ import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePersonalDetailSearchSelector from '@hooks/usePersonalDetailSearchSelector'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import {inviteToGroupChat, searchUserInServer} from '@libs/actions/Report'; @@ -24,7 +25,7 @@ import type {OptionData} from '@libs/PersonalDetailOptionsListUtils'; import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; import {getReportName} from '@libs/ReportNameUtils'; -import {getParticipantsAccountIDsForDisplay, isGroupChat, isMoneyRequestReport, isOpenExpenseReport} from '@libs/ReportUtils'; +import {getParticipantsAccountIDsForDisplay, isCurrentUserSubmitter, isGroupChat, isGroupChatAdmin, isMoneyRequestReport, isOpenExpenseReport, isPolicyAdmin} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; @@ -55,7 +56,11 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants selector: reportAttributesSelector, }); const backPath = useDynamicBackPath(DYNAMIC_ROUTES.REPORT_PARTICIPANTS_INVITE.path); - const shouldShowInvitePage = isGroupChat(report) || (isMoneyRequestReport(report) && isOpenExpenseReport(report)); + const [session] = useOnyx(ONYXKEYS.SESSION); + const currentUserAccountID = Number(session?.accountID); + const policy = usePolicy(report.policyID); + const isReportSubmitterOrAdmin = isCurrentUserSubmitter(report) || isPolicyAdmin(policy) || (isGroupChat(report) && isGroupChatAdmin(report, currentUserAccountID)); + const shouldShowInvitePage = isReportSubmitterOrAdmin && (isGroupChat(report) || (isMoneyRequestReport(report) && isOpenExpenseReport(report))); // Any existing participants and Expensify emails should not be eligible for invitation const excludedUsers: Record = { diff --git a/src/pages/DynamicReportParticipantsPage.tsx b/src/pages/DynamicReportParticipantsPage.tsx index bae25d87b5e1..5a7e56130213 100755 --- a/src/pages/DynamicReportParticipantsPage.tsx +++ b/src/pages/DynamicReportParticipantsPage.tsx @@ -91,7 +91,7 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr const isCurrentUserAdmin = isGroupChatAdmin(report, currentUserAccountID); const isGroupChat = isGroupChatUtils(report); const policy = usePolicy(report.policyID); - const isReportSubmitterOrAdmin = isCurrentUserSubmitter(report) || isPolicyAdmin(policy); + const isReportSubmitterOrAdmin = isCurrentUserSubmitter(report) || isPolicyAdmin(policy) || (isGroupChat && isCurrentUserAdmin); const shouldShowInviteButton = isReportSubmitterOrAdmin && (isGroupChat || (isMoneyRequestReport(report) && isOpenExpenseReport(report))); const isCurrentUserGroupChatAdmin = isGroupChat && isCurrentUserAdmin; const {isOffline} = useNetwork(); From 50c0bc071905ae0255392004db4b39fcc7532b05 Mon Sep 17 00:00:00 2001 From: "I.K." <54219858+M00rish@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:17:10 +0000 Subject: [PATCH 12/18] fix --- config/webpack/certificate.pem | 25 ------------------------- config/webpack/key.pem | 28 ---------------------------- 2 files changed, 53 deletions(-) delete mode 100644 config/webpack/certificate.pem delete mode 100644 config/webpack/key.pem diff --git a/config/webpack/certificate.pem b/config/webpack/certificate.pem deleted file mode 100644 index a2b215f73c87..000000000000 --- a/config/webpack/certificate.pem +++ /dev/null @@ -1,25 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIERzCCAq+gAwIBAgIQfiyCYEkgwgZeH2GiPn8f/DANBgkqhkiG9w0BAQsFADBz -MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExJDAiBgNVBAsMG2NvZGVz -cGFjZUBjb2Rlc3BhY2VzLWFlNjQ1ZDErMCkGA1UEAwwibWtjZXJ0IGNvZGVzcGFj -ZUBjb2Rlc3BhY2VzLWFlNjQ1ZDAeFw0yNjAzMjAwNTE0MjlaFw0yODA2MjAwNTE0 -MjlaME8xJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEk -MCIGA1UECwwbY29kZXNwYWNlQGNvZGVzcGFjZXMtYWU2NDVkMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvgeQ6+q1ALEmMC7YksIW/v3X2L4xzDUmAjZ4 -fyhi/+Q1Dx9hSn36Vf71w8ySE9DOQ/F9ncLjFge4di9yWFYQAwHu5Lmo5MFArvbA -p9GuDrQtqm1wi56ZjpuGmJ5V2yBAM089XomX03s1mvGrmRiMP97uT6SGfcabtBAE -PLh+JtmIxS6H9E/6/vNhUgex3RxkvXp4bZCHcHef9mSnFJXM7x0MRb+57KqQhxCv -UMMyzc6yKuAxZhJ9VGhbhQ+uJRE6o7UXH/7oxwCQ8JwDHER7eklj755b5kuhKxyw -FOUj5ZzHeLPCO75M8hoEIlb54hNnC7RQ2/QimZFisIxyulyRdQIDAQABo3sweTAO -BgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAU -vuhyqpKvit854egmlwmRiwefMCowMQYDVR0RBCowKIIVZGV2Lm5ldy5leHBlbnNp -ZnkuY29tgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggGBABSoQD47 -ACrOQ1ncfSLqfR+TPtEAU8PnVzsViFaGoT2SnxyO6PKqZ36W5DRNNOaJEImmNbQN -vBELtJ5P6KVDctw2XGXbP9zPtBfqeEfZqmEII+8tLOhWgrr9C6L7fAA5IIJGTbOs -5LcjK1bJxndbIc5lPI+wSYwiy54GveMnSV83XLahWKP/jE0sNAmlaoyVZnB/CrGV -G2W5HhNDIq6YdzBqIy+deTM2X3JzH7vEvoxZhbn6TPeIT+KE2r+/VX++9eOvbBKa -sD42UgUhqM2oaPfJFGByjzy/4OrOkA+gzJsrXlXe5P7L10Y+aQOm8qyeFEbJCaKI -eC3pLrCkU8F+ERbOgqxuP3TBhdqKQ9dCSwkISdFnBuGGXWPc2cVBjDqcscVatUr8 -nkzBoXlDsNyBXvhB0MzyIm6a3YR9OfsadASxK+vZZZxKa04n2YHkESxpal/Tc+oS -16HVTSw1sQI3qe4VueNINy836AHWenujBTbAWbHhkcLrwtvj9Ff/rZzf/g== ------END CERTIFICATE----- diff --git a/config/webpack/key.pem b/config/webpack/key.pem deleted file mode 100644 index 95a568c71d25..000000000000 --- a/config/webpack/key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+B5Dr6rUAsSYw -LtiSwhb+/dfYvjHMNSYCNnh/KGL/5DUPH2FKffpV/vXDzJIT0M5D8X2dwuMWB7h2 -L3JYVhADAe7kuajkwUCu9sCn0a4OtC2qbXCLnpmOm4aYnlXbIEAzTz1eiZfTezWa -8auZGIw/3u5PpIZ9xpu0EAQ8uH4m2YjFLof0T/r+82FSB7HdHGS9enhtkIdwd5/2 -ZKcUlczvHQxFv7nsqpCHEK9QwzLNzrIq4DFmEn1UaFuFD64lETqjtRcf/ujHAJDw -nAMcRHt6SWPvnlvmS6ErHLAU5SPlnMd4s8I7vkzyGgQiVvniE2cLtFDb9CKZkWKw -jHK6XJF1AgMBAAECggEALNkGR9dVrnsggyCbNk+eM8He7iU9BW/Jr54BZCk47CGE -sDP8tyLyMNeCUxp1oz2VYI2e0tvK0kkyLJpYBycQklUjd1Gl3Y8KYACWSUUAKjI1 -0NIRTLI6lWB5LI25v4RmAnYCCsy4S035wb6le/VCLFTFs4AlyYtU9fMNvPLhuvMV -63EpcwiQeKSxOrzKOWkTGqXGytDgc3LJGI5ewNB592Chb+N5A4SyYApO6AsuhVA4 -vhScn/9kq7EfeKYxLO1VpcJs4pk+3+GaQbgWQ5RknrTL/nH6QZ3HUpnTY1DHn2Mt -RBRHk14eVY9knBUIXNQj3NfRw9CW/CmYb+oFK59HgQKBgQDc9z9WRzyQaf8KIm7h -6q5be8/UYy4ce5UhPSi66+FOYMsh/VRvgsdGGp/lyJDDXS4/nuRKmF9SZMABEqx+ -HGnFaDafwfXQJYqyN6c5xGiachrwfy2bFxEneSxPXo9NR2IV5EYuKexXKLXiS4nx -XNy2CWU+nuZ0tpAFbJvZxYxTIQKBgQDcKKaq8di3glkXQecEpKqjhRUPDUlSTmQA -HdNYiMgtdOdcS/Ivmb4ePF01NZqu20m74YP95i3V3Fh5RO0pqNisFQLIppP4jVN5 -VwyWb8xAUQielao5dYk2E5KyfX7BHXaEG+RXFWq3tTP3A0fFVzWFeqz5QIG9oZWw -ED4KOSWH1QKBgFiJDKyyQFcQ0WqrH0iP8qqfMkfEMY6iCb/WtIIozw7Lg7iru3Au -s0+ojVbRhQdyR3ygWpJ0+QSXb8M+SzUAh3Tca76ZRbfPwwHjgLKJuuvfOmIQYyKY -HHx3dcSbe8mYB3oEDAalL5hArJfucmfuMFUT8KglnOs41v4FZXDdUXthAoGBALob -3zC4+1O96+Q+QLVrIhjTQocT2zypqd1zWRsJnfbXUVFzngrhM5dJvh1h8B4KoB0k -ObkjoPGdsGjrfHTwGCH4LP9D2eiOR9dWX/y4C3j/R4MPz/N8yX4eM6+RaNqFNo/d -KDz5jNo5YvFYI47N9x+cIacfAc41T12jjV6FCpX1AoGAT+d4Y6NCRv4zI1dZsNTp -NQ+4D8pEji7ldA9zLTMoukmBK3GhGLWObgDsI49kNOrAWE2GLDudrDkKhUSUMs9a -SLcBa/D1TrPwbH5xleoAcPMC8AvEZPZHqv4dAHBFMS3VAkpeJGCooC76r0z11qu0 -pPkCT5+TbzMfdNMc/hwVCKU= ------END PRIVATE KEY----- From 4bc82c750f9db1d94eafdbd0275b4c5a0b2b209b Mon Sep 17 00:00:00 2001 From: "I.K." <54219858+M00rish@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:18:42 +0000 Subject: [PATCH 13/18] lint --- src/pages/DynamicReportParticipantsInvitePage.tsx | 3 --- src/pages/DynamicReportParticipantsPage.tsx | 1 - 2 files changed, 4 deletions(-) diff --git a/src/pages/DynamicReportParticipantsInvitePage.tsx b/src/pages/DynamicReportParticipantsInvitePage.tsx index 5fd4bf110e38..7def34d6e20e 100644 --- a/src/pages/DynamicReportParticipantsInvitePage.tsx +++ b/src/pages/DynamicReportParticipantsInvitePage.tsx @@ -56,9 +56,6 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const reportID = report.reportID; const reportAttributesSelector = useMemo(() => reportByIDsSelector([reportID]), [reportID]); - const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, { - selector: reportAttributesSelector, - }); const backPath = useDynamicBackPath(DYNAMIC_ROUTES.REPORT_PARTICIPANTS_INVITE.path); const [session] = useOnyx(ONYXKEYS.SESSION); const currentUserAccountID = Number(session?.accountID); diff --git a/src/pages/DynamicReportParticipantsPage.tsx b/src/pages/DynamicReportParticipantsPage.tsx index 8deb923a8535..d56be53afcca 100755 --- a/src/pages/DynamicReportParticipantsPage.tsx +++ b/src/pages/DynamicReportParticipantsPage.tsx @@ -83,7 +83,6 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr const tableRef = useRef>(null); const isReportArchived = useReportIsArchived(report?.reportID); const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`); - const reportAttributes = useReportAttributes(); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const [session] = useOnyx(ONYXKEYS.SESSION); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); From b326c17315c4b2b69d84e84746413e92da6d4ecd Mon Sep 17 00:00:00 2001 From: "I.K." <54219858+M00rish@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:27:30 +0000 Subject: [PATCH 14/18] lint --- src/pages/DynamicReportParticipantsInvitePage.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/DynamicReportParticipantsInvitePage.tsx b/src/pages/DynamicReportParticipantsInvitePage.tsx index 7def34d6e20e..f72f15d12b24 100644 --- a/src/pages/DynamicReportParticipantsInvitePage.tsx +++ b/src/pages/DynamicReportParticipantsInvitePage.tsx @@ -34,8 +34,7 @@ import {newAccountIDsAndLoginsSelector, personalDetailsLoginsSelector} from '@sr import type {InvitedEmailsToAccountIDs} from '@src/types/onyx'; import getEmptyArray from '@src/types/utils/getEmptyArray'; -import reportByIDsSelector from '@selectors/ReportAttributes'; -import React, {useEffect, useMemo, useState} from 'react'; +import {useEffect, useState} from 'react'; import type {WithReportOrNotFoundProps} from './inbox/report/withReportOrNotFound'; @@ -54,8 +53,6 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants selector: personalDetailsLoginsSelector(getParticipantsAccountIDsForDisplay(report, false, true)), }); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const reportID = report.reportID; - const reportAttributesSelector = useMemo(() => reportByIDsSelector([reportID]), [reportID]); const backPath = useDynamicBackPath(DYNAMIC_ROUTES.REPORT_PARTICIPANTS_INVITE.path); const [session] = useOnyx(ONYXKEYS.SESSION); const currentUserAccountID = Number(session?.accountID); From 616304d772c68ebd19a2132cd1592bbdab15c69e Mon Sep 17 00:00:00 2001 From: M00rish Date: Sat, 25 Jul 2026 01:36:22 +0100 Subject: [PATCH 15/18] lint --- src/pages/DynamicReportParticipantsInvitePage.tsx | 2 +- src/pages/DynamicReportParticipantsPage.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/DynamicReportParticipantsInvitePage.tsx b/src/pages/DynamicReportParticipantsInvitePage.tsx index f72f15d12b24..e74de1c5774b 100644 --- a/src/pages/DynamicReportParticipantsInvitePage.tsx +++ b/src/pages/DynamicReportParticipantsInvitePage.tsx @@ -142,7 +142,7 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants if (selectedOptions.length === 0) { return; } - const invitedEmailsToAccountIDs = getInvitedEmailsToAccountIDs(selectedOptions); + if (Object.keys(invitedEmailsToAccountIDs).length === 0) { return; } diff --git a/src/pages/DynamicReportParticipantsPage.tsx b/src/pages/DynamicReportParticipantsPage.tsx index d56be53afcca..b89f9ab58433 100755 --- a/src/pages/DynamicReportParticipantsPage.tsx +++ b/src/pages/DynamicReportParticipantsPage.tsx @@ -18,7 +18,6 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; -import useReportAttributes from '@hooks/useReportAttributes'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; From 054241c568a5a0fb3fe3f457ae1d11bbb2cb5509 Mon Sep 17 00:00:00 2001 From: M00rish Date: Mon, 27 Jul 2026 02:59:00 +0100 Subject: [PATCH 16/18] review updates --- config/eslint/eslint.seatbelt.tsv | 2 +- .../DynamicReportParticipantsInvitePage.tsx | 4 +- src/pages/DynamicReportParticipantsPage.tsx | 4 +- .../InviteReportParticipantsPageUtils.ts | 23 ----------- .../unit/InviteReportParticipantsPageTest.ts | 40 ------------------- tests/unit/RequestConflictUtilsTest.ts | 40 ------------------- 6 files changed, 7 insertions(+), 106 deletions(-) delete mode 100644 src/pages/InviteReportParticipantsPageUtils.ts delete mode 100644 tests/unit/InviteReportParticipantsPageTest.ts diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index a95584c65010..32d04ef3c261 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -1947,7 +1947,7 @@ "../../tests/unit/ReportUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 19 "../../tests/unit/ReportUtilsTest.ts" "no-restricted-imports" 1 "../../tests/unit/ReportWelcomeTextTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 -"../../tests/unit/RequestConflictUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 13 +"../../tests/unit/RequestConflictUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 11 "../../tests/unit/RequestTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../tests/unit/RequireA11yDisableJustificationRuleTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../tests/unit/RequireLiveRegionForStatusUpdatesRuleTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 diff --git a/src/pages/DynamicReportParticipantsInvitePage.tsx b/src/pages/DynamicReportParticipantsInvitePage.tsx index e74de1c5774b..547099f97033 100644 --- a/src/pages/DynamicReportParticipantsInvitePage.tsx +++ b/src/pages/DynamicReportParticipantsInvitePage.tsx @@ -136,7 +136,9 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants acc[login] = accountID; return acc; }, {} as InvitedEmailsToAccountIDs); - const [newAccountIDsAndLogins] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: newAccountIDsAndLoginsSelector(invitedEmailsToAccountIDs)}); + const [newAccountIDsAndLogins] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, { + selector: newAccountIDsAndLoginsSelector(invitedEmailsToAccountIDs), + }); const inviteUsers = () => { if (selectedOptions.length === 0) { diff --git a/src/pages/DynamicReportParticipantsPage.tsx b/src/pages/DynamicReportParticipantsPage.tsx index b89f9ab58433..df81258d3516 100755 --- a/src/pages/DynamicReportParticipantsPage.tsx +++ b/src/pages/DynamicReportParticipantsPage.tsx @@ -278,7 +278,9 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr variant={CONST.BUTTON_VARIANT.SUCCESS} shouldAlwaysShowDropdownMenu pressOnEnter - customText={translate('workspace.common.selected', {count: selectedMembers.length})} + customText={translate('workspace.common.selected', { + count: selectedMembers.length, + })} size={CONST.BUTTON_SIZE.MEDIUM} onPress={() => null} isSplitButton={false} diff --git a/src/pages/InviteReportParticipantsPageUtils.ts b/src/pages/InviteReportParticipantsPageUtils.ts deleted file mode 100644 index bbd0df6b3346..000000000000 --- a/src/pages/InviteReportParticipantsPageUtils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type {InvitedEmailsToAccountIDs} from '@src/types/onyx'; - -type InviteOption = { - accountID?: number; - login?: string; -}; - -function getInvitedEmailsToAccountIDs(selectedOptions: InviteOption[]): InvitedEmailsToAccountIDs { - const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = {}; - - for (const option of selectedOptions) { - const login = option.login ?? ''; - const accountID = option.accountID; - if (!login.trim() || accountID === undefined) { - continue; - } - invitedEmailsToAccountIDs[login] = accountID; - } - - return invitedEmailsToAccountIDs; -} - -export default getInvitedEmailsToAccountIDs; diff --git a/tests/unit/InviteReportParticipantsPageTest.ts b/tests/unit/InviteReportParticipantsPageTest.ts deleted file mode 100644 index e4371d495bcd..000000000000 --- a/tests/unit/InviteReportParticipantsPageTest.ts +++ /dev/null @@ -1,40 +0,0 @@ -import getInvitedEmailsToAccountIDs from '@pages/InviteReportParticipantsPageUtils'; - -describe('InviteReportParticipantsPage', () => { - describe('getInvitedEmailsToAccountIDs', () => { - it('preserves optimistic account IDs for new invitees', () => { - // Given a typed invitee that does not have a real account yet - const login = 'new-user@example.com'; - const selectedOptions = [ - { - login, - accountID: 0, - }, - ]; - - // When building the invite map - const invitedEmailsToAccountIDs = getInvitedEmailsToAccountIDs(selectedOptions); - - // Then the optimistic accountID is preserved for the API layer - expect(Object.entries(invitedEmailsToAccountIDs)).toStrictEqual([[login, 0]]); - }); - - it('skips malformed selected options', () => { - // Given selected options that cannot be invited - const selectedOptions = [ - { - accountID: 123, - }, - { - login: 'missing-account-id@example.com', - }, - ]; - - // When building the invite map - const invitedEmailsToAccountIDs = getInvitedEmailsToAccountIDs(selectedOptions); - - // Then no invalid invitee is sent to the API layer - expect(invitedEmailsToAccountIDs).toStrictEqual({}); - }); - }); -}); diff --git a/tests/unit/RequestConflictUtilsTest.ts b/tests/unit/RequestConflictUtilsTest.ts index 6daf31a5b8e6..d3c809fa826f 100644 --- a/tests/unit/RequestConflictUtilsTest.ts +++ b/tests/unit/RequestConflictUtilsTest.ts @@ -47,34 +47,6 @@ describe('RequestConflictUtils', () => { expect(result).toEqual({conflictAction: {type: 'replace', index: 2}}); }); - it.each([ - ['push when accountIDList differs', {emailList: '', accountIDList: '483'}, {emailList: '', accountIDList: ''}, {type: 'noAction'}], - ['replace when accountIDList matches', {emailList: '', accountIDList: '483'}, {emailList: '', accountIDList: '483'}, {type: 'replace', index: 0}], - ['replace when optional participant lists are empty', {}, {emailList: '', accountIDList: ''}, {type: 'replace', index: 0}], - ])('resolveOpenReportDuplicationConflictAction should %s', (_description, queuedData, nextData, expectedConflictAction) => { - const reportID = '8071514414018373'; - - // Given an existing OpenReport request - const persistedRequests = [ - { - command: WRITE_COMMANDS.OPEN_REPORT, - data: { - reportID, - ...queuedData, - }, - }, - ]; - - // When another OpenReport is queued for the same report - const result = resolveOpenReportDuplicationConflictAction(persistedRequests, { - reportID, - ...nextData, - }); - - // Then the request should only replace when participant identifiers match - expect(result).toEqual({conflictAction: expectedConflictAction}); - }); - it('resolveCommentDeletionConflicts should return push when no special comments are found', () => { const persistedRequests = [{command: 'OpenReport'}, {command: 'AddComment', data: {reportActionID: 2}}, {command: 'CloseAccount'}]; const reportActionID = '1'; @@ -232,18 +204,6 @@ describe('RequestConflictUtils', () => { const result = resolveOpenReportDuplicationConflictAction(persistedRequests, {reportID: '1', accountIDList: '10,20'} as never); expect(result).toEqual({conflictAction: {type: 'replace', index: 0}}); }); - - it('should return noAction when queued request has participants but new follow-up request has empty participant strings', () => { - const persistedRequests = [{command: WRITE_COMMANDS.OPEN_REPORT, data: {reportID: '1', accountIDList: '483'}}]; - const result = resolveOpenReportDuplicationConflictAction(persistedRequests, {reportID: '1', emailList: '', accountIDList: ''} as never); - expect(result).toEqual({conflictAction: {type: 'noAction'}}); - }); - - it('should push when accountIDList genuinely differs (distinct active lists)', () => { - const persistedRequests = [{command: WRITE_COMMANDS.OPEN_REPORT, data: {reportID: '1', accountIDList: '483'}}]; - const result = resolveOpenReportDuplicationConflictAction(persistedRequests, {reportID: '1', accountIDList: '484'} as never); - expect(result).toEqual({conflictAction: {type: 'push'}}); - }); }); describe('resolveDetachReceiptConflicts', () => { From 12358e3ec89a868fc94e6f82a330cd2cf937acbb Mon Sep 17 00:00:00 2001 From: M00rish Date: Mon, 27 Jul 2026 16:21:30 +0100 Subject: [PATCH 17/18] lint --- src/pages/DynamicReportParticipantsInvitePage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/DynamicReportParticipantsInvitePage.tsx b/src/pages/DynamicReportParticipantsInvitePage.tsx index 547099f97033..4b81c053390a 100644 --- a/src/pages/DynamicReportParticipantsInvitePage.tsx +++ b/src/pages/DynamicReportParticipantsInvitePage.tsx @@ -39,7 +39,6 @@ import {useEffect, useState} from 'react'; import type {WithReportOrNotFoundProps} from './inbox/report/withReportOrNotFound'; import withReportOrNotFound from './inbox/report/withReportOrNotFound'; -import getInvitedEmailsToAccountIDs from './InviteReportParticipantsPageUtils'; type DynamicReportParticipantsInvitePageProps = WithReportOrNotFoundProps & WithNavigationTransitionEndProps; From ff41852419ade2117ba3b9662cc5951723b3a99b Mon Sep 17 00:00:00 2001 From: M00rish Date: Mon, 27 Jul 2026 21:12:04 +0100 Subject: [PATCH 18/18] restore original resolveOpenReportDuplicationConflictAction --- src/libs/actions/RequestConflictUtils.ts | 19 +++++-------------- .../DynamicReportParticipantsInvitePage.tsx | 4 +++- src/pages/DynamicReportParticipantsPage.tsx | 9 ++++++--- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index c20680ec6e96..ab744975bf93 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -78,20 +78,7 @@ function resolveDuplicationConflictAction(persistedRequests: AnyRequest[], reque function resolveOpenReportDuplicationConflictAction(persistedRequests: Array>, parameters: OpenReportParams): ConflictActionData { for (let index = 0; index < persistedRequests.length; index++) { const request = persistedRequests.at(index); - - // Skip irrelevant requests immediately - if (request?.command !== WRITE_COMMANDS.OPEN_REPORT || request.data?.reportID !== parameters.reportID) { - continue; - } - - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const queuedHasParticipants = !!(request.data?.emailList || request.data?.accountIDList); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const newHasParticipants = !!(parameters.emailList || parameters.accountIDList); - - const isExactParticipantMatch = (request.data?.emailList ?? '') === (parameters.emailList ?? '') && (request.data?.accountIDList ?? '') === (parameters.accountIDList ?? ''); - - if (isExactParticipantMatch || (!queuedHasParticipants && newHasParticipants) || (queuedHasParticipants && !newHasParticipants)) { + if (request?.command === WRITE_COMMANDS.OPEN_REPORT && request.data?.reportID === parameters.reportID && request.data?.emailList === parameters.emailList) { // If the previous request had guided setup data, we can safely ignore the new request if (request.data.guidedSetupData) { return { @@ -106,6 +93,10 @@ function resolveOpenReportDuplicationConflictAction(persis // ReportFetchHandler when the screen mounts has no participants. Replacing would drop the // accountIDList, leaving the server with no way to resolve the optimistic reportID — Auth // returns NIL reportSummary and PHP throws "Report not found" (da7984df). + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const queuedHasParticipants = !!(request.data?.emailList || request.data?.accountIDList); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const newHasParticipants = !!(parameters.emailList || parameters.accountIDList); if (queuedHasParticipants && !newHasParticipants) { return { conflictAction: { diff --git a/src/pages/DynamicReportParticipantsInvitePage.tsx b/src/pages/DynamicReportParticipantsInvitePage.tsx index 4b81c053390a..2414ad5eb8de 100644 --- a/src/pages/DynamicReportParticipantsInvitePage.tsx +++ b/src/pages/DynamicReportParticipantsInvitePage.tsx @@ -13,6 +13,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePersonalDetailSearchSelector from '@hooks/usePersonalDetailSearchSelector'; import usePolicy from '@hooks/usePolicy'; +import useReportAttributes from '@hooks/useReportAttributes'; import useThemeStyles from '@hooks/useThemeStyles'; import {inviteToGroupChat, searchUserInServer} from '@libs/actions/Report'; @@ -58,6 +59,7 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants const policy = usePolicy(report.policyID); const isReportSubmitterOrAdmin = isCurrentUserSubmitter(report) || isPolicyAdmin(policy) || (isGroupChat(report) && isGroupChatAdmin(report, currentUserAccountID)); const shouldShowInvitePage = isReportSubmitterOrAdmin && (isGroupChat(report) || (isMoneyRequestReport(report) && isOpenExpenseReport(report))); + const reportAttributes = useReportAttributes(); // Any existing participants and Expensify emails should not be eligible for invitation const excludedUsers: Record = { @@ -123,7 +125,7 @@ function DynamicReportParticipantsInvitePage({report}: DynamicReportParticipants toggleSelection(option); }; - const reportName = StringUtils.lineBreaksToSpaces(getReportName(report)); + const reportName = StringUtils.lineBreaksToSpaces(getReportName(report, reportAttributes)); const goBack = () => { Navigation.goBack(backPath); diff --git a/src/pages/DynamicReportParticipantsPage.tsx b/src/pages/DynamicReportParticipantsPage.tsx index df81258d3516..08e432e51fae 100755 --- a/src/pages/DynamicReportParticipantsPage.tsx +++ b/src/pages/DynamicReportParticipantsPage.tsx @@ -18,6 +18,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; +import useReportAttributes from '@hooks/useReportAttributes'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; @@ -89,11 +90,13 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr const isCurrentUserAdmin = isGroupChatAdmin(report, currentUserAccountID); const isGroupChat = isGroupChatUtils(report); const policy = usePolicy(report.policyID); - const isReportSubmitterOrAdmin = isCurrentUserSubmitter(report) || isPolicyAdmin(policy) || (isGroupChat && isCurrentUserAdmin); - const shouldShowInviteButton = isReportSubmitterOrAdmin && (isGroupChat || (isMoneyRequestReport(report) && isOpenExpenseReport(report))); const isCurrentUserGroupChatAdmin = isGroupChat && isCurrentUserAdmin; + const isReportSubmitterOrAdmin = isCurrentUserSubmitter(report) || isPolicyAdmin(policy) || isCurrentUserGroupChatAdmin; + const shouldShowInviteButton = isReportSubmitterOrAdmin && (isGroupChat || (isMoneyRequestReport(report) && isOpenExpenseReport(report))); + const {isOffline} = useNetwork(); const canSelectMultiple = isGroupChat && isCurrentUserAdmin && (isSmallScreenWidth ? isMobileSelectionModeEnabled : true); + const reportAttributes = useReportAttributes(); const {personalDetailsParticipants, participantsForDisplay} = getReportPersonalDetailsParticipants(report, personalDetails, reportMetadata); const participantsForDisplayMap = participantsForDisplay.reduce>>((acc, participant) => { @@ -268,7 +271,7 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr navigateBackToReportDetails(); } }} - subtitle={StringUtils.lineBreaksToSpaces(getReportName(report))} + subtitle={StringUtils.lineBreaksToSpaces(getReportName(report, reportAttributes))} /> {shouldShowInviteButton && (