From 81146ab137f7d76237352001083c866844439318 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Thu, 18 Sep 2025 17:30:58 +0200 Subject: [PATCH 1/9] Core logic for ReportTitleUtils --- src/libs/ReportTitleUtils.ts | 110 +++++++++++++++++++++ tests/unit/ReportTitleUtilsTest.ts | 154 +++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 src/libs/ReportTitleUtils.ts create mode 100644 tests/unit/ReportTitleUtilsTest.ts diff --git a/src/libs/ReportTitleUtils.ts b/src/libs/ReportTitleUtils.ts new file mode 100644 index 000000000000..90969b7d962b --- /dev/null +++ b/src/libs/ReportTitleUtils.ts @@ -0,0 +1,110 @@ +import type {OnyxUpdate} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report, ReportNameValuePairs} from '@src/types/onyx'; +import {getTitleReportField, isChatReport} from './ReportUtils'; + +let allReportNameValuePairs: Record = {}; + +/** + * We use Onyx.connectWithoutView because we do not use this in React components and this logic is not tied directly to the UI. + * We need up to date report name value pairs of reports to correctly determine if further updates to report's titles should be made. + * It wouldn't be possible without connection directly to Onyx. + */ +Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, + waitForCollectionCallback: true, + callback: (val) => { + allReportNameValuePairs = (val as Record) ?? {}; + }, +}); + +/** + * Get the title field from report name value pairs + */ +function getTitleFieldFromRNVP(reportID: string) { + const reportNameValuePairs = allReportNameValuePairs[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`]; + // eslint-disable-next-line @typescript-eslint/naming-convention + return reportNameValuePairs?.expensify_text_title; +} + +/** + * Update title field in report's rNVP to match the policy's title field configuration + * This is the JavaScript equivalent of the backend updateTitleFieldToMatchPolicy function + */ +function updateTitleFieldToMatchPolicy(reportID: string, policy?: Policy): OnyxUpdate[] { + if (!reportID || !policy) { + return []; + } + + // Get the policy's title field configuration + const policyTitleField = getTitleReportField(policy.fieldList ?? {}); + + // Early return if policy doesn't have a title field + if (!policyTitleField) { + return []; + } + + // Create the update to set/update the title field in rNVP + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, + value: { + // eslint-disable-next-line @typescript-eslint/naming-convention + expensify_text_title: policyTitleField, + }, + }, + ]; + + return optimisticData; +} + +/** + * Remove title field from report's rNVP when report is manually renamed + * This indicates the report should preserve its custom name + */ +function removeTitleFieldFromReport(reportID: string): OnyxUpdate[] { + if (!reportID) { + return []; + } + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, + value: { + // eslint-disable-next-line @typescript-eslint/naming-convention + expensify_text_title: null, + }, + }, + ]; + + return optimisticData; +} + +/** + * Check if a report should have its title field updated based on conditions + */ +function shouldUpdateTitleField(report: Report): boolean { + // todo: this should be more sophisticated function. check for iou etc + if (!report) { + return false; + } + // Skip chat reports + if (isChatReport(report)) { + return false; + } + + // Skip if report has statement card ID (backend check: !getValue(db, reportID, NVP_STATEMENT_CARD_ID).empty()) + // This would need to be implemented based on how statement card IDs are stored in the frontend + + const policyTitleField = getTitleFieldFromRNVP(report.reportID); + if (!policyTitleField) { + return false; + } + + return true; +} + +export {getTitleFieldFromRNVP, removeTitleFieldFromReport, shouldUpdateTitleField, updateTitleFieldToMatchPolicy}; diff --git a/tests/unit/ReportTitleUtilsTest.ts b/tests/unit/ReportTitleUtilsTest.ts new file mode 100644 index 000000000000..1c31a20a8214 --- /dev/null +++ b/tests/unit/ReportTitleUtilsTest.ts @@ -0,0 +1,154 @@ +import Onyx from 'react-native-onyx'; +import {getTitleFieldFromRNVP, removeTitleFieldFromReport, shouldUpdateTitleField, updateTitleFieldToMatchPolicy} from '@libs/ReportTitleUtils'; +// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, PolicyReportField, Report, ReportNameValuePairs} from '@src/types/onyx'; +import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +// Mock dependencies +jest.mock('@libs/ReportUtils', () => ({ + getTitleReportField: jest.fn(), + isChatReport: jest.fn(), +})); + +const mockedReportUtils = ReportUtils as jest.Mocked; + +describe('ReportTitleUtils', () => { + beforeAll(async () => { + const mergedCollection: CollectionDataSet = {}; + mergedCollection[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}12345`] = { + // eslint-disable-next-line @typescript-eslint/naming-convention + expensify_text_title: { + defaultValue: 'Report {report:total}', + }, + } as unknown as ReportNameValuePairs; + await Onyx.merge(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, mergedCollection); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getTitleFieldFromRNVP', () => { + const mockReportID = '12345'; + const mockTitleField = { + defaultValue: 'Report {report:total}', + }; + + it('should return title field when RNVP exists with title field', () => { + const result = getTitleFieldFromRNVP(mockReportID); + + expect(result).toEqual(mockTitleField); + }); + + it('should return undefined when RNVP is undefined', () => { + const result = getTitleFieldFromRNVP('55555'); + + expect(result).toBeUndefined(); + }); + }); + + describe('updateTitleFieldToMatchPolicy', () => { + const mockReportID = '12345'; + const mockTitleField: PolicyReportField = { + defaultValue: 'Test report Title', + } as unknown as PolicyReportField; + + it('should return optimistic update when valid inputs provided', () => { + const mockPolicy: Policy = { + id: 'policy123', + fieldList: { + // eslint-disable-next-line @typescript-eslint/naming-convention + text_title: mockTitleField, + }, + } as unknown as Policy; + + mockedReportUtils.getTitleReportField.mockReturnValue(mockTitleField); + + const result = updateTitleFieldToMatchPolicy(mockReportID, mockPolicy); + + expect(mockedReportUtils.getTitleReportField).toHaveBeenCalledWith(mockPolicy.fieldList); + expect(result).toHaveLength(1); + expect(result.at(0)).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${mockReportID}`, + value: { + // eslint-disable-next-line @typescript-eslint/naming-convention + expensify_text_title: mockTitleField, + }, + }); + }); + + it('should return empty array when policy is undefined', () => { + const result = updateTitleFieldToMatchPolicy(mockReportID, undefined); + + expect(result).toEqual([]); + expect(mockedReportUtils.getTitleReportField).not.toHaveBeenCalled(); + }); + }); + + describe('removeTitleFieldFromReport', () => { + const mockReportID = '12345'; + + it('should return optimistic update with null value for valid reportID', () => { + const result = removeTitleFieldFromReport(mockReportID); + + expect(result).toHaveLength(1); + expect(result.at(0)).toEqual({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${mockReportID}`, + value: { + // eslint-disable-next-line @typescript-eslint/naming-convention + expensify_text_title: null, + }, + }); + }); + }); + + describe('shouldUpdateTitleField', () => { + const mockReportID = '12345'; + + it('should return false when report is chat report', () => { + const mockReport: Report = { + reportID: mockReportID, + type: CONST.REPORT.TYPE.CHAT, + } as Report; + + mockedReportUtils.isChatReport.mockReturnValue(true); + + const result = shouldUpdateTitleField(mockReport); + + expect(mockedReportUtils.isChatReport).toHaveBeenCalledWith(mockReport); + expect(result).toBe(false); + }); + + it('should return false when report has no title field in RNVP', () => { + const mockReport: Report = { + reportID: '5555', + type: CONST.REPORT.TYPE.EXPENSE, + } as Report; + + mockedReportUtils.isChatReport.mockReturnValue(false); + + const result = shouldUpdateTitleField(mockReport); + + expect(result).toBe(false); + }); + + it('should return true when non-chat report has title field', () => { + const mockReport: Report = { + reportID: mockReportID, + type: CONST.REPORT.TYPE.EXPENSE, + } as Report; + + mockedReportUtils.isChatReport.mockReturnValue(false); + + const result = shouldUpdateTitleField(mockReport); + + expect(result).toBe(true); + }); + }); +}); From efd63d8c3d401dd554a3430d2f69dc11db2b918a Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 19 Sep 2025 10:53:34 +0200 Subject: [PATCH 2/9] fix imports --- tests/unit/ReportTitleUtilsTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/ReportTitleUtilsTest.ts b/tests/unit/ReportTitleUtilsTest.ts index 1c31a20a8214..d28aa93f9d55 100644 --- a/tests/unit/ReportTitleUtilsTest.ts +++ b/tests/unit/ReportTitleUtilsTest.ts @@ -6,7 +6,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyReportField, Report, ReportNameValuePairs} from '@src/types/onyx'; import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; -import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; // Mock dependencies jest.mock('@libs/ReportUtils', () => ({ From ed803fd7f7aa2967d3f68d10f35485b43212d322 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 19 Sep 2025 11:26:56 +0200 Subject: [PATCH 3/9] Add support for updateTitleFieldToMatchPolicy to report creation --- src/hooks/useSearchTypeMenuSections.ts | 17 +++++++++++++- src/libs/SearchUIUtils.ts | 4 +++- src/libs/actions/Report.ts | 23 ++++++++++++++++--- src/pages/NewReportWorkspaceSelectionPage.tsx | 6 +++-- src/pages/Search/EmptySearchView.tsx | 4 +++- .../AttachmentPickerWithMenuItems.tsx | 6 +++-- .../FloatingActionButtonAndPopover.tsx | 4 +++- 7 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index 6197831ab8bc..658c7860ddd0 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -44,6 +44,8 @@ const useSearchTypeMenuSections = () => { const [currentUserLoginAndAccountID] = useOnyx(ONYXKEYS.SESSION, {selector: currentUserLoginAndAccountIDSelector, canBeMissing: false}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); + const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION, {canBeMissing: true}); const typeMenuSections = useMemo( () => @@ -56,8 +58,21 @@ const useSearchTypeMenuSections = () => { activePolicyID, savedSearches, isOffline, + betas, + betaConfiguration, ), - [currentUserLoginAndAccountID?.email, currentUserLoginAndAccountID?.accountID, cardFeedsByPolicy, defaultCardFeed, allPolicies, activePolicyID, savedSearches, isOffline], + [ + currentUserLoginAndAccountID?.email, + currentUserLoginAndAccountID?.accountID, + cardFeedsByPolicy, + defaultCardFeed, + allPolicies, + activePolicyID, + savedSearches, + isOffline, + betas, + betaConfiguration, + ], ); return {typeMenuSections}; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 7ccf463c0842..6cc1840dce4d 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1831,6 +1831,8 @@ function createTypeMenuSections( activePolicyID: string | undefined, savedSearches: OnyxEntry, isOffline: boolean, + betas: OnyxTypes.Beta[] = [], + betaConfiguration?: OnyxTypes.BetaConfiguration, ): SearchTypeMenuSection[] { const typeMenuSections: SearchTypeMenuSection[] = []; @@ -1873,7 +1875,7 @@ function createTypeMenuSections( } if (workspaceIDForReportCreation && !shouldRestrictUserBillableActions(workspaceIDForReportCreation) && personalDetails) { - const createdReportID = createNewReport(personalDetails, workspaceIDForReportCreation); + const createdReportID = createNewReport(personalDetails, workspaceIDForReportCreation, false, betas, betaConfiguration); Navigation.setNavigationActionToMicrotaskQueue(() => { Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()})); }); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 3b2e2d1cbac6..73a07683a776 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -90,6 +90,7 @@ import {rand64} from '@libs/NumberUtils'; import {shouldOnboardingRedirectToOldDot} from '@libs/OnboardingUtils'; import Parser from '@libs/Parser'; import {getParsedMessageWithShortMentions} from '@libs/ParsingUtils'; +import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; import {getDefaultApprover, getMemberAccountIDsForWorkspace, getPolicy, isPaidGroupPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils, isPolicyMember} from '@libs/PolicyUtils'; @@ -97,6 +98,7 @@ import processReportIDDeeplink from '@libs/processReportIDDeeplink'; import Pusher from '@libs/Pusher'; import type {UserIsLeavingRoomEvent, UserIsTypingEvent} from '@libs/Pusher/types'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import {updateTitleFieldToMatchPolicy} from '@libs/ReportTitleUtils'; import type {OptimisticAddCommentReportAction, OptimisticChatReport, SelfDMParameters} from '@libs/ReportUtils'; import { buildOptimisticAddCommentReportAction, @@ -181,6 +183,8 @@ import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/NewRoomForm'; import type { Account, + Beta, + BetaConfiguration, DismissedProductTraining, IntroSelected, InvitedEmailsToAccountIDs, @@ -2698,7 +2702,15 @@ function navigateToConciergeChat(shouldDismissModal = false, checkIfCurrentPageA } } -function buildNewReportOptimisticData(policy: OnyxEntry, reportID: string, reportActionID: string, creatorPersonalDetails: PersonalDetails, reportPreviewReportActionID: string) { +function buildNewReportOptimisticData( + policy: OnyxEntry, + reportID: string, + reportActionID: string, + creatorPersonalDetails: PersonalDetails, + reportPreviewReportActionID: string, + betas: Beta[] = [], + betaConfiguration?: BetaConfiguration, +) { const {accountID, login} = creatorPersonalDetails; const timeOfCreation = DateUtils.getDBTime(); const parentReport = getPolicyExpenseChat(accountID, policy?.id); @@ -2752,7 +2764,7 @@ function buildNewReportOptimisticData(policy: OnyxEntry, reportID: strin const optimisticNextStep = buildNextStep(optimisticReportData, CONST.REPORT.STATUS_NUM.OPEN); const outstandingChildRequest = getOutstandingChildRequest(optimisticReportData); - const optimisticData: OnyxUpdate[] = [ + let optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -2786,6 +2798,9 @@ function buildNewReportOptimisticData(policy: OnyxEntry, reportID: strin value: optimisticNextStep, }, ]; + if (Permissions.isBetaEnabled(CONST.BETAS.AUTH_AUTO_REPORT_TITLE, betas, betaConfiguration)) { + optimisticData = optimisticData.concat(updateTitleFieldToMatchPolicy(reportID, policy)); + } const failureData: OnyxUpdate[] = [ { @@ -2851,7 +2866,7 @@ function buildNewReportOptimisticData(policy: OnyxEntry, reportID: strin }; } -function createNewReport(creatorPersonalDetails: PersonalDetails, policyID?: string, shouldNotifyNewAction = false) { +function createNewReport(creatorPersonalDetails: PersonalDetails, policyID?: string, shouldNotifyNewAction = false, betas: Beta[] = [], betaConfiguration?: BetaConfiguration) { // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation const policy = getPolicy(policyID); @@ -2865,6 +2880,8 @@ function createNewReport(creatorPersonalDetails: PersonalDetails, policyID?: str reportActionID, creatorPersonalDetails, reportPreviewReportActionID, + betas, + betaConfiguration, ); API.write( diff --git a/src/pages/NewReportWorkspaceSelectionPage.tsx b/src/pages/NewReportWorkspaceSelectionPage.tsx index 2a116f062579..3e696907226d 100644 --- a/src/pages/NewReportWorkspaceSelectionPage.tsx +++ b/src/pages/NewReportWorkspaceSelectionPage.tsx @@ -42,6 +42,8 @@ function NewReportWorkspaceSelectionPage() { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); const shouldShowLoadingIndicator = isLoadingApp && !isOffline; + const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION, {canBeMissing: true}); const navigateToNewReport = useCallback( (optimisticReportID: string) => { @@ -71,10 +73,10 @@ function NewReportWorkspaceSelectionPage() { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyID)); return; } - const optimisticReportID = createNewReport(currentUserPersonalDetails, policyID); + const optimisticReportID = createNewReport(currentUserPersonalDetails, policyID, false, betas, betaConfiguration); navigateToNewReport(optimisticReportID); }, - [currentUserPersonalDetails, navigateToNewReport], + [currentUserPersonalDetails, navigateToNewReport, betas, betaConfiguration], ); const usersWorkspaces = useMemo(() => { diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 67e2b3f0d703..97531b6ef5ad 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -156,6 +156,8 @@ function EmptySearchViewContent({ selector: (transactions) => Object.values(transactions ?? {}).filter((transaction) => transaction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length > 0, }); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); + const [betas] = useOnyx(ONYXKEYS.BETAS); + const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION); const shouldRedirectToExpensifyClassic = useMemo(() => { return areAllGroupPoliciesExpenseChatDisabled(allPolicies ?? {}); @@ -302,7 +304,7 @@ function EmptySearchViewContent({ } if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { - const createdReportID = createNewReport(currentUserPersonalDetails, workspaceIDForReportCreation); + const createdReportID = createNewReport(currentUserPersonalDetails, workspaceIDForReportCreation, false, betas, betaConfiguration); Navigation.setNavigationActionToMicrotaskQueue(() => { Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()})); }); diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 31c558e60fb8..d776d01e7028 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -134,6 +134,8 @@ function AttachmentPickerWithMenuItems({ const {isBetaEnabled} = usePermissions(); const {setIsLoaderVisible} = useFullScreenLoader(); const isReportArchived = useReportIsArchived(report?.reportID); + const [betas] = useOnyx(ONYXKEYS.BETAS); + const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION); const isManualDistanceTrackingEnabled = isBetaEnabled(CONST.BETAS.MANUAL_DISTANCE); @@ -255,10 +257,10 @@ function AttachmentPickerWithMenuItems({ { icon: Expensicons.Document, text: translate('report.newReport.createReport'), - onSelected: () => selectOption(() => createNewReport(currentUserPersonalDetails, report?.policyID, true), true), + onSelected: () => selectOption(() => createNewReport(currentUserPersonalDetails, report?.policyID, true, betas, betaConfiguration), true), }, ]; - }, [currentUserPersonalDetails, report, selectOption, translate]); + }, [currentUserPersonalDetails, report, selectOption, translate, betaConfiguration, betas]); /** * Determines if we can show the task option diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index a6934c2b00ab..09a8035e929a 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -136,6 +136,8 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.primaryLogin, canBeMissing: true}); const primaryContactMethod = primaryLogin ?? session?.email ?? ''; const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); + const [betas] = useOnyx(ONYXKEYS.BETAS); + const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION); const canSendInvoice = useMemo(() => canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email), [allPolicies, session?.email]); const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); @@ -488,7 +490,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT } if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { - const createdReportID = createNewReport(currentUserPersonalDetails, workspaceIDForReportCreation); + const createdReportID = createNewReport(currentUserPersonalDetails, workspaceIDForReportCreation, false, betas, betaConfiguration); Navigation.setNavigationActionToMicrotaskQueue(() => { Navigation.navigate( isSearchTopmostFullScreenRoute() From 1d6dcb4e75256d3fd1a0aba7e87d0f8308877e52 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 19 Sep 2025 11:37:39 +0200 Subject: [PATCH 4/9] useOnyx -> canBeMissing --- src/pages/Search/EmptySearchView.tsx | 6 ++++-- .../ReportActionCompose/AttachmentPickerWithMenuItems.tsx | 4 ++-- src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 97531b6ef5ad..895ad0341ea3 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -156,8 +156,8 @@ function EmptySearchViewContent({ selector: (transactions) => Object.values(transactions ?? {}).filter((transaction) => transaction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length > 0, }); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); - const [betas] = useOnyx(ONYXKEYS.BETAS); - const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION); + const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION, {canBeMissing: true}); const shouldRedirectToExpensifyClassic = useMemo(() => { return areAllGroupPoliciesExpenseChatDisabled(allPolicies ?? {}); @@ -430,6 +430,8 @@ function EmptySearchViewContent({ hasTransactions, tryNewDot?.hasBeenAddedToNudgeMigration, isUserPaidPolicyMember, + betas, + betaConfiguration, ]); return ( diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index d776d01e7028..9cc579c88619 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -134,8 +134,8 @@ function AttachmentPickerWithMenuItems({ const {isBetaEnabled} = usePermissions(); const {setIsLoaderVisible} = useFullScreenLoader(); const isReportArchived = useReportIsArchived(report?.reportID); - const [betas] = useOnyx(ONYXKEYS.BETAS); - const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION); + const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION, {canBeMissing: true}); const isManualDistanceTrackingEnabled = isBetaEnabled(CONST.BETAS.MANUAL_DISTANCE); diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 09a8035e929a..fe809c0c2b82 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -136,8 +136,8 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.primaryLogin, canBeMissing: true}); const primaryContactMethod = primaryLogin ?? session?.email ?? ''; const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); - const [betas] = useOnyx(ONYXKEYS.BETAS); - const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION); + const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION, {canBeMissing: true}); const canSendInvoice = useMemo(() => canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email), [allPolicies, session?.email]); const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); From 2a77e9ae8114913ffcacc30c40ed2f9e20cefc7b Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 19 Sep 2025 12:19:56 +0200 Subject: [PATCH 5/9] Additional comment --- src/libs/ReportTitleUtils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/ReportTitleUtils.ts b/src/libs/ReportTitleUtils.ts index 90969b7d962b..0db597057503 100644 --- a/src/libs/ReportTitleUtils.ts +++ b/src/libs/ReportTitleUtils.ts @@ -1,3 +1,7 @@ +/** + * This file should only be used in context of optimistic report name updates. + * We're using direct Onyx connection and this can lead to stale component's state if used in wrong context. + */ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; From 4f37d8ce83a500552d87f7e8051739d93f58697e Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 19 Sep 2025 12:53:02 +0200 Subject: [PATCH 6/9] Move betas to ReportTitleUtils --- src/hooks/useSearchTypeMenuSections.ts | 17 +----- src/libs/ReportTitleUtils.ts | 24 +++++++- src/libs/SearchUIUtils.ts | 4 +- src/libs/actions/Report.ts | 61 +++++++------------ src/pages/NewReportWorkspaceSelectionPage.tsx | 6 +- src/pages/Search/EmptySearchView.tsx | 6 +- .../AttachmentPickerWithMenuItems.tsx | 6 +- .../FloatingActionButtonAndPopover.tsx | 4 +- tests/unit/ReportTitleUtilsTest.ts | 3 + 9 files changed, 57 insertions(+), 74 deletions(-) diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index 658c7860ddd0..6197831ab8bc 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -44,8 +44,6 @@ const useSearchTypeMenuSections = () => { const [currentUserLoginAndAccountID] = useOnyx(ONYXKEYS.SESSION, {selector: currentUserLoginAndAccountIDSelector, canBeMissing: false}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); - const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); - const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION, {canBeMissing: true}); const typeMenuSections = useMemo( () => @@ -58,21 +56,8 @@ const useSearchTypeMenuSections = () => { activePolicyID, savedSearches, isOffline, - betas, - betaConfiguration, ), - [ - currentUserLoginAndAccountID?.email, - currentUserLoginAndAccountID?.accountID, - cardFeedsByPolicy, - defaultCardFeed, - allPolicies, - activePolicyID, - savedSearches, - isOffline, - betas, - betaConfiguration, - ], + [currentUserLoginAndAccountID?.email, currentUserLoginAndAccountID?.accountID, cardFeedsByPolicy, defaultCardFeed, allPolicies, activePolicyID, savedSearches, isOffline], ); return {typeMenuSections}; diff --git a/src/libs/ReportTitleUtils.ts b/src/libs/ReportTitleUtils.ts index 0db597057503..722dba0dc14e 100644 --- a/src/libs/ReportTitleUtils.ts +++ b/src/libs/ReportTitleUtils.ts @@ -4,11 +4,15 @@ */ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, Report, ReportNameValuePairs} from '@src/types/onyx'; +import type {Beta, BetaConfiguration, Policy, Report, ReportNameValuePairs} from '@src/types/onyx'; +import Permissions from './Permissions'; import {getTitleReportField, isChatReport} from './ReportUtils'; let allReportNameValuePairs: Record = {}; +let betas: Beta[] = []; +let betaConfiguration: BetaConfiguration = {}; /** * We use Onyx.connectWithoutView because we do not use this in React components and this logic is not tied directly to the UI. @@ -22,6 +26,18 @@ Onyx.connectWithoutView({ allReportNameValuePairs = (val as Record) ?? {}; }, }); +Onyx.connectWithoutView({ + key: ONYXKEYS.BETAS, + callback: (val) => { + betas = val ?? []; + }, +}); +Onyx.connectWithoutView({ + key: ONYXKEYS.BETA_CONFIGURATION, + callback: (val) => { + betaConfiguration = val ?? {}; + }, +}); /** * Get the title field from report name value pairs @@ -37,6 +53,9 @@ function getTitleFieldFromRNVP(reportID: string) { * This is the JavaScript equivalent of the backend updateTitleFieldToMatchPolicy function */ function updateTitleFieldToMatchPolicy(reportID: string, policy?: Policy): OnyxUpdate[] { + if (!Permissions.isBetaEnabled(CONST.BETAS.AUTH_AUTO_REPORT_TITLE, betas, betaConfiguration)) { + return []; + } if (!reportID || !policy) { return []; } @@ -69,6 +88,9 @@ function updateTitleFieldToMatchPolicy(reportID: string, policy?: Policy): OnyxU * This indicates the report should preserve its custom name */ function removeTitleFieldFromReport(reportID: string): OnyxUpdate[] { + if (!Permissions.isBetaEnabled(CONST.BETAS.AUTH_AUTO_REPORT_TITLE, betas, betaConfiguration)) { + return []; + } if (!reportID) { return []; } diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 6cc1840dce4d..7ccf463c0842 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1831,8 +1831,6 @@ function createTypeMenuSections( activePolicyID: string | undefined, savedSearches: OnyxEntry, isOffline: boolean, - betas: OnyxTypes.Beta[] = [], - betaConfiguration?: OnyxTypes.BetaConfiguration, ): SearchTypeMenuSection[] { const typeMenuSections: SearchTypeMenuSection[] = []; @@ -1875,7 +1873,7 @@ function createTypeMenuSections( } if (workspaceIDForReportCreation && !shouldRestrictUserBillableActions(workspaceIDForReportCreation) && personalDetails) { - const createdReportID = createNewReport(personalDetails, workspaceIDForReportCreation, false, betas, betaConfiguration); + const createdReportID = createNewReport(personalDetails, workspaceIDForReportCreation); Navigation.setNavigationActionToMicrotaskQueue(() => { Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()})); }); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 73a07683a776..d4902a423d72 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -90,7 +90,6 @@ import {rand64} from '@libs/NumberUtils'; import {shouldOnboardingRedirectToOldDot} from '@libs/OnboardingUtils'; import Parser from '@libs/Parser'; import {getParsedMessageWithShortMentions} from '@libs/ParsingUtils'; -import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; import {getDefaultApprover, getMemberAccountIDsForWorkspace, getPolicy, isPaidGroupPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils, isPolicyMember} from '@libs/PolicyUtils'; @@ -183,8 +182,6 @@ import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/NewRoomForm'; import type { Account, - Beta, - BetaConfiguration, DismissedProductTraining, IntroSelected, InvitedEmailsToAccountIDs, @@ -229,8 +226,8 @@ import { } from './RequestConflictUtils'; import {canAnonymousUserAccessRoute, isAnonymousUser, signOutAndRedirectToSignIn, waitForUserSignIn} from './Session'; import {isOnboardingFlowCompleted, onServerDataReady, setOnboardingErrorMessage} from './Welcome'; -import {getOnboardingMessages, startOnboardingFlow} from './Welcome/OnboardingFlow'; import type {OnboardingCompanySize, OnboardingMessage} from './Welcome/OnboardingFlow'; +import {getOnboardingMessages, startOnboardingFlow} from './Welcome/OnboardingFlow'; type SubscriberCallback = (isFromCurrentUser: boolean, reportAction: ReportAction | undefined) => void; @@ -2702,15 +2699,7 @@ function navigateToConciergeChat(shouldDismissModal = false, checkIfCurrentPageA } } -function buildNewReportOptimisticData( - policy: OnyxEntry, - reportID: string, - reportActionID: string, - creatorPersonalDetails: PersonalDetails, - reportPreviewReportActionID: string, - betas: Beta[] = [], - betaConfiguration?: BetaConfiguration, -) { +function buildNewReportOptimisticData(policy: OnyxEntry, reportID: string, reportActionID: string, creatorPersonalDetails: PersonalDetails, reportPreviewReportActionID: string) { const {accountID, login} = creatorPersonalDetails; const timeOfCreation = DateUtils.getDBTime(); const parentReport = getPolicyExpenseChat(accountID, policy?.id); @@ -2764,7 +2753,7 @@ function buildNewReportOptimisticData( const optimisticNextStep = buildNextStep(optimisticReportData, CONST.REPORT.STATUS_NUM.OPEN); const outstandingChildRequest = getOutstandingChildRequest(optimisticReportData); - let optimisticData: OnyxUpdate[] = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -2798,9 +2787,7 @@ function buildNewReportOptimisticData( value: optimisticNextStep, }, ]; - if (Permissions.isBetaEnabled(CONST.BETAS.AUTH_AUTO_REPORT_TITLE, betas, betaConfiguration)) { - optimisticData = optimisticData.concat(updateTitleFieldToMatchPolicy(reportID, policy)); - } + optimisticData.push(...updateTitleFieldToMatchPolicy(reportID, policy)); const failureData: OnyxUpdate[] = [ { @@ -2866,7 +2853,7 @@ function buildNewReportOptimisticData( }; } -function createNewReport(creatorPersonalDetails: PersonalDetails, policyID?: string, shouldNotifyNewAction = false, betas: Beta[] = [], betaConfiguration?: BetaConfiguration) { +function createNewReport(creatorPersonalDetails: PersonalDetails, policyID?: string, shouldNotifyNewAction = false) { // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation const policy = getPolicy(policyID); @@ -2880,8 +2867,6 @@ function createNewReport(creatorPersonalDetails: PersonalDetails, policyID?: str reportActionID, creatorPersonalDetails, reportPreviewReportActionID, - betas, - betaConfiguration, ); API.write( @@ -6088,7 +6073,7 @@ function resolveConciergeCategoryOptions( } as Partial); } -export type {Video, GuidedSetupData, TaskForParameters, IntroSelected}; +export type {GuidedSetupData, IntroSelected, TaskForParameters, Video}; export { addAttachment, @@ -6096,23 +6081,29 @@ export { addPolicyReport, broadcastUserIsLeavingRoom, broadcastUserIsTyping, + buildInviteToRoomOnyxData, buildOptimisticChangePolicyData, + changeReportPolicy, + changeReportPolicyAndInviteSubmitter, clearAddRoomMemberError, clearAvatarErrors, + clearCreateChatError, clearDeleteTransactionNavigateBackUrl, clearGroupChat, clearIOUError, clearNewRoomFormError, - setNewRoomFormLoading, clearPolicyRoomNameErrors, clearPrivateNotesError, clearReportFieldKeyErrors, completeOnboarding, createNewReport, + createTransactionThreadReport, + deleteAppReport, deleteReport, deleteReportActionDraft, deleteReportComment, deleteReportField, + dismissChangePolicyModal, dismissTrackExpenseActionableWhisper, doneCheckingPublicRoom, downloadReportPDF, @@ -6128,43 +6119,48 @@ export { getMostRecentReportID, getNewerActions, getOlderActions, + getOptimisticChatReport, getReportPrivateNote, handleReportChanged, handleUserDeletedLinksInHtml, hasErrorInPrivateNotes, inviteToGroupChat, - buildInviteToRoomOnyxData, inviteToRoom, joinRoom, leaveGroupChat, leaveRoom, + markAllMessagesAsRead, markAsManuallyExported, markCommentAsUnread, + moveIOUReportToPolicy, + moveIOUReportToPolicyAndInviteSubmitter, navigateToAndOpenChildReport, navigateToAndOpenReport, navigateToAndOpenReportWithAccountIDs, navigateToConciergeChat, navigateToConciergeChatAndDeleteReport, - clearCreateChatError, notifyNewAction, openReport, openReportFromDeepLink, openRoomMembersPage, + openUnreportedExpense, readNewestAction, - markAllMessagesAsRead, + removeFailedReport, removeFromGroupChat, removeFromRoom, - resolveActionableMentionWhisper, resolveActionableMentionConfirmWhisper, + resolveActionableMentionWhisper, resolveActionableReportMentionWhisper, resolveConciergeCategoryOptions, savePrivateNotesDraft, saveReportActionDraft, + saveReportDraft, saveReportDraftComment, searchInServer, setDeleteTransactionNavigateBackUrl, setGroupDraft, setIsComposerFullSize, + setNewRoomFormLoading, shouldShowReportActionNotification, showReportActionNotification, startNewChat, @@ -6176,10 +6172,10 @@ export { toggleSubscribeToChildReport, unsubscribeFromLeavingRoomReportChannel, unsubscribeFromReportChannel, + updateChatName, updateDescription, updateGroupChatAvatar, updateGroupChatMemberRoles, - updateChatName, updateLastVisitTime, updateLoadingInitialReportAction, updateNotificationPreference, @@ -6189,15 +6185,4 @@ export { updateReportName, updateRoomVisibility, updateWriteCapability, - deleteAppReport, - getOptimisticChatReport, - saveReportDraft, - moveIOUReportToPolicy, - moveIOUReportToPolicyAndInviteSubmitter, - dismissChangePolicyModal, - changeReportPolicy, - changeReportPolicyAndInviteSubmitter, - removeFailedReport, - createTransactionThreadReport, - openUnreportedExpense, }; diff --git a/src/pages/NewReportWorkspaceSelectionPage.tsx b/src/pages/NewReportWorkspaceSelectionPage.tsx index 3e696907226d..6bc1d6d8db8c 100644 --- a/src/pages/NewReportWorkspaceSelectionPage.tsx +++ b/src/pages/NewReportWorkspaceSelectionPage.tsx @@ -42,8 +42,6 @@ function NewReportWorkspaceSelectionPage() { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); const shouldShowLoadingIndicator = isLoadingApp && !isOffline; - const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); - const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION, {canBeMissing: true}); const navigateToNewReport = useCallback( (optimisticReportID: string) => { @@ -73,10 +71,10 @@ function NewReportWorkspaceSelectionPage() { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyID)); return; } - const optimisticReportID = createNewReport(currentUserPersonalDetails, policyID, false, betas, betaConfiguration); + const optimisticReportID = createNewReport(currentUserPersonalDetails, policyID, false); navigateToNewReport(optimisticReportID); }, - [currentUserPersonalDetails, navigateToNewReport, betas, betaConfiguration], + [currentUserPersonalDetails, navigateToNewReport], ); const usersWorkspaces = useMemo(() => { diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 895ad0341ea3..80cab3e36f3a 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -156,8 +156,6 @@ function EmptySearchViewContent({ selector: (transactions) => Object.values(transactions ?? {}).filter((transaction) => transaction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length > 0, }); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); - const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); - const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION, {canBeMissing: true}); const shouldRedirectToExpensifyClassic = useMemo(() => { return areAllGroupPoliciesExpenseChatDisabled(allPolicies ?? {}); @@ -304,7 +302,7 @@ function EmptySearchViewContent({ } if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { - const createdReportID = createNewReport(currentUserPersonalDetails, workspaceIDForReportCreation, false, betas, betaConfiguration); + const createdReportID = createNewReport(currentUserPersonalDetails, workspaceIDForReportCreation, false); Navigation.setNavigationActionToMicrotaskQueue(() => { Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()})); }); @@ -430,8 +428,6 @@ function EmptySearchViewContent({ hasTransactions, tryNewDot?.hasBeenAddedToNudgeMigration, isUserPaidPolicyMember, - betas, - betaConfiguration, ]); return ( diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 9cc579c88619..31c558e60fb8 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -134,8 +134,6 @@ function AttachmentPickerWithMenuItems({ const {isBetaEnabled} = usePermissions(); const {setIsLoaderVisible} = useFullScreenLoader(); const isReportArchived = useReportIsArchived(report?.reportID); - const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); - const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION, {canBeMissing: true}); const isManualDistanceTrackingEnabled = isBetaEnabled(CONST.BETAS.MANUAL_DISTANCE); @@ -257,10 +255,10 @@ function AttachmentPickerWithMenuItems({ { icon: Expensicons.Document, text: translate('report.newReport.createReport'), - onSelected: () => selectOption(() => createNewReport(currentUserPersonalDetails, report?.policyID, true, betas, betaConfiguration), true), + onSelected: () => selectOption(() => createNewReport(currentUserPersonalDetails, report?.policyID, true), true), }, ]; - }, [currentUserPersonalDetails, report, selectOption, translate, betaConfiguration, betas]); + }, [currentUserPersonalDetails, report, selectOption, translate]); /** * Determines if we can show the task option diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index fe809c0c2b82..ae26c0b90dfa 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -136,8 +136,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.primaryLogin, canBeMissing: true}); const primaryContactMethod = primaryLogin ?? session?.email ?? ''; const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); - const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); - const [betaConfiguration] = useOnyx(ONYXKEYS.BETA_CONFIGURATION, {canBeMissing: true}); const canSendInvoice = useMemo(() => canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email), [allPolicies, session?.email]); const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); @@ -490,7 +488,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT } if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { - const createdReportID = createNewReport(currentUserPersonalDetails, workspaceIDForReportCreation, false, betas, betaConfiguration); + const createdReportID = createNewReport(currentUserPersonalDetails, workspaceIDForReportCreation, false); Navigation.setNavigationActionToMicrotaskQueue(() => { Navigation.navigate( isSearchTopmostFullScreenRoute() diff --git a/tests/unit/ReportTitleUtilsTest.ts b/tests/unit/ReportTitleUtilsTest.ts index d28aa93f9d55..a1e4af5e9513 100644 --- a/tests/unit/ReportTitleUtilsTest.ts +++ b/tests/unit/ReportTitleUtilsTest.ts @@ -12,6 +12,9 @@ jest.mock('@libs/ReportUtils', () => ({ getTitleReportField: jest.fn(), isChatReport: jest.fn(), })); +jest.mock('@libs/Permissions', () => ({ + isBetaEnabled: jest.fn().mockReturnValue(true), +})); const mockedReportUtils = ReportUtils as jest.Mocked; From 0cfebe67b1931b3a2eb228b16f9c3e33f8d773de Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 19 Sep 2025 13:08:29 +0200 Subject: [PATCH 7/9] Clean up code --- src/libs/actions/Report.ts | 39 ++++++++++--------- src/pages/NewReportWorkspaceSelectionPage.tsx | 2 +- src/pages/Search/EmptySearchView.tsx | 2 +- .../FloatingActionButtonAndPopover.tsx | 2 +- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d4902a423d72..93675eab0b87 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -226,8 +226,8 @@ import { } from './RequestConflictUtils'; import {canAnonymousUserAccessRoute, isAnonymousUser, signOutAndRedirectToSignIn, waitForUserSignIn} from './Session'; import {isOnboardingFlowCompleted, onServerDataReady, setOnboardingErrorMessage} from './Welcome'; -import type {OnboardingCompanySize, OnboardingMessage} from './Welcome/OnboardingFlow'; import {getOnboardingMessages, startOnboardingFlow} from './Welcome/OnboardingFlow'; +import type {OnboardingCompanySize, OnboardingMessage} from './Welcome/OnboardingFlow'; type SubscriberCallback = (isFromCurrentUser: boolean, reportAction: ReportAction | undefined) => void; @@ -2787,6 +2787,7 @@ function buildNewReportOptimisticData(policy: OnyxEntry, reportID: strin value: optimisticNextStep, }, ]; + optimisticData.push(...updateTitleFieldToMatchPolicy(reportID, policy)); const failureData: OnyxUpdate[] = [ @@ -6073,7 +6074,7 @@ function resolveConciergeCategoryOptions( } as Partial); } -export type {GuidedSetupData, IntroSelected, TaskForParameters, Video}; +export type {Video, GuidedSetupData, TaskForParameters, IntroSelected}; export { addAttachment, @@ -6081,29 +6082,23 @@ export { addPolicyReport, broadcastUserIsLeavingRoom, broadcastUserIsTyping, - buildInviteToRoomOnyxData, buildOptimisticChangePolicyData, - changeReportPolicy, - changeReportPolicyAndInviteSubmitter, clearAddRoomMemberError, clearAvatarErrors, - clearCreateChatError, clearDeleteTransactionNavigateBackUrl, clearGroupChat, clearIOUError, clearNewRoomFormError, + setNewRoomFormLoading, clearPolicyRoomNameErrors, clearPrivateNotesError, clearReportFieldKeyErrors, completeOnboarding, createNewReport, - createTransactionThreadReport, - deleteAppReport, deleteReport, deleteReportActionDraft, deleteReportComment, deleteReportField, - dismissChangePolicyModal, dismissTrackExpenseActionableWhisper, doneCheckingPublicRoom, downloadReportPDF, @@ -6119,48 +6114,43 @@ export { getMostRecentReportID, getNewerActions, getOlderActions, - getOptimisticChatReport, getReportPrivateNote, handleReportChanged, handleUserDeletedLinksInHtml, hasErrorInPrivateNotes, inviteToGroupChat, + buildInviteToRoomOnyxData, inviteToRoom, joinRoom, leaveGroupChat, leaveRoom, - markAllMessagesAsRead, markAsManuallyExported, markCommentAsUnread, - moveIOUReportToPolicy, - moveIOUReportToPolicyAndInviteSubmitter, navigateToAndOpenChildReport, navigateToAndOpenReport, navigateToAndOpenReportWithAccountIDs, navigateToConciergeChat, navigateToConciergeChatAndDeleteReport, + clearCreateChatError, notifyNewAction, openReport, openReportFromDeepLink, openRoomMembersPage, - openUnreportedExpense, readNewestAction, - removeFailedReport, + markAllMessagesAsRead, removeFromGroupChat, removeFromRoom, - resolveActionableMentionConfirmWhisper, resolveActionableMentionWhisper, + resolveActionableMentionConfirmWhisper, resolveActionableReportMentionWhisper, resolveConciergeCategoryOptions, savePrivateNotesDraft, saveReportActionDraft, - saveReportDraft, saveReportDraftComment, searchInServer, setDeleteTransactionNavigateBackUrl, setGroupDraft, setIsComposerFullSize, - setNewRoomFormLoading, shouldShowReportActionNotification, showReportActionNotification, startNewChat, @@ -6172,10 +6162,10 @@ export { toggleSubscribeToChildReport, unsubscribeFromLeavingRoomReportChannel, unsubscribeFromReportChannel, - updateChatName, updateDescription, updateGroupChatAvatar, updateGroupChatMemberRoles, + updateChatName, updateLastVisitTime, updateLoadingInitialReportAction, updateNotificationPreference, @@ -6185,4 +6175,15 @@ export { updateReportName, updateRoomVisibility, updateWriteCapability, + deleteAppReport, + getOptimisticChatReport, + saveReportDraft, + moveIOUReportToPolicy, + moveIOUReportToPolicyAndInviteSubmitter, + dismissChangePolicyModal, + changeReportPolicy, + changeReportPolicyAndInviteSubmitter, + removeFailedReport, + createTransactionThreadReport, + openUnreportedExpense, }; diff --git a/src/pages/NewReportWorkspaceSelectionPage.tsx b/src/pages/NewReportWorkspaceSelectionPage.tsx index 6bc1d6d8db8c..2a116f062579 100644 --- a/src/pages/NewReportWorkspaceSelectionPage.tsx +++ b/src/pages/NewReportWorkspaceSelectionPage.tsx @@ -71,7 +71,7 @@ function NewReportWorkspaceSelectionPage() { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyID)); return; } - const optimisticReportID = createNewReport(currentUserPersonalDetails, policyID, false); + const optimisticReportID = createNewReport(currentUserPersonalDetails, policyID); navigateToNewReport(optimisticReportID); }, [currentUserPersonalDetails, navigateToNewReport], diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 80cab3e36f3a..67e2b3f0d703 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -302,7 +302,7 @@ function EmptySearchViewContent({ } if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { - const createdReportID = createNewReport(currentUserPersonalDetails, workspaceIDForReportCreation, false); + const createdReportID = createNewReport(currentUserPersonalDetails, workspaceIDForReportCreation); Navigation.setNavigationActionToMicrotaskQueue(() => { Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()})); }); diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index ae26c0b90dfa..a6934c2b00ab 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -488,7 +488,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT } if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation)) { - const createdReportID = createNewReport(currentUserPersonalDetails, workspaceIDForReportCreation, false); + const createdReportID = createNewReport(currentUserPersonalDetails, workspaceIDForReportCreation); Navigation.setNavigationActionToMicrotaskQueue(() => { Navigation.navigate( isSearchTopmostFullScreenRoute() From f06fd448691a7a1f9995ee48efdad505df18b73b Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 19 Sep 2025 16:32:54 +0200 Subject: [PATCH 8/9] fixes according to comments --- src/libs/ReportTitleUtils.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportTitleUtils.ts b/src/libs/ReportTitleUtils.ts index 722dba0dc14e..5776c0326108 100644 --- a/src/libs/ReportTitleUtils.ts +++ b/src/libs/ReportTitleUtils.ts @@ -61,10 +61,10 @@ function updateTitleFieldToMatchPolicy(reportID: string, policy?: Policy): OnyxU } // Get the policy's title field configuration - const policyTitleField = getTitleReportField(policy.fieldList ?? {}); + const reportTitleField = getTitleReportField(policy.fieldList ?? {}); // Early return if policy doesn't have a title field - if (!policyTitleField) { + if (!reportTitleField) { return []; } @@ -75,7 +75,7 @@ function updateTitleFieldToMatchPolicy(reportID: string, policy?: Policy): OnyxU key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, value: { // eslint-disable-next-line @typescript-eslint/naming-convention - expensify_text_title: policyTitleField, + expensify_text_title: reportTitleField, }, }, ]; @@ -84,8 +84,7 @@ function updateTitleFieldToMatchPolicy(reportID: string, policy?: Policy): OnyxU } /** - * Remove title field from report's rNVP when report is manually renamed - * This indicates the report should preserve its custom name + * Remove title field from report's rNVP when report is manually renamed to indicate that the manual name should be preserved, and the custom report name formula should no longer update the name. */ function removeTitleFieldFromReport(reportID: string): OnyxUpdate[] { if (!Permissions.isBetaEnabled(CONST.BETAS.AUTH_AUTO_REPORT_TITLE, betas, betaConfiguration)) { @@ -125,8 +124,8 @@ function shouldUpdateTitleField(report: Report): boolean { // Skip if report has statement card ID (backend check: !getValue(db, reportID, NVP_STATEMENT_CARD_ID).empty()) // This would need to be implemented based on how statement card IDs are stored in the frontend - const policyTitleField = getTitleFieldFromRNVP(report.reportID); - if (!policyTitleField) { + const reportTitleField = getTitleFieldFromRNVP(report.reportID); + if (!reportTitleField) { return false; } From c4baf5477456f464f5e0ae6ec814359bac1aae96 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Tue, 23 Sep 2025 09:35:41 +0200 Subject: [PATCH 9/9] comment nab fixes --- src/libs/Formula.ts | 9 ++++++--- src/libs/OptimisticReportNames.ts | 11 ++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 96627bb284ec..5a809f07ddb7 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -38,7 +38,7 @@ const FORMULA_PART_TYPES = { * Extract formula parts from a formula string, handling nested braces and escapes * Based on OldDot Formula.extract method */ -function extract(formula: string, opener = '{', closer = '}'): string[] { +function extract(formula?: string, opener = '{', closer = '}'): string[] { if (!formula || typeof formula !== 'string') { return []; } @@ -79,7 +79,7 @@ function extract(formula: string, opener = '{', closer = '}'): string[] { * Parse a formula string into an array of formula parts * Based on OldDot Formula.parse method */ -function parse(formula: string): FormulaPart[] { +function parse(formula?: string): FormulaPart[] { if (!formula || typeof formula !== 'string') { return []; } @@ -191,10 +191,13 @@ function parsePart(definition: string): FormulaPart { /** * Compute the value of a formula given a context */ -function compute(formula: string, context: FormulaContext): string { +function compute(formula?: string, context?: FormulaContext): string { if (!formula || typeof formula !== 'string') { return ''; } + if (!context) { + return ''; + } const parts = parse(formula); let result = ''; diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 6437a8eabfe8..6e83bedd0a78 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -154,9 +154,9 @@ function shouldComputeReportName(report: Report, context: UpdateContext): boolea } // Only compute names for expense reports with policies that have title fields - // Check if the policy has a title field with a formula - const titleField = getTitleFieldFromRNVP(report.reportID, context); - if (!titleField?.defaultValue) { + // Check if the report has a title field with a formula in rNVP + const reportTitleField = getTitleFieldFromRNVP(report.reportID, context); + if (!reportTitleField?.defaultValue) { return false; } return true; @@ -195,13 +195,10 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O } const titleField = getTitleFieldFromRNVP(targetReport.reportID, context); - if (!titleField?.defaultValue) { - return null; - } // Quick check: see if the update might affect the report name const updateType = determineObjectTypeByKey(incomingUpdate.key); - const formula = titleField.defaultValue; + const formula = titleField?.defaultValue; const formulaParts = parse(formula); let transaction: Transaction | undefined;