From dca57d45c21cae805c7fe22dc86399a3fba54d58 Mon Sep 17 00:00:00 2001 From: "Pujan Shah (via MelvinBot)" Date: Sun, 5 Jul 2026 13:11:03 +0000 Subject: [PATCH 1/5] Fix report fields disappearing for submitter after report approval Co-authored-by: Pujan Shah --- .../MoneyRequestViewReportFields.tsx | 11 +++++------ src/components/ReportActionItem/MoneyReportView.tsx | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx index 0663dc5494f5..2f25a7851946 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx @@ -15,9 +15,7 @@ import { getReportFieldMaps, isGroupPolicyExpenseReport as isGroupPolicyExpenseReportUtils, isInvoiceReport as isInvoiceReportUtils, - isReportFieldDisabled, isReportFieldDisabledForUser, - isReportFieldOfTypeTitle, shouldHideSingleReportField, } from '@libs/ReportUtils'; @@ -121,10 +119,11 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false, }); }, [policy, report, currentUserAccountID]); - const enabledReportFields = sortedPolicyReportFields.filter( - (reportField) => !isReportFieldDisabled(report, reportField, policy) || reportField.type === CONST.REPORT_FIELD_TYPES.FORMULA, - ); - const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && isReportFieldOfTypeTitle(enabledReportFields.at(0)); + // Base the "only the title field is configured" check on which fields are present/displayable rather than on which are + // currently editable, so a custom field stays visible (read-only) after the report is approved/settled/closed. + // `sortedPolicyReportFields` is already filtered by `shouldHideSingleReportField`, so it holds only displayable non-title + // fields; when it's empty, only the title field remains. + const isOnlyTitleFieldEnabled = sortedPolicyReportFields.every(shouldHideSingleReportField); const isGroupPolicyExpenseReport = isGroupPolicyExpenseReportUtils(report); const isInvoiceReport = isInvoiceReportUtils(report); diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index d5e3ee87644d..436401a93b15 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -32,9 +32,7 @@ import { isClosedExpenseReportWithNoExpenses as isClosedExpenseReportWithNoExpensesReportUtils, isGroupPolicyExpenseReport as isGroupPolicyExpenseReportUtils, isInvoiceReport as isInvoiceReportUtils, - isReportFieldDisabled, isReportFieldDisabledForUser, - isReportFieldOfTypeTitle, isSettled as isSettledReportUtils, shouldHideSingleReportField, } from '@libs/ReportUtils'; @@ -154,10 +152,11 @@ function MoneyReportView({ return {sortedPolicyReportFields: sorted, fieldValues: values, fieldsByName: byName}; }, [policy?.fieldList, report]); - const enabledReportFields = sortedPolicyReportFields.filter( - (reportField) => !isReportFieldDisabled(report, reportField, policy) || reportField.type === CONST.REPORT_FIELD_TYPES.FORMULA, - ); - const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && isReportFieldOfTypeTitle(enabledReportFields.at(0)); + // Whether the only report field configured for this report is the title field. This must be based on which fields are + // present/displayable, not on which are currently editable: after approval every field becomes read-only for the + // submitter, but a still-present custom field should keep showing. `shouldHideSingleReportField` hides the title field + // and list fields with no enabled option, so if it's true for every field then only the title field remains. + const isOnlyTitleFieldEnabled = sortedPolicyReportFields.every(shouldHideSingleReportField); const isClosedExpenseReportWithNoExpenses = isClosedExpenseReportWithNoExpensesReportUtils(report); const isGroupPolicyExpenseReport = isGroupPolicyExpenseReportUtils(report); const isInvoiceReport = isInvoiceReportUtils(report); From b58ae3f8cb72228f641e681cfd8c87d197083457 Mon Sep 17 00:00:00 2001 From: "Pujan Shah (via MelvinBot)" Date: Sun, 5 Jul 2026 13:11:45 +0000 Subject: [PATCH 2/5] Remove now-unused CONST import Co-authored-by: Pujan Shah --- src/components/ReportActionItem/MoneyReportView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 436401a93b15..fd5eb1635d27 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -43,7 +43,6 @@ import AnimatedEmptyStateBackground from '@pages/inbox/report/AnimatedEmptyState import variables from '@styles/variables'; -import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import {clearReportFieldKeyErrors} from '@src/libs/actions/Report'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; From 42604eb47ff57235cd456a79475ee60424606962 Mon Sep 17 00:00:00 2001 From: "Pujan Shah (via MelvinBot)" Date: Sun, 5 Jul 2026 13:20:01 +0000 Subject: [PATCH 3/5] Add regression tests for report fields visibility after approval Co-authored-by: Pujan Shah --- tests/ui/MoneyReportViewTest.tsx | 116 ++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/tests/ui/MoneyReportViewTest.tsx b/tests/ui/MoneyReportViewTest.tsx index 9e79fb8990b2..836a8d1672b9 100644 --- a/tests/ui/MoneyReportViewTest.tsx +++ b/tests/ui/MoneyReportViewTest.tsx @@ -25,6 +25,13 @@ jest.mock('@hooks/useLocalize', () => })), ); +jest.mock('@hooks/useScreenWrapperTransitionStatus', () => ({ + __esModule: true, + default: () => ({ + didScreenTransitionEnd: true, + }), +})); + jest.mock('@react-navigation/native', () => ({ ...((): typeof NativeNavigation => jest.requireActual('@react-navigation/native'))(), useNavigation: jest.fn(() => ({navigate: jest.fn(), addListener: jest.fn(() => jest.fn())})), @@ -89,7 +96,7 @@ const seedReportAndTransactions = async (transactions: OnyxTypes.Transaction[], await waitForBatchedUpdatesWithAct(); }; -const renderMoneyReportView = (report: OnyxTypes.Report, policy: OnyxTypes.Policy | undefined = undefined) => +const renderMoneyReportView = (report: OnyxTypes.Report, policy: OnyxTypes.Policy | undefined = undefined, isCombinedReport = false) => render( , ); @@ -264,3 +272,109 @@ describe('MoneyReportView reimbursable/non-reimbursable breakdown rows', () => { }); }); }); + +describe('MoneyReportView report fields visibility', () => { + const customFieldKey = `expensify_${'field_test'}` as const; + + const buildTitleField = (): OnyxTypes.PolicyReportField => ({ + fieldID: CONST.REPORT_FIELD_TITLE_FIELD_ID, + name: 'title', + type: CONST.REPORT_FIELD_TYPES.FORMULA, + target: 'expense', + orderWeight: 1, + deletable: false, + defaultValue: '{report:type} {report:startdate}', + value: 'Expense Report', + values: [], + keys: [], + externalIDs: [], + disabledOptions: [], + isTax: false, + }); + + const buildCustomTextField = (): OnyxTypes.PolicyReportField => ({ + fieldID: 'field_test', + name: 'Test', + type: CONST.REPORT_FIELD_TYPES.TEXT, + target: 'expense', + orderWeight: 2, + deletable: true, + defaultValue: '', + value: '1', + values: [], + keys: [], + externalIDs: [], + disabledOptions: [], + isTax: false, + }); + + const buildReportFieldsPolicy = (fieldList: Record): OnyxTypes.Policy => + ({ + ...LHNTestUtils.getFakePolicy(policyID, 'Policy'), + // A non-admin submitter: report fields become read-only (not editable) after approval. + role: CONST.POLICY.ROLE.USER, + type: CONST.POLICY.TYPE.TEAM, + outputCurrency: CONST.CURRENCY.USD, + areReportFieldsEnabled: true, + fieldList, + }) as OnyxTypes.Policy; + + const seedReportFieldsPolicy = async (policy: OnyxTypes.Policy, report: OnyxTypes.Report) => { + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policy); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); + }); + await waitForBatchedUpdatesWithAct(); + }; + + beforeAll(() => { + Onyx.init({keys: ONYXKEYS, evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS]}); + }); + + afterEach(async () => { + await act(async () => { + await Onyx.clear(); + }); + }); + + it('keeps a custom report field visible for a non-admin submitter after the report is approved (single-expense combined view)', async () => { + const fieldList = { + [CONST.REPORT_FIELD_TITLE_FIELD_ID]: buildTitleField(), + [customFieldKey]: buildCustomTextField(), + }; + const policy = buildReportFieldsPolicy(fieldList); + const approvedReport = buildExpenseReport({ + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + fieldList, + }); + await seedReportFieldsPolicy(policy, approvedReport); + + renderMoneyReportView(approvedReport, policy, true); + await waitForBatchedUpdatesWithAct(); + + // The custom field (rendered read-only after approval) must still show for the submitter. + await waitFor(() => { + expect(screen.getByText('Test')).toBeOnTheScreen(); + }); + }); + + it('hides the report-fields section in the combined view when only the title field is configured', async () => { + const fieldList = {[CONST.REPORT_FIELD_TITLE_FIELD_ID]: buildTitleField()}; + const policy = buildReportFieldsPolicy(fieldList); + const approvedReport = buildExpenseReport({ + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + fieldList, + }); + await seedReportFieldsPolicy(policy, approvedReport); + + renderMoneyReportView(approvedReport, policy, true); + await waitForBatchedUpdatesWithAct(); + + // The title field is shown in the report header for a combined report, so the redundant section stays hidden. + await waitFor(() => { + expect(screen.queryByText('title')).not.toBeOnTheScreen(); + }); + }); +}); From ff93af845b8a2e7c3fa387a54aa402abfb5272da Mon Sep 17 00:00:00 2001 From: "Pujan Shah (via MelvinBot)" Date: Sun, 5 Jul 2026 14:18:30 +0000 Subject: [PATCH 4/5] Simplify report-field visibility conditions Collapse the redundant report-field-visibility clauses. In MoneyReportView, (!isCombinedReport || !isOnlyTitleFieldEnabled) && !every(shouldHideSingleReportField) reduces to !isOnlyTitleFieldEnabled by absorption. In MoneyRequestViewReportFields, sortedPolicyReportFields is already filtered by shouldHideSingleReportField and isCombinedReport is never passed, so isOnlyTitleFieldEnabled and the unused isCombinedReport prop are dropped; the empty-list early return covers the case. Co-authored-by: Pujan Shah --- .../MoneyRequestViewReportFields.tsx | 15 +++++---------- .../ReportActionItem/MoneyReportView.tsx | 7 +------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx index 2f25a7851946..2e37080f7e79 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx @@ -39,9 +39,6 @@ type MoneyRequestViewReportFieldsProps = { /** Policy that the report belongs to */ policy: OnyxEntry; - /** Indicates whether the IOU report is a combined report */ - isCombinedReport?: boolean; - /** Indicates whether we have any pending actions from parent component */ pendingAction?: PendingAction; }; @@ -87,7 +84,7 @@ function ReportFieldView(reportField: EnrichedPolicyReportField, report: OnyxEnt ); } -function MoneyRequestViewReportFields({report, policy, isCombinedReport = false, pendingAction}: MoneyRequestViewReportFieldsProps) { +function MoneyRequestViewReportFields({report, policy, pendingAction}: MoneyRequestViewReportFieldsProps) { const styles = useThemeStyles(); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); @@ -119,15 +116,13 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false, }); }, [policy, report, currentUserAccountID]); - // Base the "only the title field is configured" check on which fields are present/displayable rather than on which are - // currently editable, so a custom field stays visible (read-only) after the report is approved/settled/closed. - // `sortedPolicyReportFields` is already filtered by `shouldHideSingleReportField`, so it holds only displayable non-title - // fields; when it's empty, only the title field remains. - const isOnlyTitleFieldEnabled = sortedPolicyReportFields.every(shouldHideSingleReportField); const isGroupPolicyExpenseReport = isGroupPolicyExpenseReportUtils(report); const isInvoiceReport = isInvoiceReportUtils(report); - const shouldDisplayReportFields = (isGroupPolicyExpenseReport || isInvoiceReport) && !!policy?.areReportFieldsEnabled && (!isOnlyTitleFieldEnabled || !isCombinedReport); + // `sortedPolicyReportFields` is already filtered by `shouldHideSingleReportField`, so it only holds displayable non-title + // fields. When it's empty (only the title field remains) the early return below hides the section, so we don't need a + // separate "only the title field is configured" check here. + const shouldDisplayReportFields = (isGroupPolicyExpenseReport || isInvoiceReport) && !!policy?.areReportFieldsEnabled; if (!shouldDisplayReportFields || !sortedPolicyReportFields.length) { return null; diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index fd5eb1635d27..17ffa6cebab1 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -160,12 +160,7 @@ function MoneyReportView({ const isGroupPolicyExpenseReport = isGroupPolicyExpenseReportUtils(report); const isInvoiceReport = isInvoiceReportUtils(report); - const shouldShowReportField = - !isClosedExpenseReportWithNoExpenses && - (isGroupPolicyExpenseReport || isInvoiceReport) && - !!policy?.areReportFieldsEnabled && - (!isCombinedReport || !isOnlyTitleFieldEnabled) && - !sortedPolicyReportFields.every(shouldHideSingleReportField); + const shouldShowReportField = !isClosedExpenseReportWithNoExpenses && (isGroupPolicyExpenseReport || isInvoiceReport) && !!policy?.areReportFieldsEnabled && !isOnlyTitleFieldEnabled; const hasPendingAction = transactions.some(getTransactionPendingAction); From 7ddf8148ba762f47bcf937ab1f96da8beba438ff Mon Sep 17 00:00:00 2001 From: "Pujan Shah (via MelvinBot)" Date: Mon, 6 Jul 2026 10:23:34 +0000 Subject: [PATCH 5/5] Remove unneeded explanatory comments on report-field visibility Co-authored-by: Pujan Shah --- .../MoneyRequestReportView/MoneyRequestViewReportFields.tsx | 3 --- src/components/ReportActionItem/MoneyReportView.tsx | 4 ---- 2 files changed, 7 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx index 2e37080f7e79..e18e88599e90 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx @@ -119,9 +119,6 @@ function MoneyRequestViewReportFields({report, policy, pendingAction}: MoneyRequ const isGroupPolicyExpenseReport = isGroupPolicyExpenseReportUtils(report); const isInvoiceReport = isInvoiceReportUtils(report); - // `sortedPolicyReportFields` is already filtered by `shouldHideSingleReportField`, so it only holds displayable non-title - // fields. When it's empty (only the title field remains) the early return below hides the section, so we don't need a - // separate "only the title field is configured" check here. const shouldDisplayReportFields = (isGroupPolicyExpenseReport || isInvoiceReport) && !!policy?.areReportFieldsEnabled; if (!shouldDisplayReportFields || !sortedPolicyReportFields.length) { diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 17ffa6cebab1..f42846d8febd 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -151,10 +151,6 @@ function MoneyReportView({ return {sortedPolicyReportFields: sorted, fieldValues: values, fieldsByName: byName}; }, [policy?.fieldList, report]); - // Whether the only report field configured for this report is the title field. This must be based on which fields are - // present/displayable, not on which are currently editable: after approval every field becomes read-only for the - // submitter, but a still-present custom field should keep showing. `shouldHideSingleReportField` hides the title field - // and list fields with no enabled option, so if it's true for every field then only the title field remains. const isOnlyTitleFieldEnabled = sortedPolicyReportFields.every(shouldHideSingleReportField); const isClosedExpenseReportWithNoExpenses = isClosedExpenseReportWithNoExpensesReportUtils(report); const isGroupPolicyExpenseReport = isGroupPolicyExpenseReportUtils(report);