From 84ecb573e34c6acf63b6ded64ad8228f1603a05e Mon Sep 17 00:00:00 2001 From: "Shridhar Goel (via MelvinBot)" Date: Fri, 19 Jun 2026 08:07:25 +0000 Subject: [PATCH 1/4] Recreate InitialListValueSelectorModal dynamic route migration with regression fixes Re-applies the migration of InitialListValueSelectorModal to a @react-navigation dynamic route (originally Expensify/App#91627) and adds fixes for the regressions it introduced: - Issue 93138: redirect back to the Add field page when the dynamic Initial value page is opened via refresh/deeplink without available list values, instead of rendering an empty picker. - Issue 93141: drop includePaddingTop={false} so the page no longer overlaps the device status bar on native (the prop was only safe inside the old RIGHT_DOCKED modal). - Issue 93186: sync the draft initial value back into the form so the validation error clears after a value is selected on the dynamic route. Co-authored-by: Shridhar Goel --- src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../ModalStackNavigators/index.tsx | 2 + .../RELATIONS/WORKSPACE_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 3 + .../reports/CreateReportFieldsPage.tsx | 1 - ...ynamicReportFieldsInitialListValuePage.tsx | 98 +++++++++++++++++++ .../InitialListValueSelectorModal.tsx | 71 -------------- .../ReportFieldsInitialListValuePicker.tsx | 1 + .../InitialListValueSelector/index.tsx | 48 +++------ 11 files changed, 127 insertions(+), 104 deletions(-) create mode 100644 src/pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage.tsx delete mode 100644 src/pages/workspace/reports/InitialListValueSelector/InitialListValueSelectorModal.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d9790039e6ab..c0d3f4478416 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -888,6 +888,10 @@ const DYNAMIC_ROUTES = { entryScreens: [SCREENS.REPORT, SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.RIGHT_MODAL.EXPENSE_REPORT, SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT], getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}`, }, + WORKSPACE_REPORT_FIELDS_INITIAL_LIST_VALUE: { + path: 'initial-list-value', + entryScreens: [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE], + }, } as const satisfies DynamicRoutes; const ROUTES = { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 5ff83221799a..ba5837eab71c 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -794,6 +794,7 @@ const SCREENS = { REPORT_FIELDS_VALUE_SETTINGS: 'Workspace_ReportFields_ValueSettings', REPORT_FIELDS_EDIT_VALUE: 'Workspace_ReportFields_EditValue', REPORT_FIELDS_EDIT_INITIAL_VALUE: 'Workspace_ReportFields_EditInitialValue', + DYNAMIC_REPORT_FIELDS_INITIAL_LIST_VALUE: 'Dynamic_Report_Fields_Initial_List_Value', REPORT_FIELDS_TYPE_SELECTOR: 'Workspace_ReportFields_TypeSelector', TAX_EDIT: 'Workspace_Tax_Edit', TAX_NAME: 'Workspace_Tax_Name', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 6b35bc43845b..d3a69fc80328 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -991,6 +991,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Subscription/PaymentCard').default), [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require('../../../../pages/workspace/reports/CreateReportFieldsPage').default, + [SCREENS.WORKSPACE.DYNAMIC_REPORT_FIELDS_INITIAL_LIST_VALUE]: () => + require('../../../../pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage').default, [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: () => require('../../../../pages/workspace/reports/ReportFieldsSettingsPage').default, [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: () => require('../../../../pages/workspace/reports/ReportFieldsListValuesPage').default, [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: () => require('../../../../pages/workspace/reports/ReportFieldsAddListValuePage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index c6e8cf63afc8..e19640792545 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -298,6 +298,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: { path: ROUTES.WORKSPACE_CREATE_REPORT_FIELD.route, }, + [SCREENS.WORKSPACE.DYNAMIC_REPORT_FIELDS_INITIAL_LIST_VALUE]: DYNAMIC_ROUTES.WORKSPACE_REPORT_FIELDS_INITIAL_LIST_VALUE.path, [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: { path: ROUTES.WORKSPACE_REPORT_FIELDS_LIST_VALUES.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b69da1026616..f6f93c15a3d6 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -639,6 +639,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: { policyID: string; }; + [SCREENS.WORKSPACE.DYNAMIC_REPORT_FIELDS_INITIAL_LIST_VALUE]: { + policyID: string; + }; [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: { policyID: string; reportFieldID?: string; diff --git a/src/pages/workspace/reports/CreateReportFieldsPage.tsx b/src/pages/workspace/reports/CreateReportFieldsPage.tsx index 799374bb89e7..320c689a527d 100644 --- a/src/pages/workspace/reports/CreateReportFieldsPage.tsx +++ b/src/pages/workspace/reports/CreateReportFieldsPage.tsx @@ -247,7 +247,6 @@ function WorkspaceCreateReportFieldsPage({ InputComponent={InitialListValueSelector} inputID={INPUT_IDS.INITIAL_VALUE} label={translate('common.initialValue')} - subtitle={translate('workspace.reportFields.listValuesInputSubtitle')} shouldSaveDraft /> )} diff --git a/src/pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage.tsx b/src/pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage.tsx new file mode 100644 index 000000000000..e28fedacfed0 --- /dev/null +++ b/src/pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage.tsx @@ -0,0 +1,98 @@ +import React, {useEffect} from 'react'; +import {View} from 'react-native'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {setDraftValues} from '@libs/actions/FormActions'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import {hasAccountingConnections} from '@libs/PolicyUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm'; +import ReportFieldsInitialListValuePicker from './ReportFieldsInitialListValuePicker'; + +type DynamicReportFieldsInitialListValuePageProps = WithPolicyAndFullscreenLoadingProps & + PlatformStackScreenProps; + +function DynamicReportFieldsInitialListValuePage({ + policy, + route: { + params: {policyID}, + }, +}: DynamicReportFieldsInitialListValuePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [formDraft, formDraftMetadata] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT); + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.WORKSPACE_REPORT_FIELDS_INITIAL_LIST_VALUE.path); + + const currentValue = formDraft?.[INPUT_IDS.INITIAL_VALUE] ?? ''; + const isLoadingFormDraft = formDraftMetadata.status !== 'loaded'; + // Mirror the guard used on the create page: the initial value selector is only relevant when there are + // available (non-disabled) list values. On a refresh or deeplink the form draft can be empty, in which case + // we send the user back to the Add field page instead of showing an empty picker. + const availableListValuesLength = (formDraft?.[INPUT_IDS.DISABLED_LIST_VALUES] ?? []).filter((disabledListValue) => !disabledListValue).length; + const shouldRedirectToCreatePage = !isLoadingFormDraft && availableListValuesLength === 0; + + useEffect(() => { + if (!shouldRedirectToCreatePage) { + return; + } + Navigation.goBack(backPath); + }, [shouldRedirectToCreatePage, backPath]); + + const onValueSelected = (value: string) => { + setDraftValues(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM, { + [INPUT_IDS.INITIAL_VALUE]: currentValue === value ? '' : value, + }); + Navigation.goBack(backPath); + }; + + return ( + + + Navigation.goBack(backPath)} + /> + {isLoadingFormDraft || shouldRedirectToCreatePage ? ( + + ) : ( + <> + + {translate('workspace.reportFields.listValuesInputSubtitle')} + + + + )} + + + ); +} + +export default withPolicyAndFullscreenLoading(DynamicReportFieldsInitialListValuePage); diff --git a/src/pages/workspace/reports/InitialListValueSelector/InitialListValueSelectorModal.tsx b/src/pages/workspace/reports/InitialListValueSelector/InitialListValueSelectorModal.tsx deleted file mode 100644 index a6d39b3ee6cf..000000000000 --- a/src/pages/workspace/reports/InitialListValueSelector/InitialListValueSelectorModal.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import Modal from '@components/Modal'; -import ScreenWrapper from '@components/ScreenWrapper'; -import Text from '@components/Text'; -import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ReportFieldsInitialListValuePicker from './ReportFieldsInitialListValuePicker'; - -type InitialListValueSelectorModalProps = { - /** Whether the modal is visible */ - isVisible: boolean; - - /** Selected value */ - currentValue: string; - - /** Label to display on field */ - label: string; - - /** Subtitle to display on field */ - subtitle: string; - - /** Function to call when the user selects a value */ - onValueSelected: (value: string) => void; - - /** Function to call when the user closes the value selector modal */ - onClose: () => void; -}; - -function InitialListValueSelectorModal({isVisible, currentValue, label, subtitle, onValueSelected, onClose}: InitialListValueSelectorModalProps) { - const styles = useThemeStyles(); - - const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT); - - return ( - - - - - {subtitle} - - - - - ); -} - -export default InitialListValueSelectorModal; diff --git a/src/pages/workspace/reports/InitialListValueSelector/ReportFieldsInitialListValuePicker.tsx b/src/pages/workspace/reports/InitialListValueSelector/ReportFieldsInitialListValuePicker.tsx index e57a8b862b06..9592234e92ff 100644 --- a/src/pages/workspace/reports/InitialListValueSelector/ReportFieldsInitialListValuePicker.tsx +++ b/src/pages/workspace/reports/InitialListValueSelector/ReportFieldsInitialListValuePicker.tsx @@ -39,6 +39,7 @@ function ReportFieldsInitialListValuePicker({listValues, disabledOptions, value, ListItem={SingleSelectListItem} onSelectRow={(item) => onValueChange(item.value)} initiallyFocusedItemKey={listValueOptions.find((listValue) => listValue.isSelected)?.keyForList} + shouldSingleExecuteRowSelect addBottomSafeAreaPadding /> ); diff --git a/src/pages/workspace/reports/InitialListValueSelector/index.tsx b/src/pages/workspace/reports/InitialListValueSelector/index.tsx index 0ea7cb414b64..4df9d2284327 100644 --- a/src/pages/workspace/reports/InitialListValueSelector/index.tsx +++ b/src/pages/workspace/reports/InitialListValueSelector/index.tsx @@ -1,21 +1,19 @@ import type {ForwardedRef} from 'react'; -import React, {useEffect, useState} from 'react'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; import type {MenuItemBaseProps} from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import useOnyx from '@hooks/useOnyx'; -import blurActiveElement from '@libs/Accessibility/blurActiveElement'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; +import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import InitialListValueSelectorModal from './InitialListValueSelectorModal'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; type InitialListValueSelectorProps = Pick & { /** Currently selected value */ value?: string; - /** Subtitle to display on field */ - subtitle?: string; - /** Function to call when the user selects a value */ onInputChange?: (value: string) => void; @@ -23,24 +21,9 @@ type InitialListValueSelectorProps = Pick; }; -function InitialListValueSelector({value = '', label = '', rightLabel, subtitle = '', errorText = '', onInputChange, ref}: InitialListValueSelectorProps) { +function InitialListValueSelector({value = '', label = '', rightLabel, errorText = '', onInputChange, ref}: InitialListValueSelectorProps) { const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT); - - const [isPickerVisible, setIsPickerVisible] = useState(false); - - const showPickerModal = () => { - setIsPickerVisible(true); - }; - - const hidePickerModal = () => { - setIsPickerVisible(false); - blurActiveElement(); - }; - - const updateValueInput = (initialValue: string) => { - onInputChange?.(value === initialValue ? '' : initialValue); - hidePickerModal(); - }; + const draftInitialValue = formDraft?.initialValue ?? ''; useEffect(() => { const currentValueIndex = Object.values(formDraft?.listValues ?? {}).findIndex((listValue) => listValue === value); @@ -51,6 +34,15 @@ function InitialListValueSelector({value = '', label = '', rightLabel, subtitle } }, [formDraft?.disabledListValues, formDraft?.listValues, onInputChange, value]); + // The value is selected on a separate dynamic route which writes directly to the form draft. Sync that draft + // value back into the form here so the form's value updates and any existing validation error is cleared. + useEffect(() => { + if (draftInitialValue === value) { + return; + } + onInputChange?.(draftInitialValue); + }, [draftInitialValue, value, onInputChange]); + return ( - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_REPORT_FIELDS_INITIAL_LIST_VALUE.path))} /> ); From 6ec9bab191a8155597d247e20694908365466a2a Mon Sep 17 00:00:00 2001 From: "Shridhar Goel (via MelvinBot)" Date: Fri, 19 Jun 2026 08:26:32 +0000 Subject: [PATCH 2/4] Fix typecheck: pass required reasonAttributes to FullScreenLoadingIndicator Co-authored-by: Shridhar Goel --- .../DynamicReportFieldsInitialListValuePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage.tsx b/src/pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage.tsx index e28fedacfed0..0b7c0e2362f8 100644 --- a/src/pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage.tsx +++ b/src/pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage.tsx @@ -76,7 +76,7 @@ function DynamicReportFieldsInitialListValuePage({ onBackButtonPress={() => Navigation.goBack(backPath)} /> {isLoadingFormDraft || shouldRedirectToCreatePage ? ( - + ) : ( <> From 8ac54efe4ea3bdbc95f140cc6fd95edb4728a4fa Mon Sep 17 00:00:00 2001 From: "Shridhar Goel (via MelvinBot)" Date: Sat, 20 Jun 2026 18:36:19 +0000 Subject: [PATCH 3/4] Trigger form validation when initial value is selected via dynamic route (issue 93186) Co-authored-by: Shridhar Goel --- .../reports/InitialListValueSelector/index.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/reports/InitialListValueSelector/index.tsx b/src/pages/workspace/reports/InitialListValueSelector/index.tsx index 4df9d2284327..26996455418b 100644 --- a/src/pages/workspace/reports/InitialListValueSelector/index.tsx +++ b/src/pages/workspace/reports/InitialListValueSelector/index.tsx @@ -1,5 +1,5 @@ import type {ForwardedRef} from 'react'; -import React, {useEffect} from 'react'; +import React, {useEffect, useRef} from 'react'; import {View} from 'react-native'; import type {MenuItemBaseProps} from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -34,14 +34,19 @@ function InitialListValueSelector({value = '', label = '', rightLabel, errorText } }, [formDraft?.disabledListValues, formDraft?.listValues, onInputChange, value]); - // The value is selected on a separate dynamic route which writes directly to the form draft. Sync that draft - // value back into the form here so the form's value updates and any existing validation error is cleared. + // The value is selected on a separate dynamic route which writes directly to the form draft. FormProvider + // already syncs that draft write into the form's value, but it never re-runs validation, so the "Initial value" + // required error lingers. We can't detect the change by comparing the draft against `value` (they're already in + // sync by the time this runs), so we track the previous draft value and push it through onInputChange whenever it + // changes. onInputChange triggers the form's onValidate, which clears the stale error. + const previousDraftInitialValue = useRef(draftInitialValue); useEffect(() => { - if (draftInitialValue === value) { + if (draftInitialValue === previousDraftInitialValue.current) { return; } + previousDraftInitialValue.current = draftInitialValue; onInputChange?.(draftInitialValue); - }, [draftInitialValue, value, onInputChange]); + }, [draftInitialValue, onInputChange]); return ( From df3bbec6bc22ceeb7421570792e3d827307f37a7 Mon Sep 17 00:00:00 2001 From: "Shridhar Goel (via MelvinBot)" Date: Sun, 21 Jun 2026 12:05:47 +0000 Subject: [PATCH 4/4] Use ActivityIndicator instead of FullScreenLoadingIndicator (UI-1) Co-authored-by: Shridhar Goel --- .../DynamicReportFieldsInitialListValuePage.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage.tsx b/src/pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage.tsx index 0b7c0e2362f8..f6d9bb27cd75 100644 --- a/src/pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage.tsx +++ b/src/pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage.tsx @@ -1,6 +1,6 @@ import React, {useEffect} from 'react'; import {View} from 'react-native'; -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import ActivityIndicator from '@components/ActivityIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; @@ -76,7 +76,12 @@ function DynamicReportFieldsInitialListValuePage({ onBackButtonPress={() => Navigation.goBack(backPath)} /> {isLoadingFormDraft || shouldRedirectToCreatePage ? ( - + + + ) : ( <>