Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
4d46712
feat: Add inline editing for tables on desktop
mohammadjafarinejad Feb 17, 2026
185f956
feat: Implement inline editing for transactions in search results
mohammadjafarinejad Feb 21, 2026
c49ac06
feat: Add Confirm/Cancel button row and enhance date and category pic…
mohammadjafarinejad Feb 23, 2026
7167844
Merge branch 'main' into fix/82534
mohammadjafarinejad Feb 23, 2026
ef55b1e
feat: Refactor editable cell styles and improve input handling across…
mohammadjafarinejad Feb 24, 2026
85281f2
Merge branch 'main' into fix/82534
mohammadjafarinejad Feb 24, 2026
2331d98
fix prettier
mohammadjafarinejad Feb 24, 2026
9ad8c73
feat: Enhance inline editing functionality and improve transaction ha…
mohammadjafarinejad Feb 24, 2026
f591d6f
fix: Simplify transaction ID retrieval and improve null handling in g…
mohammadjafarinejad Feb 24, 2026
25a33a2
feat: Introduce EditableProps for inline-editable cells and refactor …
mohammadjafarinejad Feb 25, 2026
3ee0dd7
Merge branch 'main' into fix/82534
mohammadjafarinejad Feb 25, 2026
2116cab
refactor: Simplify MerchantOrDescriptionCell rendering in Transaction…
mohammadjafarinejad Feb 25, 2026
90a46cc
feat: Add constants for CategoryPicker popover dimensions
mohammadjafarinejad Feb 25, 2026
37a378c
fix: address codex feedback
mohammadjafarinejad Feb 25, 2026
529f109
fix: center content in editable cell
mohammadjafarinejad Feb 26, 2026
6fb4b95
Merge branch 'main' into fix/82534
mohammadjafarinejad Feb 26, 2026
c4fd7e5
fix: incorrect text value
mohammadjafarinejad Feb 26, 2026
0e15ed7
fix: gap above confirm/cancel buttons
mohammadjafarinejad Feb 26, 2026
a39e221
fix: close editable cells when clicking outside on search table rows
mohammadjafarinejad Feb 26, 2026
ecea587
chore: fix lint
mohammadjafarinejad Feb 26, 2026
411b085
fix: improve editing state handling in TransactionListItem
mohammadjafarinejad Feb 26, 2026
6b5beac
fix: add default value for EditingCellContext
mohammadjafarinejad Feb 26, 2026
0523993
fix: remove isEditable prop from editable cells
mohammadjafarinejad Feb 27, 2026
9551cb5
fix: remove CategoryPickerPopover component
mohammadjafarinejad Feb 27, 2026
33d7d0c
fix: refactor editing cell by using module state
mohammadjafarinejad Feb 27, 2026
82bf64d
fix: update date range in DateCell
mohammadjafarinejad Feb 27, 2026
47acf9c
refactor: remove view wrapper
mohammadjafarinejad Feb 27, 2026
e3b50b6
fix: update editableCellHover borderColor
mohammadjafarinejad Feb 27, 2026
94c93a9
Refactor: Simplify showConfirmButtons logic
mohammadjafarinejad Feb 27, 2026
0077b92
Merge branch 'main' into fix/82534
mohammadjafarinejad Feb 27, 2026
8b38c5f
refactor: centralize transaction inline editing via useTransactionInl…
mohammadjafarinejad Mar 3, 2026
50df95a
Merge branch 'main' into fix/82534
mohammadjafarinejad Mar 3, 2026
38979e6
refactor: replace editing cell count with boolean
mohammadjafarinejad Mar 3, 2026
f8ba4c4
Merge branch 'Expensify:main' into fix/82534
mohammadjafarinejad Mar 3, 2026
9770aa7
chore: fix lint error
mohammadjafarinejad Mar 3, 2026
1e230ed
Merge branch 'Expensify:main' into fix/82534
mohammadjafarinejad Mar 4, 2026
93048ca
Merge branch 'Expensify:main' into fix/82534
mohammadjafarinejad Mar 11, 2026
f6c2509
fix: Prevent onPress event during cell editing in TransactionListItem
mohammadjafarinejad Mar 12, 2026
398c57f
fix: multi-line wrapping
mohammadjafarinejad Mar 12, 2026
362f407
feat: add support for editting tags
mohammadjafarinejad Mar 12, 2026
8deaee8
fix: enable editing for TAG column in search headers
mohammadjafarinejad Mar 12, 2026
6ecc91e
refactor: consolidate picker modal props
mohammadjafarinejad Mar 12, 2026
e9432dd
refactor: move TagPicker to folder index
mohammadjafarinejad Mar 12, 2026
971ebee
feat: implement useEffectivePolicyID hook and update transaction edit…
mohammadjafarinejad Mar 12, 2026
c82b4af
Merge branch 'Expensify:main' into fix/82534
mohammadjafarinejad Mar 12, 2026
a9c846c
feat: auto-cancel editing when canEdit permission is revoked
mohammadjafarinejad Mar 12, 2026
dcffdcb
feat: update useInlineEditState hook to include initial editing state…
mohammadjafarinejad Mar 12, 2026
3a94a63
Merge branch 'main' into fix/82534
mohammadjafarinejad Mar 14, 2026
04d542e
fix: sanitize line breaks for single-line inputs in MerchantOrDescrip…
mohammadjafarinejad Mar 14, 2026
738adfb
fix: prevent early return in onPress handler when editing cell
mohammadjafarinejad Mar 14, 2026
3910588
fix: allow event propagation during cell editing in selectFocusedOption
mohammadjafarinejad Mar 14, 2026
3c3c9c4
fix: remove checks for settled and approved reports in transaction ed…
mohammadjafarinejad Mar 14, 2026
b256737
fix: prevent line breaks in merchant field
mohammadjafarinejad Mar 14, 2026
a2a7ded
fix: pass explicit params to getTransactionEditPermissions instead of…
mohammadjafarinejad Mar 14, 2026
871e9b7
refactor: remove useMemo
mohammadjafarinejad Mar 14, 2026
d657c2f
fix: update transaction edit permissions to use OnyxEntry
mohammadjafarinejad Mar 14, 2026
9911ddc
fix: update transaction permissions logic to align with MoneyRequestView
mohammadjafarinejad Mar 14, 2026
097acca
Merge branch 'Expensify:main' into fix/82534
mohammadjafarinejad Mar 14, 2026
f93071a
fix: reset submit flag on entering edit mode
mohammadjafarinejad Mar 14, 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
6 changes: 6 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ const CONST = {
POPOVER_DROPDOWN_WIDTH: 334,
POPOVER_DROPDOWN_MIN_HEIGHT: 0,
POPOVER_DROPDOWN_MAX_HEIGHT: 416,
POPOVER_CATEGORY_PICKER_WIDTH: 350,
POPOVER_CATEGORY_PICKER_MAX_HEIGHT: 450,
POPOVER_MENU_MAX_HEIGHT: 496,
POPOVER_MENU_MAX_HEIGHT_MOBILE: 432,
POPOVER_DATE_WIDTH: 338,
Expand Down Expand Up @@ -7501,6 +7503,7 @@ const CONST = {
COMPLETED: 'completed',
},
},
INLINE_EDITABLE_EXPENSE_STATUSES: new Set<string>(['', 'unreported', 'drafts', 'outstanding']),
GROUP_COLUMN_PREFIX: 'group',
TABLE_COLUMNS: {
RECEIPT: 'receipt',
Expand Down Expand Up @@ -8569,6 +8572,9 @@ const CONST = {
GROUP_SELECT_ALL_CHECKBOX: 'Search-GroupSelectAllCheckbox',
SORTABLE_HEADER: 'Search-SortableHeader',
},
TABLE: {
EDITABLE_CELL: 'Table-EditableCell',
},
REPORT: {
FLOATING_MESSAGE_COUNTER: 'Report-FloatingMessageCounter',
LIST_BOUNDARY_LOADER_RETRY: 'Report-ListBoundaryLoaderRetry',
Expand Down
104 changes: 104 additions & 0 deletions src/components/CategoryPicker/CategoryPickerModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, {useRef, useState} from 'react';
import {View} from 'react-native';
import ConfirmCancelButtonRow from '@components/ConfirmCancelButtonRow';
import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent';
import type PopoverWithMeasuredContentProps from '@components/PopoverWithMeasuredContent/types';
import type {ListItem} from '@components/SelectionList/types';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import CategoryPicker from '.';

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

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

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();

// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to distinguish RHL and narrow layout
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const anchorRef = useRef<View>(null);

const [pendingItem, setPendingItem] = useState<ListItem | undefined>(undefined);

const handleConfirm = () => {
if (pendingItem) {
onSelected?.(pendingItem);
}
setPendingItem(undefined);
onClose();
};

const handleCancel = () => {
setPendingItem(undefined);
onClose();
};

return (
<PopoverWithMeasuredContent
anchorRef={anchorRef}
isVisible={isVisible}
onClose={handleCancel}
anchorPosition={anchorPosition}
popoverDimensions={popoverDimensions}
anchorAlignment={anchorAlignment}
innerContainerStyle={isSmallScreenWidth ? styles.w100 : {width: CONST.POPOVER_DROPDOWN_WIDTH}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think we need to check isSmallScreenWidth here since we only support large screens in this case?

Copy link
Contributor Author

@mohammadjafarinejad mohammadjafarinejad Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, but keeping it makes sense in case CategoryPickerModal gets reused on small screens later. Do you think we should keep it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh! Good point 👍

I think we should make this clearer. Since this file is for web and large screens only, and hasn’t been tested on native or small screens, we should create a web-specific file and have the .native.tsx version return null with a comment explaining why.

restoreFocusType={CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE}
shouldSwitchPositionIfOverflow
shouldEnableNewFocusManagement
shouldMeasureAnchorPositionFromTop={shouldMeasureAnchorPositionFromTop}
shouldSkipRemeasurement
shouldDisplayBelowModals
>
<View style={[StyleUtils.getHeight(CONST.POPOVER_DROPDOWN_MAX_HEIGHT), styles.flexColumn, styles.pt4]}>
<View style={styles.flex1}>
<CategoryPicker
policyID={policyID}
selectedCategory={pendingItem?.keyForList ?? selectedCategory}
onSubmit={setPendingItem}
/>
</View>
<ConfirmCancelButtonRow
onConfirm={handleConfirm}
onCancel={handleCancel}
isConfirmDisabled={!pendingItem}
/>
</View>
</PopoverWithMeasuredContent>
);
}

export default CategoryPickerModal;
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import React from 'react';
import RadioListItem from '@components/SelectionList/ListItem/RadioListItem';
import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections';
import type {ListItem} from '@components/SelectionList/types';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
Expand All @@ -11,9 +14,6 @@ import {getHeaderMessageForNonUserList} from '@libs/OptionsListUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import RadioListItem from './SelectionList/ListItem/RadioListItem';
import SelectionListWithSections from './SelectionList/SelectionListWithSections';
import type {ListItem} from './SelectionList/types';

type CategoryPickerProps = {
policyID: string | undefined;
Expand Down
40 changes: 40 additions & 0 deletions src/components/ConfirmCancelButtonRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Button from './Button';

type ConfirmCancelButtonRowProps = {
/** Called when the user presses Apply */
onConfirm: () => void;

/** Called when the user presses Cancel */
onCancel: () => void;

/** Whether the Apply button is disabled */
isConfirmDisabled?: boolean;
};

function ConfirmCancelButtonRow({onConfirm, onCancel, isConfirmDisabled = false}: ConfirmCancelButtonRowProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

return (
<View style={[styles.flexRow, styles.gap2, styles.ph4, styles.pb4]}>
<Button
style={[styles.flex1]}
text={translate('common.cancel')}
onPress={onCancel}
/>
<Button
style={[styles.flex1]}
success
text={translate('common.apply')}
onPress={onConfirm}
isDisabled={isConfirmDisabled}
/>
</View>
);
}

export default ConfirmCancelButtonRow;
40 changes: 35 additions & 5 deletions src/components/DatePicker/DatePickerModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {setYear} from 'date-fns';
import React, {useEffect, useRef, useState} from 'react';
import type {View} from 'react-native';
import ConfirmCancelButtonRow from '@components/ConfirmCancelButtonRow';
import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -31,10 +32,12 @@ function DatePickerModal({
isVisible,
onClose,
anchorPosition,
anchorAlignment = DEFAULT_ANCHOR_ORIGIN,
onSelected,
shouldCloseWhenBrowserNavigationChanged = false,
shouldPositionFromTop = false,
forwardedFSClass,
shouldShowConfirmButtons = false,
}: DatePickerProps) {
const [selectedDate, setSelectedDate] = useState(value ?? defaultValue ?? undefined);
const anchorRef = useRef<View>(null);
Expand All @@ -45,19 +48,39 @@ function DatePickerModal({
const {isSmallScreenWidth} = useResponsiveLayout();

useEffect(() => {
// In confirm mode, selectedDate is a pending selection and should not be synced with value until the user confirms
if (shouldShowConfirmButtons) {
return;
}
if (shouldSaveDraft && formID) {
setDraftValues(formID, {[inputID]: selectedDate});
}
if (selectedDate !== value) {
setSelectedDate(value);
}
}, [formID, inputID, selectedDate, shouldSaveDraft, value]);
}, [formID, inputID, selectedDate, shouldSaveDraft, shouldShowConfirmButtons, value]);

const handleDateSelection = (newValue: string) => {
onSelected?.(newValue);
const commitDate = (date: string) => {
onSelected?.(date);
onTouched?.();
onInputChange?.(newValue);
onInputChange?.(date);
if (shouldSaveDraft && formID) {
setDraftValues(formID, {[inputID]: date});
}
};

const handleDateSelection = (newValue: string) => {
setSelectedDate(newValue);
if (!shouldShowConfirmButtons) {
commitDate(newValue);
}
};

const handleConfirm = () => {
if (selectedDate) {
commitDate(selectedDate);
}
onClose();
};

return (
Expand All @@ -69,7 +92,7 @@ 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
shouldEnableNewFocusManagement
Expand All @@ -84,6 +107,13 @@ function DatePickerModal({
value={selectedDate}
onSelected={handleDateSelection}
/>
{shouldShowConfirmButtons && (
<ConfirmCancelButtonRow
onConfirm={handleConfirm}
onCancel={onClose}
isConfirmDisabled={!selectedDate}
/>
)}
</PopoverWithMeasuredContent>
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/components/DatePicker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ type DatePickerProps = {

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

/** When true, shows Cancel/Confirm buttons instead of immediately firing onSelected on date pick */
shouldShowConfirmButtons?: boolean;
} & Omit<BaseTextInputProps & PopoverWithMeasuredContentProps, 'anchorRef' | 'children'>;

export type {DatePickerBaseProps, DatePickerModalProps, DateInputWithPickerProps, DatePickerProps};
8 changes: 7 additions & 1 deletion src/components/MoneyRequestAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,15 @@ type MoneyRequestAmountInputProps = {
*/
shouldWrapInputInContainer?: boolean;

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

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

/** Reference to the outer element */
ref?: ForwardedRef<BaseTextInputRef>;
} & Pick<TextInputWithSymbolProps, 'autoGrowExtraSpace' | 'submitBehavior' | 'shouldUseDefaultLineHeightForPrefix' | 'onFocus' | 'onBlur'>;
} & Pick<TextInputWithSymbolProps, 'autoGrowExtraSpace' | 'submitBehavior' | 'shouldUseDefaultLineHeightForPrefix' | 'onFocus' | 'onBlur' | 'symbolTextStyle'>;

type Selection = {
start: number;
Expand Down Expand Up @@ -166,6 +169,7 @@ function MoneyRequestAmountInput({
shouldApplyPaddingToContainer = false,
shouldUseDefaultLineHeightForPrefix = true,
shouldWrapInputInContainer = true,
scrollViewStyle,
isNegative = false,
allowFlippingAmount = false,
allowNegativeInput = false,
Expand Down Expand Up @@ -244,6 +248,7 @@ function MoneyRequestAmountInput({
currency={currency}
hideSymbol={hideCurrencySymbol}
isSymbolPressable={isCurrencyPressable}
symbolTextStyle={props.symbolTextStyle}
shouldShowBigNumberPad={shouldShowBigNumberPad}
style={inputStyle}
autoGrow={autoGrow}
Expand All @@ -253,6 +258,7 @@ function MoneyRequestAmountInput({
shouldApplyPaddingToContainer={shouldApplyPaddingToContainer}
shouldUseDefaultLineHeightForPrefix={shouldUseDefaultLineHeightForPrefix}
shouldWrapInputInContainer={shouldWrapInputInContainer}
scrollViewStyle={scrollViewStyle}
containerStyle={props.containerStyle}
prefixStyle={props.prefixStyle}
prefixContainerStyle={props.prefixContainerStyle}
Expand Down
Loading
Loading