From 32767f67f5b94b8f18e45a1d64929530ee1cf27b Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Wed, 21 Oct 2026 17:25:12 +0530 Subject: [PATCH 1/4] Add a sortable Rejected column to expense and expense report searches --- src/CONST/index.ts | 8 ++ .../ExpenseReportListItemRowWide.tsx | 13 +++ .../Search/SearchList/ListItem/types.ts | 6 +- src/components/Search/SearchTableHeader.tsx | 8 ++ src/components/Search/SortableTableHeader.tsx | 5 +- .../TransactionItemRowWide.tsx | 14 +++ src/components/TransactionItemRow/types.ts | 3 + src/libs/SearchUIUtils.ts | 18 +++- src/styles/utils/index.ts | 1 + src/types/onyx/SearchResults.ts | 18 +++- tests/unit/Search/SearchUIUtilsTest.ts | 102 ++++++++++++++++++ .../ColumnAvailabilityTest.ts.snap | 1 + 12 files changed, 189 insertions(+), 8 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index cf311ef2f5ca..dc7ae4a7de11 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -8152,6 +8152,11 @@ const CONST = { search: true, reportView: true, }, + DECLINED: { + column: this.TABLE_COLUMNS.DECLINED, + search: true, + reportView: false, + }, MCC: {column: this.TABLE_COLUMNS.MCC, search: true, reportView: true}, TAX_CODE: { column: this.TABLE_COLUMNS.TAX_CODE, @@ -8234,6 +8239,7 @@ const CONST = { EXPORTED: this.TABLE_COLUMNS.EXPORTED, STATUS: this.TABLE_COLUMNS.STATUS, PAID_STATUS: this.TABLE_COLUMNS.PAID_STATUS, + DECLINED: this.TABLE_COLUMNS.DECLINED, TITLE: this.TABLE_COLUMNS.TITLE, FROM: this.TABLE_COLUMNS.FROM, TO: this.TABLE_COLUMNS.TO, @@ -8447,6 +8453,7 @@ const CONST = { ORIGINAL_AMOUNT: 'originalamount', REIMBURSABLE: 'reimbursable', BILLABLE: 'billable', + DECLINED: 'declined', TAX_RATE: 'taxrate', TOTAL_AMOUNT: 'amount', ATTENDEES: 'attendees', @@ -8729,6 +8736,7 @@ const CONST = { [this.TABLE_COLUMNS.ORIGINAL_AMOUNT]: 'purchase-amount', [this.TABLE_COLUMNS.REIMBURSABLE]: 'reimbursable', [this.TABLE_COLUMNS.BILLABLE]: 'billable', + [this.TABLE_COLUMNS.DECLINED]: 'declined', [this.TABLE_COLUMNS.TAX_RATE]: 'tax-rate', [this.TABLE_COLUMNS.TOTAL_AMOUNT]: 'amount', [this.TABLE_COLUMNS.TOTAL]: 'total', diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx index b516f8fc4269..c429a4e7e71c 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx @@ -12,8 +12,10 @@ import UserInfoCell from '@components/Search/SearchList/ListItem/UserInfoCell'; import WorkspaceCell from '@components/Search/SearchList/ListItem/WorkspaceCell'; import {useRowSelection} from '@components/Search/SearchSelectionProvider'; import {useListItemContext, useListItemHovered} from '@components/SelectionList/ListItemContext'; +import Text from '@components/Text'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -50,6 +52,7 @@ function ExpenseReportListItemRowWide({ const getSearchColumnStyles = useSearchColumnStyles(); const styles = useThemeStyles(); const theme = useTheme(); + const {translate} = useLocalize(); const expensifyIcons = useMemoizedLazyExpensifyIcons(['ArrowRight']); const {isSelected} = useRowSelection(item.keyForList); const isHovered = useListItemHovered(); @@ -158,6 +161,16 @@ function ExpenseReportListItemRowWide({ /> ), + [CONST.SEARCH.TABLE_COLUMNS.DECLINED]: ( + + + {item.declined ? translate('common.yes') : translate('common.no')} + + + ), [CONST.SEARCH.TABLE_COLUMNS.TITLE]: ( = ListItemProps & }; type TransactionListItemType = ListItem & - Transaction & { + SearchTransaction & { /** Report to which the transaction belongs */ report: Report | undefined; @@ -231,7 +233,7 @@ type TransactionGroupListItemType = ListItem & { type ExpenseReportListItemType = TransactionReportGroupListItemType; -type TransactionReportGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT} & Report & { +type TransactionReportGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT} & SearchReport & { /** The personal details of the user requesting money */ from: PersonalDetails; diff --git a/src/components/Search/SearchTableHeader.tsx b/src/components/Search/SearchTableHeader.tsx index b1477a3c0545..65464689c022 100644 --- a/src/components/Search/SearchTableHeader.tsx +++ b/src/components/Search/SearchTableHeader.tsx @@ -136,6 +136,10 @@ const getExpenseHeaders = (groupBy?: SearchGroupBy): SearchColumnConfig[] => [ columnName: CONST.SEARCH.TABLE_COLUMNS.BILLABLE, translationKey: 'common.billable', }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.DECLINED, + translationKey: 'search.filters.rejected', + }, { columnName: CONST.SEARCH.TABLE_COLUMNS.MCC, translationKey: 'common.mcc', @@ -292,6 +296,10 @@ const getExpenseReportHeaders = (profileIcon?: IconAsset): SearchColumnConfig[] columnName: CONST.SEARCH.TABLE_COLUMNS.PAID_STATUS, translationKey: 'common.paidStatus', }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.DECLINED, + translationKey: 'search.filters.rejected', + }, { columnName: CONST.SEARCH.TABLE_COLUMNS.TITLE, translationKey: 'common.title', diff --git a/src/components/Search/SortableTableHeader.tsx b/src/components/Search/SortableTableHeader.tsx index e370e4558e3a..c5ca68b3c8ed 100644 --- a/src/components/Search/SortableTableHeader.tsx +++ b/src/components/Search/SortableTableHeader.tsx @@ -83,7 +83,8 @@ function SortableTableHeader({ const isSortable = shouldShowSorting && isColumnSortable; const sortByColumnName = sortColumnName ?? columnName; const isActive = sortBy === sortByColumnName; - const isReimbursableOrBillableColumn = columnName === CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE || columnName === CONST.SEARCH.TABLE_COLUMNS.BILLABLE; + const isYesNoColumn = + columnName === CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE || columnName === CONST.SEARCH.TABLE_COLUMNS.BILLABLE || columnName === CONST.SEARCH.TABLE_COLUMNS.DECLINED; const isConversionAmountColumn = columnName === CONST.SEARCH.TABLE_COLUMNS.GROUP_AMOUNT_DEBITED || columnName === CONST.SEARCH.TABLE_COLUMNS.GROUP_AMOUNT_REIMBURSED || @@ -91,7 +92,7 @@ function SortableTableHeader({ columnName === CONST.SEARCH.TABLE_COLUMNS.AMOUNT_REIMBURSED; const textStyle = [ columnName === CONST.SEARCH.TABLE_COLUMNS.RECEIPT ? StyleUtils.getTextOverflowStyle('clip') : null, - isReimbursableOrBillableColumn || isConversionAmountColumn ? styles.flexShrink1 : null, + isYesNoColumn || isConversionAmountColumn ? styles.flexShrink1 : null, ]; return ( diff --git a/src/components/TransactionItemRow/TransactionItemRowWide.tsx b/src/components/TransactionItemRow/TransactionItemRowWide.tsx index 4f0d76272dc0..cb1b05da2fe6 100644 --- a/src/components/TransactionItemRow/TransactionItemRowWide.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowWide.tsx @@ -381,6 +381,20 @@ function TransactionItemRowWide({ ); + case CONST.SEARCH.TABLE_COLUMNS.DECLINED: + return ( + + + {transactionItem.declined ? translate('common.yes') : translate('common.no')} + + + ); case CONST.SEARCH.TABLE_COLUMNS.ACTION: return ( { + // Ranked as 'yes'/'no' to match the backend, with a report that never got the field ranked as not rejected. + const aValue = a.declined ? CONST.SEARCH.BOOLEAN.YES : CONST.SEARCH.BOOLEAN.NO; + const bValue = b.declined ? CONST.SEARCH.BOOLEAN.YES : CONST.SEARCH.BOOLEAN.NO; + return compareValues(aValue, bValue, sortOrder, sortBy, localeCompare); + }); + } + const sortingProperty = sortBy === CONST.SEARCH.SORT_BY_COLUMNS.CATEGORY_GL_CODE || sortBy === CONST.SEARCH.SORT_BY_COLUMNS.TAG_GL_CODE ? undefined : expenseReportColumnNamesToSortingProperty[sortBy]; @@ -4526,6 +4536,8 @@ function getSearchColumnTranslationKey(column: SearchSortBy, type?: SearchDataTy return 'common.reimbursable'; case CONST.SEARCH.TABLE_COLUMNS.BILLABLE: return 'common.billable'; + case CONST.SEARCH.TABLE_COLUMNS.DECLINED: + return 'search.filters.rejected'; case CONST.SEARCH.TABLE_COLUMNS.MCC: return 'common.mcc'; case CONST.SEARCH.TABLE_COLUMNS.TAX_CODE: @@ -6708,6 +6720,7 @@ function getColumnsToShow({ [CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS]: false, [CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE]: false, [CONST.SEARCH.TABLE_COLUMNS.BILLABLE]: false, + [CONST.SEARCH.TABLE_COLUMNS.DECLINED]: false, [CONST.SEARCH.TABLE_COLUMNS.MCC]: false, [CONST.SEARCH.TABLE_COLUMNS.TAX_CODE]: false, [CONST.SEARCH.TABLE_COLUMNS.TAX_RATE]: false, @@ -7147,6 +7160,7 @@ function getTableMinWidth( } else if ( column === CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE || column === CONST.SEARCH.TABLE_COLUMNS.BILLABLE || + column === CONST.SEARCH.TABLE_COLUMNS.DECLINED || column === CONST.SEARCH.TABLE_COLUMNS.MCC || column === CONST.SEARCH.TABLE_COLUMNS.TAX_CODE ) { diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index c67a6f25b9ee..76a1f3216415 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -2122,6 +2122,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ break; case CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE: case CONST.SEARCH.TABLE_COLUMNS.BILLABLE: + case CONST.SEARCH.TABLE_COLUMNS.DECLINED: columnWidth = {...getWidthStyle(variables.w80)}; break; case CONST.SEARCH.TABLE_COLUMNS.MCC: diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index f92f71490bc6..4e186aac9a4e 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -285,12 +285,24 @@ type SearchQuarterGroup = SearchGroupBase & { quarter: number; }; +/** Model of an expense in the search results */ +type SearchTransaction = Transaction & { + /** Whether the expense was ever rejected, only returned when the Rejected column is shown or sorted by */ + declined?: boolean; +}; + +/** Model of a report in the search results */ +type SearchReport = Report & { + /** Whether the expense report was ever rejected, only returned when the Rejected column is shown or sorted by */ + declined?: boolean; +}; + /** SearchResultDataType */ -type SearchResultDataType = PrefixedRecord & +type SearchResultDataType = PrefixedRecord & Partial | undefined>> & PrefixedRecord> & Partial> & - PrefixedRecord & + PrefixedRecord & PrefixedRecord & PrefixedRecord & PrefixedRecord & @@ -326,6 +338,8 @@ export default SearchResults; export type { ListItemDataType, SearchTask, + SearchTransaction, + SearchReport, SearchTransactionAction, SearchDataTypes, SearchResultsInfo, diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index f7e499695c64..542ae8eacb42 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -8596,6 +8596,63 @@ describe('SearchUIUtils', () => { ]); }); + it('should sort expenses by the Rejected column, ranking expenses without the field as not rejected', () => { + // Given a rejected expense, a never rejected one, and one returned without the field, with `inserted` set so + // the tie breaker between the equal rows is predictable + const baseTransaction = transactionsListItems.at(0); + if (!baseTransaction) { + throw new Error('Missing base transaction fixture'); + } + const transactions: TransactionListItemType[] = [ + {...baseTransaction, transactionID: 'rejected', keyForList: 'rejected', inserted: '2024-01-01', declined: true}, + {...baseTransaction, transactionID: 'not-rejected', keyForList: 'not-rejected', inserted: '2024-02-02', declined: false}, + {...baseTransaction, transactionID: 'no-field', keyForList: 'no-field', inserted: '2024-03-03'}, + ]; + + // When the expenses are sorted by Rejected in each direction + const sortTransactions = (sortOrder: SortOrder) => + SearchUIUtils.getSortedSections( + CONST.SEARCH.DATA_TYPES.EXPENSE, + [...transactions], + localeCompare, + translateLocal, + CONST.SEARCH.TABLE_COLUMNS.DECLINED, + sortOrder, + undefined, + ).map((item) => ('transactionID' in item ? item.transactionID : undefined)); + + // Then the expense without the field ranks with the not rejected one, the way its cell shows "No", so the local + // sort agrees with the backend's 'yes'/'no' ranking + expect(sortTransactions(CONST.SEARCH.SORT_ORDER.ASC)).toEqual(['not-rejected', 'no-field', 'rejected']); + expect(sortTransactions(CONST.SEARCH.SORT_ORDER.DESC)).toEqual(['rejected', 'no-field', 'not-rejected']); + }); + + it('should sort expense reports by the Rejected column, ranking reports without the field as not rejected', () => { + // Given a rejected expense report, a never rejected one, and one returned without the field + const createReport = (keyForList: string, declined?: boolean) => + createMock({ + groupedBy: 'expense-report', + keyForList, + reportID: keyForList, + created: '2025-08-12 17:11:22', + currency: 'USD', + transactions: [], + ...(declined === undefined ? {} : {declined}), + }); + const reports = [createReport('report_rejected', true), createReport('report_not_rejected', false), createReport('report_no_field')]; + + // When the reports are sorted by Rejected in each direction + const sortReports = (sortOrder: SortOrder) => + SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, [...reports], localeCompare, translateLocal, CONST.SEARCH.TABLE_COLUMNS.DECLINED, sortOrder).map( + (report) => report.keyForList, + ); + + // Then the report without the field ranks with the not rejected one, the way its cell shows "No", and equal + // reports keep the order the backend returned them in + expect(sortReports(CONST.SEARCH.SORT_ORDER.ASC)).toStrictEqual(['report_not_rejected', 'report_no_field', 'report_rejected']); + expect(sortReports(CONST.SEARCH.SORT_ORDER.DESC)).toStrictEqual(['report_rejected', 'report_not_rejected', 'report_no_field']); + }); + it('should return getSortedReportData result when type is expense-report', () => { expect(SearchUIUtils.getSortedSections(CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, transactionReportGroupListItems, localeCompare, translateLocal, 'date', 'asc')).toStrictEqual( transactionReportGroupListItems, @@ -11884,6 +11941,40 @@ describe('SearchUIUtils', () => { expect(result).toContain(CONST.SEARCH.TABLE_COLUMNS.TOTAL); }); + test('Should keep the Rejected column hidden on expense searches until the user picks it', () => { + // Given an expense the backend marked as rejected + const rejectedTransaction = {...createRandomTransaction(4), declined: true}; + const pickedColumns = [CONST.SEARCH.TABLE_COLUMNS.DATE, CONST.SEARCH.TABLE_COLUMNS.DECLINED, CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]; + + // When the columns are resolved with and without the user picking Rejected + const defaultColumns = SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [rejectedTransaction], visibleColumns: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE}); + const pickedResult = SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [rejectedTransaction], visibleColumns: pickedColumns, type: CONST.SEARCH.DATA_TYPES.EXPENSE}); + + // Then Rejected is offered in the Search column picker but only shown once picked, and it is not offered in the + // single-report view because the report view's transactions never carry the field + expect(SearchUIUtils.getCustomColumns(CONST.SEARCH.DATA_TYPES.EXPENSE)).toContain(CONST.SEARCH.TABLE_COLUMNS.DECLINED); + expect(SearchUIUtils.getCustomColumnDefault(CONST.SEARCH.DATA_TYPES.EXPENSE)).not.toContain(CONST.SEARCH.TABLE_COLUMNS.DECLINED); + expect(Object.values(CONST.SEARCH.REPORT_DETAILS_CUSTOM_COLUMNS)).not.toContain(CONST.SEARCH.TABLE_COLUMNS.DECLINED); + expect(defaultColumns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.DECLINED); + expect(pickedResult).toContain(CONST.SEARCH.TABLE_COLUMNS.DECLINED); + }); + + test('Should keep the Rejected column hidden on expense report searches until the user picks it', () => { + // Given the column lists for expense report searches + const visibleColumns = [CONST.SEARCH.TABLE_COLUMNS.DATE, CONST.SEARCH.TABLE_COLUMNS.DECLINED, CONST.SEARCH.TABLE_COLUMNS.TOTAL]; + + // When the columns are resolved with and without the user picking Rejected + const defaultColumns = SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [], visibleColumns: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT}); + const pickedResult = SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [], visibleColumns, type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT}); + + // Then Rejected is offered in the expense report column picker, stays out of the defaults, and is shown in the + // picked position once chosen, so it is sent in the query columns the backend reads to return the field + expect(SearchUIUtils.getCustomColumns(CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT)).toContain(CONST.SEARCH.TABLE_COLUMNS.DECLINED); + expect(SearchUIUtils.getCustomColumnDefault(CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT)).not.toContain(CONST.SEARCH.TABLE_COLUMNS.DECLINED); + expect(defaultColumns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.DECLINED); + expect(pickedResult).toEqual(visibleColumns); + }); + test('Should not force Avatar for group-by:from when toggled off', () => { const visibleColumns = [CONST.SEARCH.TABLE_COLUMNS.GROUP_FROM, CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL]; @@ -13298,6 +13389,17 @@ describe('SearchUIUtils', () => { const translationKey = SearchUIUtils.getSearchColumnTranslationKey(CONST.SEARCH.TABLE_COLUMNS.WITHDRAWAL_ID); expect(translationKey).toBe('common.withdrawalID'); }); + + it('should label the DECLINED column with the Rejected filter label', () => { + // Given the Rejected column, whose header is also sent as the export column label + // When its translation key is resolved for expenses and expense reports + const expenseKey = SearchUIUtils.getSearchColumnTranslationKey(CONST.SEARCH.TABLE_COLUMNS.DECLINED, CONST.SEARCH.DATA_TYPES.EXPENSE); + const expenseReportKey = SearchUIUtils.getSearchColumnTranslationKey(CONST.SEARCH.TABLE_COLUMNS.DECLINED, CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT); + + // Then it reuses the declined: filter's "Rejected" label, so the column, the filter, and the export header match + expect(expenseKey).toBe('search.filters.rejected'); + expect(expenseReportKey).toBe('search.filters.rejected'); + }); }); describe('createAndOpenSearchTransactionThread', () => { diff --git a/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap b/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap index c85a7d23b544..c53741bba724 100644 --- a/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap +++ b/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap @@ -61,6 +61,7 @@ exports[`Column availability single source of truth the derived picker lists mat "base62ReportID", "reimbursable", "billable", + "declined", "mcc", "taxCode", "taxrate", From 73c2826419962917046477ba17fb6c87bd1b8029 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Sun, 25 Oct 2026 17:12:34 +0530 Subject: [PATCH 2/4] Keep the Search row's declined field out of transactions built from it --- src/libs/getTransactionFromTransactionListItem.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/getTransactionFromTransactionListItem.ts b/src/libs/getTransactionFromTransactionListItem.ts index 7be736d4dfa2..aaa7256232cb 100644 --- a/src/libs/getTransactionFromTransactionListItem.ts +++ b/src/libs/getTransactionFromTransactionListItem.ts @@ -42,6 +42,7 @@ const SEARCH_TRANSACTION_ROW_ONLY_FIELDS: Record Date: Mon, 26 Oct 2026 13:24:49 +0530 Subject: [PATCH 3/4] Share the Yes/No Search cell between the expense and report rows --- .../ExpenseReportListItemRowWide.tsx | 11 ++------ .../Search/SearchList/ListItem/YesNoCell.tsx | 27 +++++++++++++++++++ .../TransactionItemRowWide.tsx | 9 ++----- 3 files changed, 31 insertions(+), 16 deletions(-) create mode 100644 src/components/Search/SearchList/ListItem/YesNoCell.tsx diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx index c429a4e7e71c..648436352fbf 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx @@ -10,12 +10,11 @@ import TextCell from '@components/Search/SearchList/ListItem/TextCell'; import TotalCell from '@components/Search/SearchList/ListItem/TotalCell'; import UserInfoCell from '@components/Search/SearchList/ListItem/UserInfoCell'; import WorkspaceCell from '@components/Search/SearchList/ListItem/WorkspaceCell'; +import YesNoCell from '@components/Search/SearchList/ListItem/YesNoCell'; import {useRowSelection} from '@components/Search/SearchSelectionProvider'; import {useListItemContext, useListItemHovered} from '@components/SelectionList/ListItemContext'; -import Text from '@components/Text'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -52,7 +51,6 @@ function ExpenseReportListItemRowWide({ const getSearchColumnStyles = useSearchColumnStyles(); const styles = useThemeStyles(); const theme = useTheme(); - const {translate} = useLocalize(); const expensifyIcons = useMemoizedLazyExpensifyIcons(['ArrowRight']); const {isSelected} = useRowSelection(item.keyForList); const isHovered = useListItemHovered(); @@ -163,12 +161,7 @@ function ExpenseReportListItemRowWide({ ), [CONST.SEARCH.TABLE_COLUMNS.DECLINED]: ( - - {item.declined ? translate('common.yes') : translate('common.no')} - + ), [CONST.SEARCH.TABLE_COLUMNS.TITLE]: ( diff --git a/src/components/Search/SearchList/ListItem/YesNoCell.tsx b/src/components/Search/SearchList/ListItem/YesNoCell.tsx new file mode 100644 index 000000000000..09262a6cb8c7 --- /dev/null +++ b/src/components/Search/SearchList/ListItem/YesNoCell.tsx @@ -0,0 +1,27 @@ +import Text from '@components/Text'; + +import useLocalize from '@hooks/useLocalize'; + +import {COPYABLE_TEXT_DATA_SET} from '@libs/SelectionScraper'; + +import React from 'react'; + +type YesNoCellProps = { + /** Whether the cell shows Yes or No */ + value: boolean; +}; + +function YesNoCell({value}: YesNoCellProps) { + const {translate} = useLocalize(); + + return ( + + {value ? translate('common.yes') : translate('common.no')} + + ); +} + +export default YesNoCell; diff --git a/src/components/TransactionItemRow/TransactionItemRowWide.tsx b/src/components/TransactionItemRow/TransactionItemRowWide.tsx index c25933f4d0ee..2d8188417b67 100644 --- a/src/components/TransactionItemRow/TransactionItemRowWide.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowWide.tsx @@ -12,8 +12,8 @@ import TextCell from '@components/Search/SearchList/ListItem/TextCell'; import AmountCell from '@components/Search/SearchList/ListItem/TotalCell'; import UserInfoCell from '@components/Search/SearchList/ListItem/UserInfoCell'; import WorkspaceCell from '@components/Search/SearchList/ListItem/WorkspaceCell'; +import YesNoCell from '@components/Search/SearchList/ListItem/YesNoCell'; import type {SearchColumnType} from '@components/Search/types'; -import Text from '@components/Text'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -185,12 +185,7 @@ function TransactionItemRowWide({ key={columnName} style={[getSearchColumnStyles(columnName)]} > - - {value ? translate('common.yes') : translate('common.no')} - + ); From 6dc80e11d03a2704d2d259dfd01259ee96e385d3 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Tue, 27 Oct 2026 08:19:46 +0530 Subject: [PATCH 4/4] Drop the boolean sort cast and reuse the SearchTransaction declined type --- src/components/TransactionItemRow/types.ts | 4 ++-- src/libs/SearchUIUtils.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/TransactionItemRow/types.ts b/src/components/TransactionItemRow/types.ts index ff71d47260af..cc20676ea66f 100644 --- a/src/components/TransactionItemRow/types.ts +++ b/src/components/TransactionItemRow/types.ts @@ -5,7 +5,7 @@ import type {ModifiedMouseEvent} from '@libs/Navigation/helpers/openInternalRout import type {CardList, PersonalDetails, Policy, PolicyCategories, PolicyTagLists, Report, ReportAction, TransactionViolation} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; -import type {SearchTransactionAction} from '@src/types/onyx/SearchResults'; +import type {SearchTransaction, SearchTransactionAction} from '@src/types/onyx/SearchResults'; import type {StyleProp, ViewStyle} from 'react-native'; @@ -62,7 +62,7 @@ type TransactionWithOptionalSearchFields = TransactionWithOptionalHighlight & { policyID?: string; /** Whether the expense was ever rejected, provided on search transactions when the Rejected column is shown or sorted by */ - declined?: boolean; + declined?: SearchTransaction['declined']; }; type TransactionItemRowProps = { diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 413e37d5c600..b531e5c16899 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3976,8 +3976,7 @@ function getTransactionSortValue(transaction: TransactionListItemType, sortingPr } if (sortingProperty === 'reimbursable' || sortingProperty === 'billable' || sortingProperty === 'declined') { - const boolValue = transaction[sortingProperty as keyof TransactionListItemType]; - return boolValue ? CONST.SEARCH.BOOLEAN.YES : CONST.SEARCH.BOOLEAN.NO; + return transaction[sortingProperty] ? CONST.SEARCH.BOOLEAN.YES : CONST.SEARCH.BOOLEAN.NO; } if (sortingProperty === CONST.SEARCH.TABLE_COLUMNS.HELD) {