Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 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
399bdbb
Merge main into issue-101300-customize-fields
brunovjk Oct 23, 2026
64f7947
Fix expense field visibility and preserve report column preferences
brunovjk Oct 23, 2026
88682fe
Add screen reader actions for native field reordering
brunovjk Oct 23, 2026
2e1994f
Clarify field props and improve review test readability
brunovjk Oct 23, 2026
cd58ab3
Fix configured expense fields and preserve shared column choices
brunovjk Oct 24, 2026
cf00492
Keep reservation hints below the category row
brunovjk Oct 24, 2026
78b77f1
Merge branch 'Expensify:main' into issue-101300-customize-fields
brunovjk Oct 24, 2026
34bf099
Preserve report column aliases when customizing expense fields
brunovjk Oct 24, 2026
b602397
Merge main to resolve conflicts
brunovjk Oct 27, 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 @@ -1969,6 +1969,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 @@ -2029,6 +2030,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 @@ -2878,6 +2878,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 @@ -475,6 +475,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
128 changes: 77 additions & 51 deletions src/components/ColumnsSettingsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,34 @@ 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 = {
/** The translation key for the title. Defaults to the search columns heading. */
titleKey?: TranslationPaths;

/** All columns available for selection, excluding the separate group columns. */
allColumns: SearchCustomColumnIds[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
allColumns: SearchCustomColumnIds[];
/** All columns available for selection */
allColumns: SearchCustomColumnIds[];


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

function ColumnsSettingsList({
titleKey = 'search.columns',
allColumns,
defaultSelectedColumns,
currentColumns,
Expand Down Expand Up @@ -120,7 +108,10 @@ function ColumnsSettingsList({
return defaultColumns;
}

const selected = selectedColumnIds.map((columnId) => ({columnId, isSelected: true}));
const selected = selectedColumnIds.map((columnId) => ({
columnId,
isSelected: true,
}));
const unselected = allCustomColumns.filter((columnId) => !selectedColumnIds.includes(columnId)).map((columnId) => ({columnId, isSelected: false}));

return [...selected, ...unselected];
Expand Down Expand Up @@ -208,16 +199,28 @@ function ColumnsSettingsList({
const typeFocusedIndex = focusedIndex >= groupLength ? focusedIndex - groupLength : -1;

const onGroupDragEnd = ({data}: {data: typeof allColumnsList}) => {
const newGroupColumns = data.map((item) => ({columnId: item.value, isSelected: item.isSelected}));
const existingTypeColumns = typeColumnsList.map((item) => ({columnId: item.value, isSelected: item.isSelected}));
const newGroupColumns = data.map((item) => ({
columnId: item.value,
isSelected: item.isSelected,
}));
const existingTypeColumns = typeColumnsList.map((item) => ({
columnId: item.value,
isSelected: item.isSelected,
}));
const newColumns = [...existingTypeColumns, ...newGroupColumns];
setColumns(newColumns);
setFocusedIndex(-1);
};

const onTypeDragEnd = ({data}: {data: typeof allColumnsList}) => {
const newTypeColumns = data.map((item) => ({columnId: item.value, isSelected: item.isSelected}));
const existingGroupColumns = groupColumnsList.map((item) => ({columnId: item.value, isSelected: item.isSelected}));
const newTypeColumns = data.map((item) => ({
columnId: item.value,
isSelected: item.isSelected,
}));
const existingGroupColumns = groupColumnsList.map((item) => ({
columnId: item.value,
isSelected: item.isSelected,
}));
const newColumns = [...existingGroupColumns, ...newTypeColumns];
setColumns(newColumns);
setFocusedIndex(-1);
Expand All @@ -231,14 +234,39 @@ function ColumnsSettingsList({
onSave(selectedColumnIds);
};

const renderItem = ({item, isFocused}: {item: ListItem; isFocused?: boolean}) => {
const moveColumn = (item: ColumnItem, offset: number) => {
const isGroupColumn = groupColumns.includes(item.value);
const columnList = isGroupColumn ? groupColumnsList : typeColumnsList;
const index = columnList.findIndex((column) => column.value === item.value);
const targetIndex = index + offset;
const targetItem = columnList.at(targetIndex);
if (index < 0 || targetIndex < 0 || item.isDragDisabled || !targetItem || targetItem.isDragDisabled) {
return;
}

const reorderedColumns = [...columnList];
reorderedColumns.splice(index, 1);
reorderedColumns.splice(targetIndex, 0, item);
const onDragEnd = isGroupColumn ? onGroupDragEnd : onTypeDragEnd;
onDragEnd({data: reorderedColumns});
};

const renderItem = ({item, isFocused, drag, isActive}: DraggableListRenderItemParams<ColumnItem>) => {
const columnList = groupColumns.includes(item.value) ? groupColumnsList : typeColumnsList;
const index = columnList.findIndex((column) => column.value === item.value);
const canMoveUp = !item.isDragDisabled && index > 0 && !columnList.at(index - 1)?.isDragDisabled;
const nextColumn = columnList.at(index + 1);
const canMoveDown = !item.isDragDisabled && !!nextColumn && !nextColumn.isDragDisabled;

return (
<MultiSelectListItem
<ColumnsSettingsListItem
item={item}
isFocused={isFocused}
showTooltip={false}
drag={drag}
isActive={isActive}
onSelectRow={onSelectItem}
isDisabled={item.isDisabled}
onMoveUp={canMoveUp ? () => moveColumn(item, -1) : undefined}
onMoveDown={canMoveDown ? () => moveColumn(item, 1) : undefined}
/>
);
};
Expand All @@ -250,15 +278,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 +320,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
70 changes: 70 additions & 0 deletions src/components/ColumnsSettingsListItem.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import useLocalize from '@hooks/useLocalize';
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, onMoveUp, onMoveDown}: ColumnsSettingsListItemProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const accessibilityActions = [
...(onMoveUp ? [{name: 'moveUp', label: translate('common.moveUp')}] : []),
...(onMoveDown ? [{name: 'moveDown', label: translate('common.moveDown')}] : []),
];
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}
accessibilityActions={accessibilityActions}
onAccessibilityAction={({nativeEvent}) => {
if (nativeEvent.actionName === 'moveUp') {
onMoveUp?.();
} else if (nativeEvent.actionName === 'moveDown') {
onMoveDown?.();
}
}}
/>
</View>
);
}

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

import type {ColumnItem, ColumnsSettingsListItemProps} from './ColumnsSettingsListItem/types';

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

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};
41 changes: 41 additions & 0 deletions src/components/ColumnsSettingsListItem/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type {DraggableListRenderItemParams} from '@components/DraggableList/types';
import type {SearchCustomColumnIds} from '@components/Search/types';
import type {ListItemProps} from '@components/SelectionList/ListItem/types';

import type React from 'react';

type ColumnItem = {
/** Localized column label, also used as the screen reader name. */
text: string;

/** Column ID passed back when a row is reordered. */
value: SearchCustomColumnIds;

/** Stable row key matching the column ID. */
keyForList: SearchCustomColumnIds;

/** Selected columns appear before the alphabetically ordered unselected columns. */
isSelected: boolean;

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

/** Unselected columns remain visible without becoming drop targets. */
isDragDisabled: boolean;

/** Drag icon wrapped in a touch target by the native variant. */
leftElement: React.JSX.Element;
};

type ColumnsSettingsListItemProps = Pick<DraggableListRenderItemParams<ColumnItem>, 'item' | 'isFocused' | 'drag' | 'isActive'> & {
/** Toggles optional columns without changing the selection of required columns. */
onSelectRow: ListItemProps<ColumnItem>['onSelectRow'];

/** Omitted when this row is already first or cannot be reordered. */
onMoveUp?: () => void;

/** Omitted when the next row is unselected or this row cannot be reordered. */
onMoveDown?: () => void;
};
Comment on lines +7 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The AI coder understands that comments should add information, which is good, but it misunderstands "information" and puts implementation details into JSDoc.

For example, "Drag icon wrapped in a touch target by the native variant" says which component wraps the icon. It will change over time, and it doesn't explain the prop's purpose or which part of the UI it belongs to. The comment before the refactor was better: "Element rendered on the left side of the list item (e.g. drag handle)".

Another example is "Selected columns appear before the alphabetically ordered unselected columns". It's a detail of only one screen. The comment before the refactor was better since it explained the prop's purpose: the table shows only the selected columns.

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 = {
allColumns: SearchCustomColumnIds[];
/** The default set of selected columns when no customization has been applied */
defaultSelectedColumns: SearchCustomColumnIds[];
/** The currently selected and ordered columns */
currentColumns: SearchCustomColumnIds[];
/** Columns that cannot be deselected by the user */
requiredColumns: Set<SearchCustomColumnIds>;
/** The active group-by field (e.g. category, tag) */
groupBy?: string;
/** The currently selected columns specific to the active group-by mode */
groupColumns?: SearchCustomColumnIds[];
/** The default columns for the active group-by mode when no customization has been applied */
defaultGroupColumns?: SearchCustomColumnIds[];
/** Callback fired with the updated column list when the user saves changes */
onSave: (columns: SearchCustomColumnIds[]) => void;
/** The active Search type, used to resolve type-specific column labels */
type?: SearchDataTypes;
};


export type {ColumnItem, ColumnsSettingsListItemProps};
Loading
Loading