Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9504bc0
Show configured report columns
brunovjk Sep 28, 2026
7eca905
add tests
brunovjk Sep 28, 2026
8c7682f
Resolve merge conflicts
brunovjk Oct 2, 2026
3ba3919
Show billable and reimbursable values as read-only when controls are …
brunovjk Oct 5, 2026
abb4be3
fix: improve JSDoc comment formatting per CONSISTENCY-15
brunovjk Oct 5, 2026
bc39752
Merge branch 'main' into issue-101300-customize-fields
brunovjk Oct 5, 2026
cf1bd13
Refactor money request fields into focused components and simplify fi…
brunovjk Oct 5, 2026
9143f7f
Merge branch 'Expensify:main' into issue-101300-customize-fields
brunovjk Oct 5, 2026
51ce651
Merge branch 'Expensify:main' into issue-101300-customize-fields
brunovjk Oct 5, 2026
510fed7
ReturnType
brunovjk Oct 5, 2026
496043c
Simplify money request field components with composition and action-b…
brunovjk Oct 6, 2026
67b4364
Fix attendee field types and missing cardList arguments in report act…
brunovjk Oct 6, 2026
176ba3a
TypeScript, React Compiler verification
brunovjk Oct 6, 2026
66b9feb
Total per attendee
brunovjk Oct 6, 2026
89d7086
fix lint
brunovjk Oct 6, 2026
8ea4cf3
Merge branch 'Expensify:main' into issue-101300-customize-fields
brunovjk Oct 6, 2026
4c5b547
Merge branch 'Expensify:main' into issue-101300-customize-fields
brunovjk Oct 7, 2026
a54498f
Merge main and resolve expense field conflicts
Oct 15, 2026
3470ce6
Fix column reordering on native platforms
brunovjk Oct 15, 2026
963c6cb
Merge branch 'main' into issue-101300-customize-fields
brunovjk Oct 16, 2026
1d04e89
Fix menu formatting
brunovjk Oct 16, 2026
625e69f
Fix receipt visibility test selector
brunovjk Oct 16, 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/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1711,6 +1711,7 @@ const CONST = {
PRINT: 'print',
CHANGE_WORKSPACE: 'changeWorkspace',
CHANGE_APPROVER: 'changeApprover',
CUSTOMIZE_FIELDS: 'customizeFields',
VIEW_DETAILS: 'viewDetails',
DELETE: 'delete',
RETRACT: 'retract',
Expand Down Expand Up @@ -1771,6 +1772,7 @@ const CONST = {
HOLD: 'hold',
REMOVE_HOLD: 'removeHold',
SPLIT: 'split',
CUSTOMIZE_FIELDS: 'customizeFields',
VIEW_DETAILS: 'viewDetails',
DELETE: 'delete',
REJECT: 'reject',
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2875,6 +2875,10 @@ const ROUTES = {
route: 'r/:reportID/change-approver/reassign',
getRoute: (reportID: string) => `r/${reportID}/change-approver/reassign` as const,
},
REPORT_SETTINGS_FIELDS: {
route: 'r/:reportID/settings/fields',
getRoute: (reportID: string) => `r/${reportID}/settings/fields` as const,
},
REPORT_SETTINGS_COLUMNS: {
route: 'r/:reportID/settings/columns',
getRoute: (reportID: string) => `r/${reportID}/settings/columns` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ const SCREENS = {
VISIBILITY: 'Report_Settings_Visibility',
DYNAMIC_SETTINGS_VISIBILITY: 'Dynamic_Report_Settings_Visibility',
COLUMNS: 'Report_Settings_Columns',
FIELDS: 'Report_Settings_Fields',
},

NEW_TASK: {
Expand Down
89 changes: 36 additions & 53 deletions src/components/ColumnsSettingsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,33 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {getSearchColumnTranslationKey} from '@libs/SearchUIUtils';

import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';

import type {ComponentRef} from 'react';

import React, {useRef, useState} from 'react';
import {View} from 'react-native';

import type {ColumnItem} from './ColumnsSettingsListItem';
import type {DraggableListRenderItemParams} from './DraggableList/types';
import type {SearchCustomColumnIds} from './Search/types';
import type {ListItem} from './SelectionList/types';

import Button from './Button';
import ColumnsSettingsListItem from './ColumnsSettingsListItem';
import DraggableList from './DraggableList';
import HeaderWithBackButton from './HeaderWithBackButton';
import Icon from './Icon';
import ScreenWrapper from './ScreenWrapper';
import ScrollView from './ScrollView';
import MultiSelectListItem from './SelectionList/ListItem/MultiSelectListItem';
import Text from './Text';
import TextLink from './TextLink';

type ColumnItem = {
/** Display label for the column */
text: string;

value: SearchCustomColumnIds;
keyForList: SearchCustomColumnIds;

/** Whether the column is currently enabled/visible */
isSelected: boolean;

/** Whether the column toggle is disabled (e.g. for required columns) */
isDisabled: boolean;

/** Whether the column cannot be reordered via drag */
isDragDisabled: boolean;

/** Element rendered on the left side of the list item (e.g. drag handle) */
leftElement: React.JSX.Element;
};

type ColumnsSettingsListProps = {
/** Heading for the surface sharing the column preferences */
titleKey?: TranslationPaths;

allColumns: SearchCustomColumnIds[];

/** The default set of selected columns when no customization has been applied */
Expand Down Expand Up @@ -76,6 +62,7 @@ type ColumnsSettingsListProps = {
};

function ColumnsSettingsList({
titleKey = 'search.columns',
allColumns,
defaultSelectedColumns,
currentColumns,
Expand Down Expand Up @@ -231,17 +218,15 @@ function ColumnsSettingsList({
onSave(selectedColumnIds);
};

const renderItem = ({item, isFocused}: {item: ListItem; isFocused?: boolean}) => {
return (
<MultiSelectListItem
item={item}
isFocused={isFocused}
showTooltip={false}
onSelectRow={onSelectItem}
isDisabled={item.isDisabled}
/>
);
};
const renderItem = ({item, isFocused, drag, isActive}: DraggableListRenderItemParams<ColumnItem>) => (
<ColumnsSettingsListItem
item={item}
isFocused={isFocused}
drag={drag}
isActive={isActive}
onSelectRow={onSelectItem}
/>
);

return (
<ScreenWrapper
Expand All @@ -250,15 +235,13 @@ function ColumnsSettingsList({
offlineIndicatorStyle={styles.mtAuto}
includeSafeAreaPaddingBottom
>
<HeaderWithBackButton title={translate('search.columns')}>
{!isDefaultState && <TextLink onPress={resetColumns}>{translate('search.resetColumns')}</TextLink>}
</HeaderWithBackButton>
<HeaderWithBackButton title={translate(titleKey)}>{!isDefaultState && <TextLink onPress={resetColumns}>{translate('search.resetColumns')}</TextLink>}</HeaderWithBackButton>
<View style={styles.flex1}>
<ScrollView
style={styles.flex1}
contentContainerStyle={styles.flex1}
>
{isGrouped ? (
{isGrouped ? (
<ScrollView
style={styles.flex1}
contentContainerStyle={styles.flex1}
>
<View ref={containerRef}>
<View style={[styles.ph5, styles.pb3]}>
<Text style={styles.textLabelSupporting}>{translate('search.groupColumns')}</Text>
Expand Down Expand Up @@ -294,19 +277,19 @@ function ColumnsSettingsList({
focusedIndex={typeFocusedIndex}
/>
</View>
) : (
<DraggableList
disableScroll
data={typeColumnsList}
keyExtractor={(item) => item.value}
onDragEnd={onTypeDragEnd}
onSelectRow={onSelectItem}
isItemDragDisabled={(item) => item.isDragDisabled}
isItemDisabled={(item) => item.isDisabled}
renderItem={renderItem}
/>
)}
</ScrollView>
</ScrollView>
) : (
<DraggableList
data={typeColumnsList}
keyExtractor={(item) => item.value}
onDragEnd={onTypeDragEnd}
onSelectRow={onSelectItem}
shouldRestrictDragToEnabledItems
isItemDragDisabled={(item) => item.isDragDisabled}
isItemDisabled={(item) => item.isDisabled}
renderItem={renderItem}
/>
)}
</View>
<View style={[styles.ph5, styles.pb5]}>
<Button
Expand Down
56 changes: 56 additions & 0 deletions src/components/ColumnsSettingsListItem.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import useThemeStyles from '@hooks/useThemeStyles';

import CONST from '@src/CONST';

import React from 'react';
import {View} from 'react-native';

import type {ColumnsSettingsListItemProps} from './ColumnsSettingsListItem';

import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import MultiSelectListItem from './SelectionList/ListItem/MultiSelectListItem';

function ColumnsSettingsListItem({item, isFocused, drag, isActive, onSelectRow}: ColumnsSettingsListItemProps) {
const styles = useThemeStyles();
const startDrag = () => {
if (item.isDragDisabled || isActive) {
return;
}
drag();
};
const rowItem = {
...item,
leftElement: (
<PressableWithoutFeedback
accessible={false}
testID={`column-drag-handle-${item.value}`}
disabled={item.isDragDisabled || isActive}
onPressIn={(event) => {
// Dragging must not toggle the checkbox.
event.stopPropagation();
startDrag();
}}
onPress={(event) => event?.stopPropagation()}
sentryLabel={CONST.SENTRY_LABEL.SELECTION_LIST.BASE_LIST_ITEM}
>
{item.leftElement}
</PressableWithoutFeedback>
),
};

return (
// Press feedback dims the row, so its backing must hide other rows during a drag.
<View style={styles.appBG}>
<MultiSelectListItem
item={rowItem}
isFocused={isActive ? true : isFocused}
showTooltip={false}
onSelectRow={onSelectRow}
onLongPressRow={startDrag}
isDisabled={item.isDisabled}
/>
</View>
);
}

export default ColumnsSettingsListItem;
43 changes: 43 additions & 0 deletions src/components/ColumnsSettingsListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Selection rows shared by the expense and report column pickers.
import React from 'react';

import type {DraggableListRenderItemParams} from './DraggableList/types';
import type {SearchCustomColumnIds} from './Search/types';
import type {ListItemProps} from './SelectionList/ListItem/types';

import MultiSelectListItem from './SelectionList/ListItem/MultiSelectListItem';

type ColumnItem = {
text: string;

value: SearchCustomColumnIds;
keyForList: SearchCustomColumnIds;

isSelected: boolean;

/** Required fields cannot be deselected, but can still be reordered. */
isDisabled: boolean;

isDragDisabled: boolean;

leftElement: React.JSX.Element;
};

type ColumnsSettingsListItemProps = Pick<DraggableListRenderItemParams<ColumnItem>, 'item' | 'isFocused' | 'drag' | 'isActive'> & {
onSelectRow: ListItemProps<ColumnItem>['onSelectRow'];
};

function ColumnsSettingsListItem({item, isFocused, onSelectRow}: ColumnsSettingsListItemProps) {
return (
<MultiSelectListItem
item={item}
isFocused={isFocused}
showTooltip={false}
onSelectRow={onSelectRow}
isDisabled={item.isDisabled}
/>
);
}

export default ColumnsSettingsListItem;
export type {ColumnItem, ColumnsSettingsListItemProps};
37 changes: 36 additions & 1 deletion src/components/DraggableList/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,54 @@ import useThemeStyles from '@hooks/useThemeStyles';
import type {FlatList} from 'react-native-gesture-handler';

import React from 'react';
import {View} from 'react-native';
import DraggableFlatList from 'react-native-draggable-flatlist';

import type DraggableListProps from './types';

function DraggableList<T>({ref, onSelectRow, focusedIndex, isItemDragDisabled, isItemDisabled, ...viewProps}: DraggableListProps<T> & {ref?: React.ForwardedRef<FlatList<T>>}) {
function DraggableList<T>({
ref,
onSelectRow,
focusedIndex,
isItemDragDisabled,
isItemDisabled,
shouldRestrictDragToEnabledItems = false,
data,
onDragEnd,
ListFooterComponent,
...viewProps
}: DraggableListProps<T> & {ref?: React.ForwardedRef<FlatList<T>>}) {
const styles = useThemeStyles();
const draggableItems = shouldRestrictDragToEnabledItems ? data.filter((item) => !isItemDragDisabled?.(item)) : data;
const fixedItems = shouldRestrictDragToEnabledItems ? data.filter((item) => isItemDragDisabled?.(item)) : [];

// Fixed rows stay visible without becoming drop targets. The dragged row may hover over
// them, then snap back to the last valid position, matching the web column picker's behavior.
const footer =
fixedItems.length > 0 ? (
<View>
{fixedItems.map((item, index) => (
<React.Fragment key={viewProps.keyExtractor(item, draggableItems.length + index)}>
{viewProps.renderItem({item, getIndex: () => draggableItems.length + index, drag: () => {}, isActive: false})}
</React.Fragment>
))}
{ListFooterComponent}
</View>
) : (
ListFooterComponent
);

return (
<DraggableFlatList
ref={ref}
containerStyle={styles.flex1}
contentContainerStyle={styles.flexGrow1}
ListFooterComponentStyle={styles.flex1}
{...viewProps}
data={draggableItems}
ListFooterComponent={footer}
dragItemOverflow={shouldRestrictDragToEnabledItems}
onDragEnd={onDragEnd ? ({data: reorderedItems}) => onDragEnd({data: [...reorderedItems, ...fixedItems]}) : undefined}
/>
);
}
Expand Down
5 changes: 4 additions & 1 deletion src/components/DraggableList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type DraggableListProps<T> = {

isItemDragDisabled?: (item: T) => boolean;

/** Keep drag-disabled rows after the drag area, so a native drag cannot finish between them. */
shouldRestrictDragToEnabledItems?: boolean;

/** Whether the given item should be skipped during keyboard navigation */
isItemDisabled?: (item: T) => boolean;

Expand All @@ -50,4 +53,4 @@ type SortableItemProps = {
};

export default DraggableListProps;
export type {SortableItemProps};
export type {SortableItemProps, DraggableListRenderItemParams};
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {search} from '@libs/actions/Search';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import getPlatform from '@libs/getPlatform';
import {getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils';
import Navigation from '@libs/Navigation/Navigation';
import TransitionTracker from '@libs/Navigation/TransitionTracker';
import {isTrackOnboardingChoice} from '@libs/OnboardingUtils';
import type {KYCFlowEvent, TriggerKYCFlow, WorkspacePolicyPaymentOption} from '@libs/PaymentUtils';
Expand All @@ -71,6 +72,7 @@ import {payInvoice, payMoneyRequest} from '@userActions/IOU/PayMoneyRequest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import {loginSelector} from '@src/selectors/PersonalDetails';
import {createMoveExpenseReportNVPSelector} from '@src/selectors/Report';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
Expand Down Expand Up @@ -316,7 +318,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo
? sortPoliciesByName(activeAdminPolicies, localeCompare)
: [];

const expensifyIcons = useMemoizedLazyExpensifyIcons(['Info', 'Cash', 'ArrowRight', 'Building']);
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Info', 'Cash', 'ArrowRight', 'Building', 'Columns']);

// Build PAY action sub-items. Workspace-policy entries carry the policy as data and have no onSelected;
// MoneyReportHeaderKYCDropdown picks them up via onSubItemSelected where triggerKYCFlow is in scope.
Expand Down Expand Up @@ -421,6 +423,17 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo

// Merge all action implementations
const secondaryActionsImplementation: Record<string, (typeof lifecycleActionEntries)[string]> = {
[CONST.REPORT.SECONDARY_ACTIONS.CUSTOMIZE_FIELDS]: {
value: CONST.REPORT.SECONDARY_ACTIONS.CUSTOMIZE_FIELDS,
text: translate('search.customizeFields'),
icon: expensifyIcons.Columns,
onSelected: () => {
if (!reportID) {
return;
}
Navigation.navigate(ROUTES.REPORT_SETTINGS_FIELDS.getRoute(reportID));
},
},
[CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS]: {
value: CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS,
text: translate('iou.viewDetails'),
Expand Down
Loading
Loading