Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d9ff3bd
Reapply "feat: Add inline editing for tables on desktop"
mohammadjafarinejad Apr 29, 2026
cee943c
fix: Unreported expense row is broken, amount
mohammadjafarinejad Apr 24, 2026
dee8d7d
fix: Merchant in distance expense can be edited by inline editing
mohammadjafarinejad Apr 24, 2026
3b19a92
fix: Merchant field accepts (none) merchant
mohammadjafarinejad Apr 24, 2026
c5ebbc2
fix: Negative sign cannot be entered on amount field
mohammadjafarinejad Apr 24, 2026
d80e491
refactor: simplify canEditRestricted logic
mohammadjafarinejad Apr 24, 2026
9b4bf06
fix: use original Onyx for complete policy data
mohammadjafarinejad Apr 29, 2026
37d11ed
fix: add isScanning check to transaction edit permissions
mohammadjafarinejad Apr 29, 2026
211728a
fix: use requestAnimationFrame for popover opening
mohammadjafarinejad Apr 29, 2026
89a0a0b
fix: update merchant editing logic
mohammadjafarinejad Apr 29, 2026
de01021
fix: disable category editing when shouldSelectPolicy is true
mohammadjafarinejad Apr 29, 2026
aac22b5
fix: update transaction amount validation to allow zero for IOU reports
mohammadjafarinejad Apr 29, 2026
1739e89
Merge branch 'main' into fix/82534-v2
mohammadjafarinejad May 1, 2026
637ded6
fix: remove unused variable
mohammadjafarinejad May 1, 2026
d78f14a
fix: resolve no-restricted-imports violation
mohammadjafarinejad May 1, 2026
8af50c4
fix: add money request amount validation
mohammadjafarinejad May 1, 2026
14df527
fix: add shouldEnableBackdropInNarrowPane props to modals
mohammadjafarinejad May 1, 2026
1a1369a
fix: prevent row press during inline editing
mohammadjafarinejad May 1, 2026
0a13da8
fix: use completePolicy in getEditParams
mohammadjafarinejad May 1, 2026
fba4e59
fix: check areCategoriesEnabled in transaction edit permission
mohammadjafarinejad May 2, 2026
e788c96
fix: handle fallback for transactionThreadReport
mohammadjafarinejad May 2, 2026
c64c0bd
fix: update handleSave to trim merchant values before saving
mohammadjafarinejad May 2, 2026
7ab8121
fix: only disable merchant and amount fields during receipt scanning
mohammadjafarinejad May 2, 2026
29d919f
Merge branch 'main' into fix/82534-v2
mohammadjafarinejad May 2, 2026
d943e25
feat: add merchant validation logic
mohammadjafarinejad May 2, 2026
3ea4a76
chore: update comment
mohammadjafarinejad May 2, 2026
ad2f7a3
feat: implement adaptive popover height for Category and Tag pickers
mohammadjafarinejad May 2, 2026
d1643eb
fix: sync localValue
mohammadjafarinejad May 2, 2026
d074593
fix: remove redundant test for onSave
mohammadjafarinejad May 2, 2026
d5e3f23
test: add unit tests for inline editing
mohammadjafarinejad May 3, 2026
b9a4116
test: add unit tests for money request amount and merchant validation
mohammadjafarinejad May 3, 2026
f7e52c4
test: fix and consolidate transaction edit unit tests
mohammadjafarinejad May 3, 2026
f6100ad
fix: persist inline edits in snapshot data when offline
mohammadjafarinejad May 3, 2026
657e61c
Merge branch 'main' into fix/82534-v2
mohammadjafarinejad May 4, 2026
ab117cd
Merge branch 'main' into fix/82534-v2
mohammadjafarinejad May 5, 2026
11b8c55
fix: replace adjustedPopoverHeight with fixed height
mohammadjafarinejad May 6, 2026
d9ed613
refactor: rename backdrop prop
mohammadjafarinejad May 6, 2026
6aa2610
refactor: remove unused props and components
mohammadjafarinejad May 6, 2026
4b4313c
fix: update isValidMerchant validation for IOU reports
mohammadjafarinejad May 6, 2026
746d25e
fix: add equality check and address PR feedback
mohammadjafarinejad May 6, 2026
e91528f
Merge branch 'main' into fix/82534-v2
mohammadjafarinejad May 6, 2026
7b76715
fix: resolve TS error
mohammadjafarinejad May 6, 2026
9dec36b
Merge branch 'main' into fix/82534-v2
mohammadjafarinejad May 10, 2026
41fd31f
revert: remove unrelated changes to YearPickerModal
mohammadjafarinejad May 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import SafeArea from './components/SafeArea';
import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider';
import SidePanelContextProvider from './components/SidePanel/SidePanelContextProvider';
import SVGDefinitionsProvider from './components/SVGDefinitionsProvider';
import {EditingCellProvider} from './components/Table/EditableCell';
import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider';
import ThemeProvider from './components/ThemeProvider';
import ThemeStylesProvider from './components/ThemeStylesContextProvider';
Expand Down Expand Up @@ -115,6 +116,7 @@ function App() {
FullScreenLoaderContextProvider,
ModalProvider,
SidePanelContextProvider,
EditingCellProvider,
]}
>
<CustomStatusBarAndBackground />
Expand Down
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9035,6 +9035,9 @@ const CONST = {
SORTABLE_HEADER: 'Search-SortableHeader',
UNREPORTED_EXPENSE_LIST_ITEM: 'UnreportedExpenseListItem',
},
TABLE: {
EDITABLE_CELL: 'Table-EditableCell',
},
REPORT: {
FLOATING_MESSAGE_COUNTER: 'Report-FloatingMessageCounter',
LIST_BOUNDARY_LOADER_RETRY: 'Report-ListBoundaryLoaderRetry',
Expand Down
86 changes: 86 additions & 0 deletions src/components/CategoryPicker/CategoryPickerModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, {useRef} from 'react';
import {View} from 'react-native';
import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent';
import type PopoverWithMeasuredContentProps from '@components/PopoverWithMeasuredContent/types';
import type {ListItem} from '@components/SelectionList/types';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import CategoryPicker from '.';

const popoverDimensions = {
width: CONST.POPOVER_DROPDOWN_WIDTH,
height: CONST.POPOVER_DROPDOWN_MAX_HEIGHT,
};

const DEFAULT_ANCHOR_ALIGNMENT = {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
};

type CategoryPickerModalProps = {
/** Callback to close the modal */
onClose: () => void;

/** The policy whose categories should be shown */
policyID: string | undefined;

/** Currently selected category */
selectedCategory?: string;

/** Called when the user confirms a category selection */
onSelected?: (item: ListItem) => void;
} & Omit<PopoverWithMeasuredContentProps, 'anchorRef' | 'children' | 'onClose'>;

function CategoryPickerModal({
isVisible,
onClose,
anchorPosition,
policyID,
selectedCategory,
onSelected,
anchorAlignment = DEFAULT_ANCHOR_ALIGNMENT,
shouldMeasureAnchorPositionFromTop = false,
}: CategoryPickerModalProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const anchorRef = useRef<View>(null);

const handleCategorySelect = (item: ListItem) => {
// If clicking the same category that's already selected, treat it as deselection
if (item.keyForList === selectedCategory) {
onSelected?.({keyForList: '', searchText: ''});
} else {
onSelected?.(item);
}
onClose();
};

return (
<PopoverWithMeasuredContent
anchorRef={anchorRef}
isVisible={isVisible}
onClose={onClose}
anchorPosition={anchorPosition}
popoverDimensions={popoverDimensions}
anchorAlignment={anchorAlignment}
innerContainerStyle={StyleUtils.getWidthStyle(popoverDimensions.width)}
restoreFocusType={CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE}
shouldSwitchPositionIfOverflow
shouldEnableNewFocusManagement
shouldMeasureAnchorPositionFromTop={shouldMeasureAnchorPositionFromTop}
shouldSkipRemeasurement
shouldDisplayBelowModals
>
<View style={[StyleUtils.getHeight(popoverDimensions.height), styles.flexColumn, styles.pt4]}>
<CategoryPicker
selectedCategory={selectedCategory}
policyID={policyID}
onSubmit={handleCategorySelect}
/>
</View>
</PopoverWithMeasuredContent>
);
}

export default CategoryPickerModal;
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import React from 'react';
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections';
import type {ListItem} from '@components/SelectionList/types';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -12,10 +16,6 @@ import {getHeaderMessageForNonUserList} from '@libs/OptionsListUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import SingleSelectListItem from './SelectionList/ListItem/SingleSelectListItem';
import SelectionListWithSections from './SelectionList/SelectionListWithSections';
import type {ListItem} from './SelectionList/types';
import type {BaseTextInputRef} from './TextInput/BaseTextInput/types';

type CategoryPickerProps = {
policyID: string | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ type MonthPickerModalProps = {

/** Function to call when the user closes the month picker */
onClose?: () => void;

/** Whether RIGHT_DOCKED modal should keep backdrop in narrow pane context */
shouldEnableBackdropInNarrowPane?: boolean;
};

function MonthPickerModal({isVisible, currentMonth = new Date().getMonth(), onMonthChange, onClose}: MonthPickerModalProps) {
function MonthPickerModal({isVisible, currentMonth = new Date().getMonth(), onMonthChange, onClose, shouldEnableBackdropInNarrowPane = false}: MonthPickerModalProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [searchText, setSearchText] = useState('');
Expand Down Expand Up @@ -66,6 +69,7 @@ function MonthPickerModal({isVisible, currentMonth = new Date().getMonth(), onMo
shouldHandleNavigationBack
shouldUseCustomBackdrop
onBackdropPress={onClose}
shouldKeepRightDockedBackdropInNarrowPane={shouldEnableBackdropInNarrowPane}
enableEdgeToEdgeBottomSafeAreaPadding
>
<ScreenWrapper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ type YearPickerModalProps = {

/** Function to call when the user closes the year picker */
onClose?: () => void;

/** Whether RIGHT_DOCKED modal should keep backdrop in narrow pane context */
shouldEnableBackdropInNarrowPane?: boolean;
};

function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear(), onYearChange, onClose}: YearPickerModalProps) {
function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear(), onYearChange, onClose, shouldEnableBackdropInNarrowPane = false}: YearPickerModalProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [searchText, setSearchText] = useState('');
Expand Down Expand Up @@ -67,6 +70,7 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear
shouldHandleNavigationBack
shouldUseCustomBackdrop
onBackdropPress={onClose}
shouldKeepRightDockedBackdropInNarrowPane={shouldEnableBackdropInNarrowPane}
enableEdgeToEdgeBottomSafeAreaPadding
>
<ScreenWrapper
Expand Down
6 changes: 6 additions & 0 deletions src/components/DatePicker/CalendarPicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ type CalendarPickerProps = {

/** Optional style override for the header container */
headerContainerStyle?: StyleProp<ViewStyle>;

/** Whether Month/Year right-docked picker modals should keep backdrop in narrow pane context */
shouldEnableMonthYearBackdropInNarrowPane?: boolean;
};

function getInitialCurrentDateView(value: Date | string, minDate: Date, maxDate: Date) {
Expand Down Expand Up @@ -71,6 +74,7 @@ function CalendarPicker({
DayComponent = Day,
selectableDates,
headerContainerStyle,
shouldEnableMonthYearBackdropInNarrowPane = false,
}: CalendarPickerProps) {
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
Expand Down Expand Up @@ -424,12 +428,14 @@ function CalendarPicker({
currentYear={currentYearView}
onYearChange={onYearSelected}
onClose={() => setIsYearPickerVisible(false)}
shouldEnableBackdropInNarrowPane={shouldEnableMonthYearBackdropInNarrowPane}
/>
<MonthPickerModal
isVisible={isMonthPickerVisible}
currentMonth={currentMonthView}
onMonthChange={onMonthSelected}
onClose={() => setIsMonthPickerVisible(false)}
shouldEnableBackdropInNarrowPane={shouldEnableMonthYearBackdropInNarrowPane}
/>
</View>
);
Expand Down
7 changes: 6 additions & 1 deletion src/components/DatePicker/DatePickerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const DEFAULT_ANCHOR_ORIGIN = {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
};

const popoverDimensions = {
height: CONST.POPOVER_DATE_MIN_HEIGHT,
width: CONST.POPOVER_DATE_WIDTH,
Expand All @@ -31,10 +32,12 @@ function DatePickerModal({
isVisible,
onClose,
anchorPosition,
anchorAlignment = DEFAULT_ANCHOR_ORIGIN,
onSelected,
shouldCloseWhenBrowserNavigationChanged = false,
shouldPositionFromTop = false,
forwardedFSClass,
shouldEnableMonthYearBackdropInNarrowPane = false,
}: DatePickerProps) {
const [selectedDate, setSelectedDate] = useState(value ?? defaultValue ?? undefined);
const anchorRef = useRef<View>(null);
Expand Down Expand Up @@ -69,7 +72,8 @@ function DatePickerModal({
popoverDimensions={popoverDimensions}
shouldCloseWhenBrowserNavigationChanged={shouldCloseWhenBrowserNavigationChanged}
innerContainerStyle={isSmallScreenWidth ? styles.w100 : {width: CONST.POPOVER_DATE_WIDTH}}
anchorAlignment={DEFAULT_ANCHOR_ORIGIN}
anchorAlignment={anchorAlignment}
restoreFocusType={CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE}
shouldSwitchPositionIfOverflow
shouldReturnFocus={false}
shouldMeasureAnchorPositionFromTop={shouldPositionFromTop}
Expand All @@ -82,6 +86,7 @@ function DatePickerModal({
maxDate={maxDate}
value={selectedDate}
onSelected={handleDateSelection}
shouldEnableMonthYearBackdropInNarrowPane={shouldEnableMonthYearBackdropInNarrowPane}
/>
</PopoverWithMeasuredContent>
);
Expand Down
12 changes: 12 additions & 0 deletions src/components/DatePicker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ type DatePickerBaseProps = ForwardedFSClassProps & {

/** ID of the wrapping form */
formID?: keyof OnyxFormValuesMapping;

/**
* Whether Month/Year right-docked picker modals should keep backdrop in narrow pane context.
* Used by inline editing flows that require background dimming.
*/
shouldEnableMonthYearBackdropInNarrowPane?: boolean;
};

type DatePickerModalProps = DatePickerBaseProps & {
Expand Down Expand Up @@ -103,6 +109,12 @@ type DatePickerProps = {

/** If the popover will be positioned from the top */
shouldPositionFromTop?: boolean;

/**
* Whether Month/Year right-docked picker modals should keep backdrop in narrow pane context.
* Used by inline editing flows that require background dimming.
*/
shouldEnableMonthYearBackdropInNarrowPane?: boolean;
} & Omit<BaseTextInputProps & PopoverWithMeasuredContentProps, 'anchorRef' | 'children'>;

export type {DatePickerBaseProps, DatePickerModalProps, DateInputWithPickerProps, DatePickerProps};
5 changes: 4 additions & 1 deletion src/components/Modal/BaseModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ function BaseModal({
forwardedFSClass = CONST.FULLSTORY.CLASS.UNMASK,
ref,
shouldDisplayBelowModals = false,
shouldKeepRightDockedBackdropInNarrowPane = false,
shouldWrapModalChildrenInScrollViewIfBottomDockedInLandscapeMode = true,
}: BaseModalProps) {
// When the `enableEdgeToEdgeBottomSafeAreaPadding` prop is explicitly set, we enable edge-to-edge mode.
Expand Down Expand Up @@ -305,8 +306,10 @@ function BaseModal({
const {originalValues} = useContext(ScreenWrapperOfflineIndicatorContext);
const offlineIndicatorContextValue = useMemo(() => (isInNarrowPane ? (originalValues ?? {}) : {}), [isInNarrowPane, originalValues]);

const shouldSuppressRightDockedBackdrop =
type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED && !isSmallScreenWidth && (isInNarrowPane || isInNarrowPaneModal) && !shouldKeepRightDockedBackdropInNarrowPane;
const backdropOpacityAdjusted =
hideBackdrop || (type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED && !isSmallScreenWidth && (isInNarrowPane || isInNarrowPaneModal)) // right_docked modals shouldn't add backdrops when opened in same-width RHP
hideBackdrop || shouldSuppressRightDockedBackdrop // right_docked modals shouldn't add backdrops when opened in same-width RHP
? 0
: backdropOpacity;

Expand Down
6 changes: 6 additions & 0 deletions src/components/Modal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ type BaseModalProps = Partial<ReanimatedModalProps> &
*/
shouldDisplayBelowModals?: boolean;

/**
* Whether a RIGHT_DOCKED modal should keep its backdrop when rendered in a narrow pane context.
* See https://github.com/Expensify/App/issues/88645 for more details.
*/
shouldKeepRightDockedBackdropInNarrowPane?: boolean;

/**
* Whether the modal should wrap the children in a scroll view if it is a bottom docked modal in landscape mode.
* Defaults to true.
Expand Down
21 changes: 20 additions & 1 deletion src/components/MoneyRequestAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ type MoneyRequestAmountInputProps = {
/** Whether to allow direct negative input (for split amounts where value is already negative) */
allowNegativeInput?: boolean;

/** Style for the negative symbol */
negativeSymbolStyle?: StyleProp<TextStyle>;

/** The testID of the input. Used to locate this view in end-to-end tests. */
testID?: string;

Expand All @@ -124,6 +127,15 @@ type MoneyRequestAmountInputProps = {
*/
shouldWrapInputInContainer?: boolean;

/** Style applied to the outer ScrollView inside NumberWithSymbolForm */
scrollViewStyle?: StyleProp<ViewStyle>;

/**
* Whether to refocus the input when clicking on the ScrollView empty space.
* Prevents focus loss when clicking empty space left of the right-aligned input.
*/
shouldRefocusOnScrollViewClick?: boolean;

/** Whether the input is disabled or not */
disabled?: boolean;

Expand All @@ -132,7 +144,7 @@ type MoneyRequestAmountInputProps = {

/** Determines which keyboard to open */
keyboardType?: KeyboardTypeOptions;
} & Pick<TextInputWithSymbolProps, 'autoGrowExtraSpace' | 'submitBehavior' | 'shouldUseDefaultLineHeightForPrefix' | 'onFocus' | 'onBlur'>;
} & Pick<TextInputWithSymbolProps, 'autoGrowExtraSpace' | 'submitBehavior' | 'shouldUseDefaultLineHeightForPrefix' | 'onFocus' | 'onBlur' | 'symbolTextStyle'>;

type Selection = {
start: number;
Expand Down Expand Up @@ -167,9 +179,12 @@ function MoneyRequestAmountInput({
shouldApplyPaddingToContainer = false,
shouldUseDefaultLineHeightForPrefix = true,
shouldWrapInputInContainer = true,
scrollViewStyle,
shouldRefocusOnScrollViewClick = false,
isNegative = false,
allowFlippingAmount = false,
allowNegativeInput = false,
negativeSymbolStyle,
toggleNegative,
clearNegative,
ref,
Expand Down Expand Up @@ -245,6 +260,7 @@ function MoneyRequestAmountInput({
currency={currency}
hideSymbol={hideCurrencySymbol}
isSymbolPressable={isCurrencyPressable}
symbolTextStyle={props.symbolTextStyle}
shouldShowBigNumberPad={shouldShowBigNumberPad}
style={inputStyle}
autoGrow={autoGrow}
Expand All @@ -254,12 +270,15 @@ function MoneyRequestAmountInput({
shouldApplyPaddingToContainer={shouldApplyPaddingToContainer}
shouldUseDefaultLineHeightForPrefix={shouldUseDefaultLineHeightForPrefix}
shouldWrapInputInContainer={shouldWrapInputInContainer}
scrollViewStyle={scrollViewStyle}
shouldRefocusOnScrollViewClick={shouldRefocusOnScrollViewClick}
containerStyle={props.containerStyle}
prefixStyle={props.prefixStyle}
prefixContainerStyle={props.prefixContainerStyle}
touchableInputWrapperStyle={props.touchableInputWrapperStyle}
contentWidth={contentWidth}
isNegative={isNegative}
negativeSymbolStyle={negativeSymbolStyle}
testID={testID}
errorText={props.errorText}
footer={props.footer}
Expand Down
Loading
Loading