From eb8c18b5a08bac1d7b5d5f0c480f10f5c424e165 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Thu, 17 Sep 2026 15:26:26 +0530 Subject: [PATCH 1/7] Add Vendor column to expense Search and report tables --- src/CONST/index.ts | 7 +++ src/components/ColumnsSettingsList.tsx | 25 ++++++--- .../MoneyRequestReportTableHeader.tsx | 8 ++- .../MoneyRequestReportTransactionList.tsx | 3 +- src/components/Search/SearchTableHeader.tsx | 24 ++++++++- .../TransactionItemRowWide.tsx | 11 +++- src/libs/PolicyUtils.ts | 42 +++++++++++++++ src/libs/SearchUIUtils.ts | 15 +++++- src/libs/getSearchColumnContentToMeasure.ts | 6 ++- src/pages/Search/SearchColumnsPage.tsx | 20 +++++++- .../Report/ReportDetailsColumnsPage.tsx | 17 +++++-- src/styles/utils/index.ts | 1 + tests/unit/PolicyUtilsTest.ts | 51 +++++++++++++++++++ tests/unit/Search/SearchUIUtilsTest.ts | 27 ++++++++++ .../ColumnAvailabilityTest.ts.snap | 2 + 15 files changed, 240 insertions(+), 19 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 1cb71e53c2da..af84328ef060 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7360,6 +7360,11 @@ const CONST = { search: true, reportView: true, }, + VENDOR: { + column: this.TABLE_COLUMNS.VENDOR, + search: true, + reportView: true, + }, DESCRIPTION: { column: this.TABLE_COLUMNS.DESCRIPTION, search: true, @@ -7721,6 +7726,7 @@ const CONST = { POSTED: 'posted', EXPORTED: 'exported', MERCHANT: 'merchant', + VENDOR: 'vendor', DESCRIPTION: 'description', FROM: 'from', TO: 'to', @@ -7963,6 +7969,7 @@ const CONST = { [this.TABLE_COLUMNS.POSTED]: 'posted', [this.TABLE_COLUMNS.EXPORTED]: 'exported', [this.TABLE_COLUMNS.MERCHANT]: 'merchant', + [this.TABLE_COLUMNS.VENDOR]: 'vendor', [this.TABLE_COLUMNS.DESCRIPTION]: 'description', [this.TABLE_COLUMNS.FROM]: 'from', [this.TABLE_COLUMNS.TO]: 'to', diff --git a/src/components/ColumnsSettingsList.tsx b/src/components/ColumnsSettingsList.tsx index 304de4ec2acb..018d92173702 100644 --- a/src/components/ColumnsSettingsList.tsx +++ b/src/components/ColumnsSettingsList.tsx @@ -65,11 +65,24 @@ type ColumnsSettingsListProps = { /** The default columns for the active group-by mode when no customization has been applied */ defaultGroupColumns?: SearchCustomColumnIds[]; + /** Whether the vendor column is labelled "Supplier", which is what Xero calls vendors */ + shouldUseSupplierLabel?: boolean; + /** Callback fired with the updated column list when the user saves changes */ onSave: (columns: SearchCustomColumnIds[]) => void; }; -function ColumnsSettingsList({allColumns, defaultSelectedColumns, currentColumns, requiredColumns, groupBy, groupColumns = [], defaultGroupColumns = [], onSave}: ColumnsSettingsListProps) { +function ColumnsSettingsList({ + allColumns, + defaultSelectedColumns, + currentColumns, + requiredColumns, + groupBy, + groupColumns = [], + defaultGroupColumns = [], + shouldUseSupplierLabel = false, + onSave, +}: ColumnsSettingsListProps) { const theme = useTheme(); const styles = useThemeStyles(); const icons = useMemoizedLazyExpensifyIcons(['DragHandles']); @@ -85,8 +98,8 @@ function ColumnsSettingsList({allColumns, defaultSelectedColumns, currentColumns const unselected = columnsToSort .filter((col) => !col.isSelected) .sort((a, b) => { - const textA = translate(getSearchColumnTranslationKey(a.value)); - const textB = translate(getSearchColumnTranslationKey(b.value)); + const textA = translate(getSearchColumnTranslationKey(a.value, shouldUseSupplierLabel)); + const textB = translate(getSearchColumnTranslationKey(b.value, shouldUseSupplierLabel)); return localeCompare(textA, textB); }); return [...selected, ...unselected]; @@ -118,7 +131,7 @@ function ColumnsSettingsList({allColumns, defaultSelectedColumns, currentColumns const isEffectivelySelected = isRequired || isSelected; const isDragDisabled = !isEffectivelySelected; return { - text: translate(getSearchColumnTranslationKey(columnId)), + text: translate(getSearchColumnTranslationKey(columnId, shouldUseSupplierLabel)), value: columnId, keyForList: columnId, isSelected: isEffectivelySelected, @@ -164,8 +177,8 @@ function ColumnsSettingsList({allColumns, defaultSelectedColumns, currentColumns const selectedCols = prevColumns.filter((col) => col.isSelected); const unselected = prevColumns.filter((col) => !col.isSelected && col.columnId !== updatedColumnId); const unselectedSorted = unselected.sort((a, b) => { - const textA = translate(getSearchColumnTranslationKey(a.columnId)); - const textB = translate(getSearchColumnTranslationKey(b.columnId)); + const textA = translate(getSearchColumnTranslationKey(a.columnId, shouldUseSupplierLabel)); + const textB = translate(getSearchColumnTranslationKey(b.columnId, shouldUseSupplierLabel)); return localeCompare(textA, textB); }); return [...selectedCols, {columnId: updatedColumnId, isSelected: true}, ...unselectedSorted]; diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx index d328b2b08a9e..cc9bab2c5d0a 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx @@ -5,6 +5,7 @@ import type {SearchColumnType, SearchSortBy, SortOrder, TableColumnSize} from '@ import useThemeStyles from '@hooks/useThemeStyles'; import {isSortableColumnName} from '@libs/ReportUtils'; +import {getSearchColumnTranslationKey} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; @@ -22,6 +23,9 @@ type SearchTableHeaderProps = { shouldShowSorting: boolean; columns: SearchColumnType[]; shouldRemoveTotalColumnFlex?: boolean; + + /** Whether the vendor column is labelled "Supplier", which is what Xero calls vendors */ + shouldUseSupplierLabel?: boolean; }; function MoneyRequestReportTableHeader({ sortBy, @@ -34,6 +38,7 @@ function MoneyRequestReportTableHeader({ amountColumnSize, taxAmountColumnSize, shouldRemoveTotalColumnFlex, + shouldUseSupplierLabel = false, }: SearchTableHeaderProps) { const styles = useThemeStyles(); @@ -41,6 +46,7 @@ function MoneyRequestReportTableHeader({ () => [ ...getExpenseHeaders().map((header) => ({ ...header, + translationKey: header.columnName === CONST.SEARCH.TABLE_COLUMNS.VENDOR ? getSearchColumnTranslationKey(header.columnName, shouldUseSupplierLabel) : header.translationKey, isColumnSortable: isSortableColumnName(header.columnName), })), { @@ -49,7 +55,7 @@ function MoneyRequestReportTableHeader({ isColumnSortable: false, }, ], - [], + [shouldUseSupplierLabel], ); const orderedColumnConfig = useMemo(() => { diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index fe676d9b3280..2945401d0c29 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -36,7 +36,7 @@ import {resolveTransactionCardFields} from '@libs/CardUtils'; import {isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils'; import {navigationRef} from '@libs/Navigation/Navigation'; import {getDistanceExpenseTypeForPolicy} from '@libs/PolicyDistanceRatesUtils'; -import {isPolicyTaxEnabled} from '@libs/PolicyUtils'; +import {isPolicyTaxEnabled, isXeroActiveMatchingSource} from '@libs/PolicyUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {groupTransactionsByCategory, groupTransactionsByTag} from '@libs/ReportLayoutUtils'; import { @@ -943,6 +943,7 @@ function MoneyRequestReportTransactionList({ sortOrder={sortOrder} shouldRemoveTotalColumnFlex={hasFlexColumn(columnsToShow)} columns={columnsToShow} + shouldUseSupplierLabel={isXeroActiveMatchingSource(policy)} dateColumnSize={dateColumnSize} postedColumnSize={postedColumnSize} amountColumnSize={amountColumnSize} diff --git a/src/components/Search/SearchTableHeader.tsx b/src/components/Search/SearchTableHeader.tsx index 1969275c0f7b..b452dc6b2041 100644 --- a/src/components/Search/SearchTableHeader.tsx +++ b/src/components/Search/SearchTableHeader.tsx @@ -1,12 +1,20 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import {getVendorSearchAvailability} from '@libs/PolicyUtils'; +import {getSearchColumnTranslationKey} from '@libs/SearchUIUtils'; + import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy} from '@src/types/onyx'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import type IconAsset from '@src/types/utils/IconAsset'; +import type {OnyxCollection} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import React, {useCallback, useMemo} from 'react'; @@ -67,6 +75,11 @@ const getExpenseHeaders = (groupBy?: SearchGroupBy): SearchColumnConfig[] => [ translationKey: 'common.merchant', canEdit: true, }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.VENDOR, + translationKey: 'common.vendor', + isColumnSortable: false, + }, { columnName: CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION, translationKey: 'common.description', @@ -572,6 +585,11 @@ function SearchTableHeader({ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth; + const {isBetaEnabled} = usePermissions(); + const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); + const [shouldUseSupplierLabel = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { + selector: (allPolicies: OnyxCollection) => getVendorSearchAvailability(allPolicies, isVendorMatchingBetaEnabled).shouldUseSupplierLabel, + }); // Only load Profile icon when it's needed for EXPENSE_REPORT type or grouped transactions const icons = useMemoizedLazyExpensifyIcons(type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT || !!groupBy ? ['Profile', 'Bank', 'CreditCard'] : []) satisfies SearchHeaderIcons; @@ -601,7 +619,9 @@ function SearchTableHeader({ for (const col of columns) { const config = configMap.get(col); if (config) { - orderedConfig.push(config); + orderedConfig.push( + config.columnName === CONST.SEARCH.TABLE_COLUMNS.VENDOR ? {...config, translationKey: getSearchColumnTranslationKey(config.columnName, shouldUseSupplierLabel)} : config, + ); addedColumns.add(col); } } @@ -613,7 +633,7 @@ function SearchTableHeader({ } return orderedConfig; - }, [columnConfig, columns]); + }, [columnConfig, columns, shouldUseSupplierLabel]); if (displayNarrowVersion) { return; diff --git a/src/components/TransactionItemRow/TransactionItemRowWide.tsx b/src/components/TransactionItemRow/TransactionItemRowWide.tsx index bcf69deb6697..13610187f0f8 100644 --- a/src/components/TransactionItemRow/TransactionItemRowWide.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowWide.tsx @@ -25,7 +25,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {getCategoryGLCode} from '@libs/CategoryUtils'; import getBase62ReportID from '@libs/getBase62ReportID'; -import {isTaxCodeCustomized, getTagGLCode} from '@libs/PolicyUtils'; +import {getTagGLCode, getVendorDisplayName, isTaxCodeCustomized} from '@libs/PolicyUtils'; import {getReportName} from '@libs/ReportNameUtils'; import {getReimbursableTotal, isExpenseReport} from '@libs/ReportUtils'; import {getViolationsForTransaction} from '@libs/SearchUIUtils'; @@ -391,6 +391,15 @@ function TransactionItemRowWide({ /> ); + case CONST.SEARCH.TABLE_COLUMNS.VENDOR: + return ( + + + + ); case CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION: return ( , isVendorMatchingBetaEnabled ); } +type VendorSearchAvailability = { + /** Whether at least one workspace the user can see has the vendor feature, so Search can offer the vendor column. */ + isAvailable: boolean; + + /** Whether every eligible workspace takes its vendors from Xero, which calls them suppliers. */ + shouldUseSupplierLabel: boolean; +}; + +/** + * Search spans every workspace at once, so the vendor column is offered when any workspace has the vendor feature, + * and the "Supplier" wording is only used when no eligible workspace would call them vendors. + */ +function getVendorSearchAvailability(policies: OnyxCollection, isVendorMatchingBetaEnabled: boolean): VendorSearchAvailability { + let isAvailable = false; + let areAllEligiblePoliciesXero = true; + for (const policy of Object.values(policies ?? {})) { + if (!hasVendorFeature(policy, isVendorMatchingBetaEnabled)) { + continue; + } + isAvailable = true; + if (!isXeroActiveMatchingSource(policy)) { + areAllEligiblePoliciesXero = false; + } + } + return {isAvailable, shouldUseSupplierLabel: isAvailable && areAllEligiblePoliciesXero}; +} + /** * Single source of truth for which connected integration scopes the vendor field for this workspace * (QBO, Sage Intacct, Xero, Rillet, or DualEntry) and what its vendor list looks like. Returns `undefined` when no @@ -2910,6 +2938,18 @@ function findVendorByID(policy: OnyxEntry, vendorID: string | undefined) return getDualEntryVendors(policy).find((vendor) => vendor.id === vendorID); } +/** + * Display name of the vendor assigned to a transaction, or an empty string when none is assigned. The workspace's + * synced vendor list wins so renames in the accounting system show through, then the name persisted on the + * transaction when the vendor was assigned covers vendors that have since left the synced list. + */ +function getVendorDisplayName(policy: OnyxEntry, vendor: TransactionCommentVendor | undefined): string { + if (!vendor?.externalID) { + return ''; + } + return findVendorByID(policy, vendor.externalID)?.name ?? vendor.name ?? ''; +} + /** * Resolves the text shown for a stored merchant-rule vendor ID. Prefer the active vendor-matching * source, use the unavailable label when its loaded list no longer contains the vendor, and retain @@ -3475,6 +3515,7 @@ export { getConnectedIntegration, getConnectionExporters, findVendorByID, + getVendorDisplayName, getActiveVendorMatchingIntegration, getMatchingVendorByID, getMatchingVendors, @@ -3489,6 +3530,7 @@ export { isXeroActiveMatchingSource, isXeroVendorMatchingActive, hasVendorFeature, + getVendorSearchAvailability, isMatchingVendorListLoaded, getValidConnectedIntegration, getCountOfEnabledTagsOfList, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 701928b00741..1e607081f927 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -319,6 +319,7 @@ const transactionColumnNamesToSortingProperty: TransactionSorting = { [CONST.SEARCH.TABLE_COLUMNS.EXPORTED]: 'exported' as const, [CONST.SEARCH.TABLE_COLUMNS.TAG]: 'tag' as const, [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: 'formattedMerchant' as const, + [CONST.SEARCH.TABLE_COLUMNS.VENDOR]: null, [CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: 'formattedTotal' as const, [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: 'category' as const, [CONST.SEARCH.TABLE_COLUMNS.ORIGINAL_AMOUNT]: 'originalAmount' as const, @@ -4305,7 +4306,10 @@ function getCustomColumnDefault(value?: SearchDataTypes | SearchGroupBy): Search } } -function getSearchColumnTranslationKey(column: SearchSortBy): TranslationPaths { +/** + * Xero calls vendors suppliers, so the vendor column reads "Supplier" when every eligible workspace takes its vendors from Xero. + */ +function getSearchColumnTranslationKey(column: SearchSortBy, shouldUseSupplierLabel = false): TranslationPaths { switch (column) { case CONST.SEARCH.TABLE_COLUMNS.AVATAR: return 'common.avatar'; @@ -4327,6 +4331,8 @@ function getSearchColumnTranslationKey(column: SearchSortBy): TranslationPaths { return 'search.filters.exported'; case CONST.SEARCH.TABLE_COLUMNS.MERCHANT: return 'common.merchant'; + case CONST.SEARCH.TABLE_COLUMNS.VENDOR: + return shouldUseSupplierLabel ? 'common.supplier' : 'common.vendor'; case CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION: return 'common.description'; case CONST.SEARCH.TABLE_COLUMNS.FROM: @@ -6386,6 +6392,7 @@ function getColumnsToShow({ [CONST.SEARCH.TABLE_COLUMNS.DATE]: true, [CONST.SEARCH.TABLE_COLUMNS.POSTED]: false, [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: false, + [CONST.SEARCH.TABLE_COLUMNS.VENDOR]: false, [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: false, [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: false, [CONST.SEARCH.TABLE_COLUMNS.CATEGORY_GL_CODE]: false, @@ -6416,6 +6423,7 @@ function getColumnsToShow({ [CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: false, [CONST.SEARCH.TABLE_COLUMNS.APPROVED]: false, [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: false, + [CONST.SEARCH.TABLE_COLUMNS.VENDOR]: false, [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: false, [CONST.SEARCH.TABLE_COLUMNS.FROM]: false, [CONST.SEARCH.TABLE_COLUMNS.TO]: false, @@ -6517,6 +6525,10 @@ function getColumnsToShow({ columns[CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION] = true; } + if (transaction.comment?.vendor?.externalID) { + columns[CONST.SEARCH.TABLE_COLUMNS.VENDOR] = true; + } + const hasCategory = (() => { const category = getCategory(transaction); return category !== '' && category !== CONST.SEARCH.CATEGORY_EMPTY_VALUE; @@ -7030,6 +7042,7 @@ function shouldShowDeleteOption( const FLEX_COLUMNS = new Set([ CONST.SEARCH.TABLE_COLUMNS.MERCHANT, + CONST.SEARCH.TABLE_COLUMNS.VENDOR, CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION, CONST.SEARCH.TABLE_COLUMNS.CATEGORY, CONST.SEARCH.TABLE_COLUMNS.CATEGORY_GL_CODE, diff --git a/src/libs/getSearchColumnContentToMeasure.ts b/src/libs/getSearchColumnContentToMeasure.ts index 79aaacabfe8c..b63c270e4de2 100644 --- a/src/libs/getSearchColumnContentToMeasure.ts +++ b/src/libs/getSearchColumnContentToMeasure.ts @@ -17,7 +17,7 @@ import type {MeasurableFont} from './measureTextWidth/types'; import {getCompanyCardDescription} from './CardUtils'; import {getCategoryGLCode, getDecodedLeafCategoryName, isCategoryMissing} from './CategoryUtils'; import getBase62ReportID from './getBase62ReportID'; -import {getTagGLCode} from './PolicyUtils'; +import {getTagGLCode, getVendorDisplayName} from './PolicyUtils'; import {getReportName} from './ReportNameUtils'; import {getReportStatusTranslation} from './ReportUtils'; import { @@ -90,6 +90,7 @@ const EDITABLE_SEARCH_COLUMNS = new Set([ const DYNAMICALLY_SIZED_SEARCH_COLUMNS = new Set([ CONST.SEARCH.TABLE_COLUMNS.STATUS, CONST.SEARCH.TABLE_COLUMNS.MERCHANT, + CONST.SEARCH.TABLE_COLUMNS.VENDOR, CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION, CONST.SEARCH.TABLE_COLUMNS.CATEGORY, CONST.SEARCH.TABLE_COLUMNS.TAG, @@ -131,6 +132,7 @@ const HUGGED_SEARCH_COLUMNS = new Set([CONST.SEARCH.TABLE_COLU const SEARCH_COLUMN_HEADER_TRANSLATION_KEYS: Partial> = { [CONST.SEARCH.TABLE_COLUMNS.STATUS]: 'common.status', [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: 'common.merchant', + [CONST.SEARCH.TABLE_COLUMNS.VENDOR]: 'common.vendor', [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: 'common.description', [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: 'common.category', [CONST.SEARCH.TABLE_COLUMNS.TAG]: 'common.tag', @@ -216,6 +218,8 @@ function getTransactionColumnContentToMeasure( ]; case CONST.SEARCH.TABLE_COLUMNS.MERCHANT: return [{text: getMerchantName(item, translate)}]; + case CONST.SEARCH.TABLE_COLUMNS.VENDOR: + return [{text: getVendorDisplayName(item.policy, item.comment?.vendor)}]; case CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION: return [{text: getDescription(item)}]; case CONST.SEARCH.TABLE_COLUMNS.CATEGORY: diff --git a/src/pages/Search/SearchColumnsPage.tsx b/src/pages/Search/SearchColumnsPage.tsx index 41939acbbef9..8e222ba8a8c0 100644 --- a/src/pages/Search/SearchColumnsPage.tsx +++ b/src/pages/Search/SearchColumnsPage.tsx @@ -3,8 +3,10 @@ import {useSearchQueryContext} from '@components/Search/SearchContext'; import type {SearchCustomColumnIds} from '@components/Search/types'; import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; import Navigation from '@libs/Navigation/Navigation'; +import {getVendorSearchAvailability} from '@libs/PolicyUtils'; import {buildQueryStringFromFilterFormValues, getCurrentSearchQueryJSON, hasValuesIncludeViolationFilter} from '@libs/SearchQueryUtils'; import {getCustomColumnDefault, getCustomColumns, insertColumnBeforeTotalAmount} from '@libs/SearchUIUtils'; @@ -12,12 +14,21 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; +import type {Policy} from '@src/types/onyx'; + +import type {OnyxCollection} from 'react-native-onyx'; import React from 'react'; function SearchColumnsPage() { const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); const {currentSearchKey} = useSearchQueryContext(); + const {isBetaEnabled} = usePermissions(); + const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); + const [vendorSearchAvailability] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { + selector: (allPolicies: OnyxCollection) => getVendorSearchAvailability(allPolicies, isVendorMatchingBetaEnabled), + }); + const isVendorColumnAvailable = vendorSearchAvailability?.isAvailable ?? false; const groupBy = searchAdvancedFiltersForm?.groupBy; const queryType = searchAdvancedFiltersForm?.type ?? CONST.SEARCH.DATA_TYPES.EXPENSE; @@ -25,11 +36,15 @@ function SearchColumnsPage() { // Violations data is only returned when these filters are set, so hide the column otherwise. const shouldRequireViolationsColumn = hasValuesIncludeViolationFilter(searchAdvancedFiltersForm?.has); - const allTypeCustomColumns = getCustomColumns(queryType).filter((column) => shouldRequireViolationsColumn || column !== CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS); + // The vendor column only exists for workspaces with the vendor feature, so hide it when none of the user's workspaces has it. + const isColumnAvailable = (column: SearchCustomColumnIds) => + (shouldRequireViolationsColumn || column !== CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS) && (isVendorColumnAvailable || column !== CONST.SEARCH.TABLE_COLUMNS.VENDOR); + + const allTypeCustomColumns = getCustomColumns(queryType).filter(isColumnAvailable); const allGroupCustomColumns = getCustomColumns(groupBy); const defaultGroupCustomColumns = getCustomColumnDefault(groupBy); const defaultTypeCustomColumns = [...getCustomColumnDefault(queryType)]; - const currentColumns = [...(searchAdvancedFiltersForm?.columns ?? [])].filter((column) => shouldRequireViolationsColumn || column !== CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS); + const currentColumns = [...(searchAdvancedFiltersForm?.columns ?? [])].filter(isColumnAvailable); // We need at least one element with flex1 in the table to ensure the table looks good in the UI, so we don't allow removing the total columns // since it makes sense for them to show up in an expense management App and it fixes the layout issues. @@ -82,6 +97,7 @@ function SearchColumnsPage() { groupBy={groupBy} groupColumns={allGroupCustomColumns} defaultGroupColumns={defaultGroupCustomColumns} + shouldUseSupplierLabel={vendorSearchAvailability?.shouldUseSupplierLabel} onSave={applyChanges} /> ); diff --git a/src/pages/settings/Report/ReportDetailsColumnsPage.tsx b/src/pages/settings/Report/ReportDetailsColumnsPage.tsx index 84fd8918fc7d..73bea8de328e 100644 --- a/src/pages/settings/Report/ReportDetailsColumnsPage.tsx +++ b/src/pages/settings/Report/ReportDetailsColumnsPage.tsx @@ -3,12 +3,13 @@ import type {SearchCustomColumnIds} from '@components/Search/types'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; import {setReportDetailsColumns} from '@libs/actions/ReportLayout'; import {isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import {isPolicyTaxEnabled} from '@libs/PolicyUtils'; +import {hasVendorFeature, isPolicyTaxEnabled, isXeroActiveMatchingSource} from '@libs/PolicyUtils'; import {isIOUReport} from '@libs/ReportUtils'; import {getColumnsToShow} from '@libs/SearchUIUtils'; import {hasNonReimbursableTransactions} from '@libs/TransactionUtils'; @@ -65,6 +66,11 @@ function ReportDetailsColumnsPage() { }, }); const currentUserDetails = useCurrentUserPersonalDetails(); + const {isBetaEnabled} = usePermissions(); + + // The vendor column only exists for workspaces with the vendor feature, so hide it when this report's workspace lacks it. + const isVendorColumnAvailable = hasVendorFeature(policy, isBetaEnabled(CONST.BETAS.VENDOR_MATCHING)); + const isColumnAvailable = (column: SearchCustomColumnIds) => isVendorColumnAvailable || column !== CONST.SEARCH.TABLE_COLUMNS.VENDOR; // Wait for transactions to load before rendering. ColumnsSettingsList snapshots // currentColumns in useState on mount and does not sync prop updates, so we must @@ -100,12 +106,14 @@ function ReportDetailsColumnsPage() { return visibleColumns.filter(isReportDetailsCustomColumn); }, [reportDetailsColumns, reportTransactions, currentUserDetails?.accountID, report, policy]); + const allColumns = ALL_REPORT_DETAILS_CUSTOM_COLUMNS.filter(isColumnAvailable); + const currentColumns = effectiveColumns.filter(isColumnAvailable); const requiredColumns = new Set([CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]); const handleSave = (selectedColumnIds: SearchCustomColumnIds[]) => { // Skip saving if columns haven't changed from the effective state, to avoid // switching from the default path to the custom path in getColumnsToShow unnecessarily. - if (!arraysEqual(selectedColumnIds, effectiveColumns)) { + if (!arraysEqual(selectedColumnIds, currentColumns)) { setReportDetailsColumns(selectedColumnIds, reportDetailsColumns); } Navigation.goBack(); @@ -117,10 +125,11 @@ function ReportDetailsColumnsPage() { return ( ); diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 314ec3de6b00..4cd604c0f0e0 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -2148,6 +2148,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ case CONST.SEARCH.TABLE_COLUMNS.REPORT_ID: case CONST.SEARCH.TABLE_COLUMNS.BASE_62_REPORT_ID: case CONST.SEARCH.TABLE_COLUMNS.MERCHANT: + case CONST.SEARCH.TABLE_COLUMNS.VENDOR: case CONST.SEARCH.TABLE_COLUMNS.FROM: case CONST.SEARCH.TABLE_COLUMNS.TO: case CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER: diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index ee9ccad7691d..2599daba5b5a 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -15,6 +15,8 @@ import { canSendInvoiceFromWorkspace, evaluateApprovalWorkflowRule, findVendorByID, + getVendorDisplayName, + getVendorSearchAvailability, getActivePolicies, getActivePoliciesWithExpenseChat, getActivePoliciesWithExpenseChatAndPerDiemEnabled, @@ -4613,6 +4615,55 @@ describe('PolicyUtils', () => { }); }); + describe('getVendorSearchAvailability', () => { + const qboPolicy: Policy = {...buildQBOPolicy(CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD), id: 'qbo'}; + const xeroPolicy: Policy = {...buildXeroPolicy(), id: 'xero'}; + const plainPolicy: Policy = {...createRandomPolicy(3), connections: undefined, id: 'plain'}; + const qboKey = `${ONYXKEYS.COLLECTION.POLICY}qbo`; + const xeroKey = `${ONYXKEYS.COLLECTION.POLICY}xero`; + const plainKey = `${ONYXKEYS.COLLECTION.POLICY}plain`; + + it('is unavailable when no workspace has the vendor feature', () => { + expect(getVendorSearchAvailability({[plainKey]: plainPolicy}, true)).toEqual({isAvailable: false, shouldUseSupplierLabel: false}); + }); + + it('is available with the vendor label for a QBO workspace exporting card expenses as credit card transactions, without the beta', () => { + expect(getVendorSearchAvailability({[qboKey]: qboPolicy, [plainKey]: plainPolicy}, false)).toEqual({isAvailable: true, shouldUseSupplierLabel: false}); + }); + + it('uses the supplier label when every eligible workspace takes its vendors from Xero', () => { + expect(getVendorSearchAvailability({[xeroKey]: xeroPolicy, [plainKey]: plainPolicy}, true)).toEqual({isAvailable: true, shouldUseSupplierLabel: true}); + }); + + it('keeps the vendor label when Xero and QBO workspaces are both eligible', () => { + expect(getVendorSearchAvailability({[xeroKey]: xeroPolicy, [qboKey]: qboPolicy}, true)).toEqual({isAvailable: true, shouldUseSupplierLabel: false}); + }); + + it('ignores beta-gated integrations while the beta is off', () => { + expect(getVendorSearchAvailability({[xeroKey]: xeroPolicy}, false).isAvailable).toBe(false); + }); + }); + + describe('getVendorDisplayName', () => { + const qboPolicy = buildQBOPolicy(CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD); + + it('returns an empty string when no vendor is assigned', () => { + expect(getVendorDisplayName(qboPolicy, undefined)).toBe(''); + }); + + it('prefers the synced vendor name over the name stored on the transaction', () => { + expect(getVendorDisplayName(qboPolicy, {externalID: 'v-1', name: 'Old Acme', wasManuallySet: true})).toBe('Acme Co'); + }); + + it('falls back to the stored name when the vendor is no longer in the synced list', () => { + expect(getVendorDisplayName(qboPolicy, {externalID: 'gone', name: 'Former Vendor', wasManuallySet: true})).toBe('Former Vendor'); + }); + + it('returns an empty string for a legacy vendor with neither a synced nor a stored name', () => { + expect(getVendorDisplayName(undefined, {externalID: 'v-9', wasManuallySet: true})).toBe(''); + }); + }); + describe('findVendorByID', () => { it('resolves a QBO vendor even when the current export mode is no longer vendor-matching (Vendor Bill)', () => { const policy = buildQBOPolicy(CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL, [{id: 'v-1', name: 'Acme', currency: 'USD'}]); diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 8792aaed1d16..c0646bcd59e0 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -48,6 +48,7 @@ import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import createRandomPolicy from '../../utils/collections/policies'; +import createRandomTransaction from '../../utils/collections/transaction'; import createMock from '../../utils/createMock'; import getOnyxValue from '../../utils/getOnyxValue'; import {convertToDisplayString, formatPhoneNumber, getCurrencyDecimalsLocal, localeCompare, translateLocal} from '../../utils/TestHelper'; @@ -11089,6 +11090,21 @@ describe('SearchUIUtils', () => { }); describe('Test getColumnsToShow', () => { + test('Should show the vendor column only when an expense has a vendor assigned', () => { + const transactionWithoutVendor = createRandomTransaction(1); + const transactionWithVendor = {...createRandomTransaction(2), comment: {vendor: {externalID: 'qbo-1', name: 'Acme Tools', wasManuallySet: true}}}; + + expect(SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [transactionWithoutVendor], visibleColumns: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE})).not.toContain( + CONST.SEARCH.TABLE_COLUMNS.VENDOR, + ); + expect(SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [transactionWithVendor], visibleColumns: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE})).toContain( + CONST.SEARCH.TABLE_COLUMNS.VENDOR, + ); + expect( + SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [transactionWithVendor], visibleColumns: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE, isExpenseReportView: true}), + ).toContain(CONST.SEARCH.TABLE_COLUMNS.VENDOR); + }); + test('Should show all default columns when no custom columns are saved & viewing expense reports', () => { expect(SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [], visibleColumns: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT})).toEqual([ CONST.SEARCH.TABLE_COLUMNS.AVATAR, @@ -13966,6 +13982,17 @@ describe('SearchUIUtils', () => { }); }); + describe('vendor column label', () => { + test('Should label the vendor column as Vendor by default and as Supplier for Xero-only workspaces', () => { + expect(SearchUIUtils.getSearchColumnTranslationKey(CONST.SEARCH.TABLE_COLUMNS.VENDOR)).toBe('common.vendor'); + expect(SearchUIUtils.getSearchColumnTranslationKey(CONST.SEARCH.TABLE_COLUMNS.VENDOR, true)).toBe('common.supplier'); + }); + + test('Should leave other labels untouched when the supplier wording is on', () => { + expect(SearchUIUtils.getSearchColumnTranslationKey(CONST.SEARCH.TABLE_COLUMNS.CATEGORY, true)).toBe('common.category'); + }); + }); + describe('getDisplayValue', () => { test('returns translated has option labels from getHasOptions', () => { const result = SearchUIUtils.getDisplayValue('has', {has: [CONST.SEARCH.HAS_VALUES.SUBMITTED_VIOLATION]}, CONST.SEARCH.DATA_TYPES.EXPENSE, translateLocal, localeCompare); diff --git a/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap b/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap index d415876f37bb..c85a7d23b544 100644 --- a/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap +++ b/tests/unit/Search/__snapshots__/ColumnAvailabilityTest.ts.snap @@ -6,6 +6,7 @@ exports[`Column availability single source of truth the derived picker lists mat "date", "posted", "merchant", + "vendor", "description", "card", "category", @@ -41,6 +42,7 @@ exports[`Column availability single source of truth the derived picker lists mat "posted", "exported", "merchant", + "vendor", "description", "from", "to", From e15eaa009e4da5be72c619c4af48bdd90f9cdbc2 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Thu, 17 Sep 2026 16:49:37 +0530 Subject: [PATCH 2/7] Move VendorSearchAvailability type into the PolicyUtils type block --- src/libs/PolicyUtils.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index d6ad51dd519f..dc72380636bf 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -108,6 +108,14 @@ type ApprovalWorkflowRuleMatch = { forwardsTo?: string; }; +type VendorSearchAvailability = { + /** Whether at least one workspace the user can see has the vendor feature, so Search can offer the vendor column. */ + isAvailable: boolean; + + /** Whether every eligible workspace takes its vendors from Xero, which calls them suppliers. */ + shouldUseSupplierLabel: boolean; +}; + /** * Returns true if the policy has no fieldList or its fieldList is empty. */ @@ -2726,14 +2734,6 @@ function hasVendorFeature(policy: OnyxEntry, isVendorMatchingBetaEnabled ); } -type VendorSearchAvailability = { - /** Whether at least one workspace the user can see has the vendor feature, so Search can offer the vendor column. */ - isAvailable: boolean; - - /** Whether every eligible workspace takes its vendors from Xero, which calls them suppliers. */ - shouldUseSupplierLabel: boolean; -}; - /** * Search spans every workspace at once, so the vendor column is offered when any workspace has the vendor feature, * and the "Supplier" wording is only used when no eligible workspace would call them vendors. From 6a95b61e72e59bfb29e079a0529af6010c6c7222 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Thu, 17 Sep 2026 17:22:56 +0530 Subject: [PATCH 3/7] Tighten the getVendorDisplayName comment --- src/libs/PolicyUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index dc72380636bf..1db762632dac 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -2939,9 +2939,9 @@ function findVendorByID(policy: OnyxEntry, vendorID: string | undefined) } /** - * Display name of the vendor assigned to a transaction, or an empty string when none is assigned. The workspace's - * synced vendor list wins so renames in the accounting system show through, then the name persisted on the - * transaction when the vendor was assigned covers vendors that have since left the synced list. + * Display name of a transaction's vendor, or an empty string when none is assigned. The workspace's synced vendor list + * wins so renames in the accounting system show through. The name stored on the transaction covers vendors since + * removed from that list. */ function getVendorDisplayName(policy: OnyxEntry, vendor: TransactionCommentVendor | undefined): string { if (!vendor?.externalID) { From 7470a2cb1301831b834401798cc1e7fb94a83c2f Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Thu, 17 Sep 2026 17:53:31 +0530 Subject: [PATCH 4/7] Show the Vendor column on Search only when picked --- src/libs/SearchUIUtils.ts | 8 ++++---- tests/unit/Search/SearchUIUtilsTest.ts | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 1e607081f927..3d39900c784c 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -6525,10 +6525,6 @@ function getColumnsToShow({ columns[CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION] = true; } - if (transaction.comment?.vendor?.externalID) { - columns[CONST.SEARCH.TABLE_COLUMNS.VENDOR] = true; - } - const hasCategory = (() => { const category = getCategory(transaction); return category !== '' && category !== CONST.SEARCH.CATEGORY_EMPTY_VALUE; @@ -6573,6 +6569,10 @@ function getColumnsToShow({ columns[CONST.SEARCH.TABLE_COLUMNS.CARD] = true; } + if (transaction.comment?.vendor?.externalID) { + columns[CONST.SEARCH.TABLE_COLUMNS.VENDOR] = true; + } + // Only show tax columns when the user explicitly chooses to display them. if (customResult) { // Show both TAX_RATE and TAX_AMOUNT when the transaction has a meaningful tax signal. diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index c0646bcd59e0..90db2dd63bc8 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -11090,16 +11090,20 @@ describe('SearchUIUtils', () => { }); describe('Test getColumnsToShow', () => { - test('Should show the vendor column only when an expense has a vendor assigned', () => { + test('Should show the vendor column on Search only when picked, and in the report view when an expense has a vendor assigned', () => { const transactionWithoutVendor = createRandomTransaction(1); const transactionWithVendor = {...createRandomTransaction(2), comment: {vendor: {externalID: 'qbo-1', name: 'Acme Tools', wasManuallySet: true}}}; + const pickedColumns = [CONST.SEARCH.TABLE_COLUMNS.DATE, CONST.SEARCH.TABLE_COLUMNS.VENDOR, CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]; - expect(SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [transactionWithoutVendor], visibleColumns: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE})).not.toContain( + expect(SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [transactionWithVendor], visibleColumns: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE})).not.toContain( CONST.SEARCH.TABLE_COLUMNS.VENDOR, ); - expect(SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [transactionWithVendor], visibleColumns: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE})).toContain( + expect(SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [transactionWithVendor], visibleColumns: pickedColumns, type: CONST.SEARCH.DATA_TYPES.EXPENSE})).toContain( CONST.SEARCH.TABLE_COLUMNS.VENDOR, ); + expect( + SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [transactionWithoutVendor], visibleColumns: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE, isExpenseReportView: true}), + ).not.toContain(CONST.SEARCH.TABLE_COLUMNS.VENDOR); expect( SearchUIUtils.getColumnsToShow({currentAccountID: 1, data: [transactionWithVendor], visibleColumns: [], type: CONST.SEARCH.DATA_TYPES.EXPENSE, isExpenseReportView: true}), ).toContain(CONST.SEARCH.TABLE_COLUMNS.VENDOR); From 3267b865f8ab36de316f5bbb7b4f811717a040eb Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Thu, 17 Sep 2026 18:11:40 +0530 Subject: [PATCH 5/7] Hide the saved Vendor column on reports whose workspace lacks the vendor feature --- .../MoneyRequestReportTransactionList.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 2945401d0c29..c3db0a3e97d4 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -22,6 +22,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNavigateToTransactionThread from '@hooks/useNavigateToTransactionThread'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; @@ -36,7 +37,7 @@ import {resolveTransactionCardFields} from '@libs/CardUtils'; import {isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils'; import {navigationRef} from '@libs/Navigation/Navigation'; import {getDistanceExpenseTypeForPolicy} from '@libs/PolicyDistanceRatesUtils'; -import {isPolicyTaxEnabled, isXeroActiveMatchingSource} from '@libs/PolicyUtils'; +import {hasVendorFeature, isPolicyTaxEnabled, isXeroActiveMatchingSource} from '@libs/PolicyUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {groupTransactionsByCategory, groupTransactionsByTag} from '@libs/ReportLayoutUtils'; import { @@ -291,6 +292,7 @@ function MoneyRequestReportTransactionList({ const longPressModalRef = useRef(null); const {reportPendingAction} = getReportOfflinePendingActionAndErrors(report); const {isOffline} = useNetwork(); + const {isBetaEnabled} = usePermissions(); const isTaxEnabled = isPolicyTaxEnabled(policy); const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = getMoneyRequestSpendBreakdown(report); @@ -518,12 +520,15 @@ function MoneyRequestReportTransactionList({ const isExpenseReportViewFromIOUReport = isIOUReport(report); const shouldShowBillableColumn = isBillableEnabledOnPolicy(policy); const shouldShowCommentsColumn = useMemo(() => Object.values(reportActions ?? {}).some((action) => (action?.childVisibleActionCount ?? 0) > 0), [reportActions]); + // The saved column list is account-wide, so drop the vendor column on reports whose workspace lacks the vendor feature. + const isVendorColumnAvailable = hasVendorFeature(policy, isBetaEnabled(CONST.BETAS.VENDOR_MATCHING)); const columnsToShow = useMemo(() => { + const savedColumns = (reportDetailsColumns ?? []).filter((column) => isVendorColumnAvailable || column !== CONST.SEARCH.TABLE_COLUMNS.VENDOR); return getColumnsToShow({ currentAccountID: currentUserDetails?.accountID, data: transactions, report, - visibleColumns: (isExpenseReportViewFromIOUReport ? [] : (reportDetailsColumns ?? [])) as SearchCustomColumnIds[], + visibleColumns: (isExpenseReportViewFromIOUReport ? [] : savedColumns) as SearchCustomColumnIds[], isExpenseReportView: true, isExpenseReportViewFromIOUReport, shouldShowBillableColumn, @@ -539,6 +544,7 @@ function MoneyRequestReportTransactionList({ shouldShowBillableColumn, shouldShowCommentsColumn, reportDetailsColumns, + isVendorColumnAvailable, report, isTaxEnabled, shouldShowExpenseReportBreakDown, From 4e26954d6b793cf4ed7047bb574cc21f1d05d75f Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Fri, 18 Sep 2026 21:48:30 +0530 Subject: [PATCH 6/7] Label the vendor column Vendor on every workspace Product standardised on "Vendor" across integrations, so the Search header, the report table header, and both Columns pickers no longer switch to "Supplier" for Xero workspaces. --- src/components/ColumnsSettingsList.tsx | 25 ++++------------- .../MoneyRequestReportTableHeader.tsx | 8 +----- .../MoneyRequestReportTableHeaderRow.tsx | 5 ---- .../MoneyRequestReportTransactionList.tsx | 3 +- src/components/Search/SearchTableHeader.tsx | 19 ++----------- src/libs/PolicyUtils.ts | 28 +++---------------- src/libs/SearchUIUtils.ts | 7 ++--- src/pages/Search/SearchColumnsPage.tsx | 8 ++---- .../Report/ReportDetailsColumnsPage.tsx | 3 +- tests/unit/PolicyUtilsTest.ts | 22 ++++++--------- tests/unit/Search/SearchUIUtilsTest.ts | 7 +---- 11 files changed, 30 insertions(+), 105 deletions(-) diff --git a/src/components/ColumnsSettingsList.tsx b/src/components/ColumnsSettingsList.tsx index 41007c9b5981..42889d995cd9 100644 --- a/src/components/ColumnsSettingsList.tsx +++ b/src/components/ColumnsSettingsList.tsx @@ -65,24 +65,11 @@ type ColumnsSettingsListProps = { /** The default columns for the active group-by mode when no customization has been applied */ defaultGroupColumns?: SearchCustomColumnIds[]; - /** Whether the vendor column is labelled "Supplier", which is what Xero calls vendors */ - shouldUseSupplierLabel?: boolean; - /** Callback fired with the updated column list when the user saves changes */ onSave: (columns: SearchCustomColumnIds[]) => void; }; -function ColumnsSettingsList({ - allColumns, - defaultSelectedColumns, - currentColumns, - requiredColumns, - groupBy, - groupColumns = [], - defaultGroupColumns = [], - shouldUseSupplierLabel = false, - onSave, -}: ColumnsSettingsListProps) { +function ColumnsSettingsList({allColumns, defaultSelectedColumns, currentColumns, requiredColumns, groupBy, groupColumns = [], defaultGroupColumns = [], onSave}: ColumnsSettingsListProps) { const theme = useTheme(); const styles = useThemeStyles(); const icons = useMemoizedLazyExpensifyIcons(['DragHandles']); @@ -98,8 +85,8 @@ function ColumnsSettingsList({ const unselected = columnsToSort .filter((col) => !col.isSelected) .sort((a, b) => { - const textA = translate(getSearchColumnTranslationKey(a.value, shouldUseSupplierLabel)); - const textB = translate(getSearchColumnTranslationKey(b.value, shouldUseSupplierLabel)); + const textA = translate(getSearchColumnTranslationKey(a.value)); + const textB = translate(getSearchColumnTranslationKey(b.value)); return localeCompare(textA, textB); }); return [...selected, ...unselected]; @@ -131,7 +118,7 @@ function ColumnsSettingsList({ const isEffectivelySelected = isRequired || isSelected; const isDragDisabled = !isEffectivelySelected; return { - text: translate(getSearchColumnTranslationKey(columnId, shouldUseSupplierLabel)), + text: translate(getSearchColumnTranslationKey(columnId)), value: columnId, keyForList: columnId, isSelected: isEffectivelySelected, @@ -177,8 +164,8 @@ function ColumnsSettingsList({ const selectedCols = prevColumns.filter((col) => col.isSelected); const unselected = prevColumns.filter((col) => !col.isSelected && col.columnId !== updatedColumnId); const unselectedSorted = unselected.sort((a, b) => { - const textA = translate(getSearchColumnTranslationKey(a.columnId, shouldUseSupplierLabel)); - const textB = translate(getSearchColumnTranslationKey(b.columnId, shouldUseSupplierLabel)); + const textA = translate(getSearchColumnTranslationKey(a.columnId)); + const textB = translate(getSearchColumnTranslationKey(b.columnId)); return localeCompare(textA, textB); }); return [...selectedCols, {columnId: updatedColumnId, isSelected: true}, ...unselectedSorted]; diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx index cc9bab2c5d0a..d328b2b08a9e 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx @@ -5,7 +5,6 @@ import type {SearchColumnType, SearchSortBy, SortOrder, TableColumnSize} from '@ import useThemeStyles from '@hooks/useThemeStyles'; import {isSortableColumnName} from '@libs/ReportUtils'; -import {getSearchColumnTranslationKey} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; @@ -23,9 +22,6 @@ type SearchTableHeaderProps = { shouldShowSorting: boolean; columns: SearchColumnType[]; shouldRemoveTotalColumnFlex?: boolean; - - /** Whether the vendor column is labelled "Supplier", which is what Xero calls vendors */ - shouldUseSupplierLabel?: boolean; }; function MoneyRequestReportTableHeader({ sortBy, @@ -38,7 +34,6 @@ function MoneyRequestReportTableHeader({ amountColumnSize, taxAmountColumnSize, shouldRemoveTotalColumnFlex, - shouldUseSupplierLabel = false, }: SearchTableHeaderProps) { const styles = useThemeStyles(); @@ -46,7 +41,6 @@ function MoneyRequestReportTableHeader({ () => [ ...getExpenseHeaders().map((header) => ({ ...header, - translationKey: header.columnName === CONST.SEARCH.TABLE_COLUMNS.VENDOR ? getSearchColumnTranslationKey(header.columnName, shouldUseSupplierLabel) : header.translationKey, isColumnSortable: isSortableColumnName(header.columnName), })), { @@ -55,7 +49,7 @@ function MoneyRequestReportTableHeader({ isColumnSortable: false, }, ], - [shouldUseSupplierLabel], + [], ); const orderedColumnConfig = useMemo(() => { diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeaderRow.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeaderRow.tsx index b5437388cf06..d36e6fc79b24 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeaderRow.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeaderRow.tsx @@ -34,9 +34,6 @@ type MoneyRequestReportTableHeaderRowProps = { /** The columns the table renders, in order */ columns: SearchColumnType[]; - /** Whether the vendor column is labelled "Supplier", which is what Xero calls vendors */ - shouldUseSupplierLabel?: boolean; - /** The column the table is currently sorted by */ sortBy: SortableColumnName; @@ -69,7 +66,6 @@ function MoneyRequestReportTableHeaderRow({ transactions, pendingAction, columns, - shouldUseSupplierLabel = false, sortBy, sortOrder, onSortPress, @@ -137,7 +133,6 @@ function MoneyRequestReportTableHeaderRow({ sortOrder={sortOrder} shouldRemoveTotalColumnFlex={hasFlexColumn(columns)} columns={columns} - shouldUseSupplierLabel={shouldUseSupplierLabel} dateColumnSize={dateColumnSize} postedColumnSize={postedColumnSize} amountColumnSize={amountColumnSize} diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index ea0d9287ad83..a50c26142ada 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -24,7 +24,7 @@ import {clearActiveTransactionIDs, getActiveTransactionIDs, setActiveTransaction import {resolveTransactionCardFields} from '@libs/CardUtils'; import {isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils'; import {navigationRef} from '@libs/Navigation/Navigation'; -import {hasVendorFeature, isPolicyTaxEnabled, isXeroActiveMatchingSource} from '@libs/PolicyUtils'; +import {hasVendorFeature, isPolicyTaxEnabled} from '@libs/PolicyUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {groupTransactionsByCategory, groupTransactionsByTag} from '@libs/ReportLayoutUtils'; import { @@ -818,7 +818,6 @@ function MoneyRequestReportTransactionList({ transactions={transactions} pendingAction={reportPendingAction} columns={columnsToShow} - shouldUseSupplierLabel={isXeroActiveMatchingSource(policy)} sortBy={sortBy} sortOrder={sortOrder} onSortPress={(selectedSortBy, selectedSortOrder) => { diff --git a/src/components/Search/SearchTableHeader.tsx b/src/components/Search/SearchTableHeader.tsx index b452dc6b2041..93cdab2a6dfe 100644 --- a/src/components/Search/SearchTableHeader.tsx +++ b/src/components/Search/SearchTableHeader.tsx @@ -1,20 +1,12 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getVendorSearchAvailability} from '@libs/PolicyUtils'; -import {getSearchColumnTranslationKey} from '@libs/SearchUIUtils'; - import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy} from '@src/types/onyx'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {OnyxCollection} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import React, {useCallback, useMemo} from 'react'; @@ -585,11 +577,6 @@ function SearchTableHeader({ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth; - const {isBetaEnabled} = usePermissions(); - const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); - const [shouldUseSupplierLabel = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { - selector: (allPolicies: OnyxCollection) => getVendorSearchAvailability(allPolicies, isVendorMatchingBetaEnabled).shouldUseSupplierLabel, - }); // Only load Profile icon when it's needed for EXPENSE_REPORT type or grouped transactions const icons = useMemoizedLazyExpensifyIcons(type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT || !!groupBy ? ['Profile', 'Bank', 'CreditCard'] : []) satisfies SearchHeaderIcons; @@ -619,9 +606,7 @@ function SearchTableHeader({ for (const col of columns) { const config = configMap.get(col); if (config) { - orderedConfig.push( - config.columnName === CONST.SEARCH.TABLE_COLUMNS.VENDOR ? {...config, translationKey: getSearchColumnTranslationKey(config.columnName, shouldUseSupplierLabel)} : config, - ); + orderedConfig.push(config); addedColumns.add(col); } } @@ -633,7 +618,7 @@ function SearchTableHeader({ } return orderedConfig; - }, [columnConfig, columns, shouldUseSupplierLabel]); + }, [columnConfig, columns]); if (displayNarrowVersion) { return; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 1aa2b19611cc..475330e90757 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -108,14 +108,6 @@ type ApprovalWorkflowRuleMatch = { forwardsTo?: string; }; -type VendorSearchAvailability = { - /** Whether at least one workspace the user can see has the vendor feature, so Search can offer the vendor column. */ - isAvailable: boolean; - - /** Whether every eligible workspace takes its vendors from Xero, which calls them suppliers. */ - shouldUseSupplierLabel: boolean; -}; - /** * Returns true if the policy has no fieldList or its fieldList is empty. */ @@ -2742,22 +2734,10 @@ function hasVendorFeature(policy: OnyxEntry, isVendorMatchingBetaEnabled } /** - * Search spans every workspace at once, so the vendor column is offered when any workspace has the vendor feature, - * and the "Supplier" wording is only used when no eligible workspace would call them vendors. + * Search spans every workspace at once, so the vendor column is offered when any workspace has the vendor feature. */ -function getVendorSearchAvailability(policies: OnyxCollection, isVendorMatchingBetaEnabled: boolean): VendorSearchAvailability { - let isAvailable = false; - let areAllEligiblePoliciesXero = true; - for (const policy of Object.values(policies ?? {})) { - if (!hasVendorFeature(policy, isVendorMatchingBetaEnabled)) { - continue; - } - isAvailable = true; - if (!isXeroActiveMatchingSource(policy)) { - areAllEligiblePoliciesXero = false; - } - } - return {isAvailable, shouldUseSupplierLabel: isAvailable && areAllEligiblePoliciesXero}; +function hasVendorFeatureOnAnyPolicy(policies: OnyxCollection, isVendorMatchingBetaEnabled: boolean): boolean { + return Object.values(policies ?? {}).some((policy) => hasVendorFeature(policy, isVendorMatchingBetaEnabled)); } /** @@ -3537,7 +3517,7 @@ export { isXeroActiveMatchingSource, isXeroVendorMatchingActive, hasVendorFeature, - getVendorSearchAvailability, + hasVendorFeatureOnAnyPolicy, isMatchingVendorListLoaded, getValidConnectedIntegration, getCountOfEnabledTagsOfList, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 855118ffbe04..a8c426554884 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -4307,10 +4307,7 @@ function getCustomColumnDefault(value?: SearchDataTypes | SearchGroupBy): Search } } -/** - * Xero calls vendors suppliers, so the vendor column reads "Supplier" when every eligible workspace takes its vendors from Xero. - */ -function getSearchColumnTranslationKey(column: SearchSortBy, shouldUseSupplierLabel = false): TranslationPaths { +function getSearchColumnTranslationKey(column: SearchSortBy): TranslationPaths { switch (column) { case CONST.SEARCH.TABLE_COLUMNS.AVATAR: return 'common.avatar'; @@ -4333,7 +4330,7 @@ function getSearchColumnTranslationKey(column: SearchSortBy, shouldUseSupplierLa case CONST.SEARCH.TABLE_COLUMNS.MERCHANT: return 'common.merchant'; case CONST.SEARCH.TABLE_COLUMNS.VENDOR: - return shouldUseSupplierLabel ? 'common.supplier' : 'common.vendor'; + return 'common.vendor'; case CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION: return 'common.description'; case CONST.SEARCH.TABLE_COLUMNS.FROM: diff --git a/src/pages/Search/SearchColumnsPage.tsx b/src/pages/Search/SearchColumnsPage.tsx index 8e222ba8a8c0..bc133ee88116 100644 --- a/src/pages/Search/SearchColumnsPage.tsx +++ b/src/pages/Search/SearchColumnsPage.tsx @@ -6,7 +6,7 @@ import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import Navigation from '@libs/Navigation/Navigation'; -import {getVendorSearchAvailability} from '@libs/PolicyUtils'; +import {hasVendorFeatureOnAnyPolicy} from '@libs/PolicyUtils'; import {buildQueryStringFromFilterFormValues, getCurrentSearchQueryJSON, hasValuesIncludeViolationFilter} from '@libs/SearchQueryUtils'; import {getCustomColumnDefault, getCustomColumns, insertColumnBeforeTotalAmount} from '@libs/SearchUIUtils'; @@ -25,10 +25,9 @@ function SearchColumnsPage() { const {currentSearchKey} = useSearchQueryContext(); const {isBetaEnabled} = usePermissions(); const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); - const [vendorSearchAvailability] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { - selector: (allPolicies: OnyxCollection) => getVendorSearchAvailability(allPolicies, isVendorMatchingBetaEnabled), + const [isVendorColumnAvailable = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { + selector: (allPolicies: OnyxCollection) => hasVendorFeatureOnAnyPolicy(allPolicies, isVendorMatchingBetaEnabled), }); - const isVendorColumnAvailable = vendorSearchAvailability?.isAvailable ?? false; const groupBy = searchAdvancedFiltersForm?.groupBy; const queryType = searchAdvancedFiltersForm?.type ?? CONST.SEARCH.DATA_TYPES.EXPENSE; @@ -97,7 +96,6 @@ function SearchColumnsPage() { groupBy={groupBy} groupColumns={allGroupCustomColumns} defaultGroupColumns={defaultGroupCustomColumns} - shouldUseSupplierLabel={vendorSearchAvailability?.shouldUseSupplierLabel} onSave={applyChanges} /> ); diff --git a/src/pages/settings/Report/ReportDetailsColumnsPage.tsx b/src/pages/settings/Report/ReportDetailsColumnsPage.tsx index 73bea8de328e..2428e21379e2 100644 --- a/src/pages/settings/Report/ReportDetailsColumnsPage.tsx +++ b/src/pages/settings/Report/ReportDetailsColumnsPage.tsx @@ -9,7 +9,7 @@ import {setReportDetailsColumns} from '@libs/actions/ReportLayout'; import {isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import {hasVendorFeature, isPolicyTaxEnabled, isXeroActiveMatchingSource} from '@libs/PolicyUtils'; +import {hasVendorFeature, isPolicyTaxEnabled} from '@libs/PolicyUtils'; import {isIOUReport} from '@libs/ReportUtils'; import {getColumnsToShow} from '@libs/SearchUIUtils'; import {hasNonReimbursableTransactions} from '@libs/TransactionUtils'; @@ -129,7 +129,6 @@ function ReportDetailsColumnsPage() { defaultSelectedColumns={REPORT_DETAILS_DEFAULT_COLUMNS} currentColumns={currentColumns} requiredColumns={requiredColumns} - shouldUseSupplierLabel={isXeroActiveMatchingSource(policy)} onSave={handleSave} /> ); diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index c04066044f01..c123ac79551b 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -16,7 +16,7 @@ import { evaluateApprovalWorkflowRule, findVendorByID, getVendorDisplayName, - getVendorSearchAvailability, + hasVendorFeatureOnAnyPolicy, getActivePolicies, getActivePoliciesWithExpenseChat, getActivePoliciesWithExpenseChatAndPerDiemEnabled, @@ -4623,7 +4623,7 @@ describe('PolicyUtils', () => { }); }); - describe('getVendorSearchAvailability', () => { + describe('hasVendorFeatureOnAnyPolicy', () => { const qboPolicy: Policy = {...buildQBOPolicy(CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD), id: 'qbo'}; const xeroPolicy: Policy = {...buildXeroPolicy(), id: 'xero'}; const plainPolicy: Policy = {...createRandomPolicy(3), connections: undefined, id: 'plain'}; @@ -4631,24 +4631,20 @@ describe('PolicyUtils', () => { const xeroKey = `${ONYXKEYS.COLLECTION.POLICY}xero`; const plainKey = `${ONYXKEYS.COLLECTION.POLICY}plain`; - it('is unavailable when no workspace has the vendor feature', () => { - expect(getVendorSearchAvailability({[plainKey]: plainPolicy}, true)).toEqual({isAvailable: false, shouldUseSupplierLabel: false}); + it('is false when no workspace has the vendor feature', () => { + expect(hasVendorFeatureOnAnyPolicy({[plainKey]: plainPolicy}, true)).toBe(false); }); - it('is available with the vendor label for a QBO workspace exporting card expenses as credit card transactions, without the beta', () => { - expect(getVendorSearchAvailability({[qboKey]: qboPolicy, [plainKey]: plainPolicy}, false)).toEqual({isAvailable: true, shouldUseSupplierLabel: false}); + it('is true for a QBO workspace exporting card expenses as credit card transactions, without the beta', () => { + expect(hasVendorFeatureOnAnyPolicy({[qboKey]: qboPolicy, [plainKey]: plainPolicy}, false)).toBe(true); }); - it('uses the supplier label when every eligible workspace takes its vendors from Xero', () => { - expect(getVendorSearchAvailability({[xeroKey]: xeroPolicy, [plainKey]: plainPolicy}, true)).toEqual({isAvailable: true, shouldUseSupplierLabel: true}); - }); - - it('keeps the vendor label when Xero and QBO workspaces are both eligible', () => { - expect(getVendorSearchAvailability({[xeroKey]: xeroPolicy, [qboKey]: qboPolicy}, true)).toEqual({isAvailable: true, shouldUseSupplierLabel: false}); + it('is true for a Xero workspace with the beta', () => { + expect(hasVendorFeatureOnAnyPolicy({[xeroKey]: xeroPolicy, [plainKey]: plainPolicy}, true)).toBe(true); }); it('ignores beta-gated integrations while the beta is off', () => { - expect(getVendorSearchAvailability({[xeroKey]: xeroPolicy}, false).isAvailable).toBe(false); + expect(hasVendorFeatureOnAnyPolicy({[xeroKey]: xeroPolicy}, false)).toBe(false); }); }); diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index bd66dd756ba7..de756d9eacca 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -13988,13 +13988,8 @@ describe('SearchUIUtils', () => { }); describe('vendor column label', () => { - test('Should label the vendor column as Vendor by default and as Supplier for Xero-only workspaces', () => { + test('Should label the vendor column as Vendor', () => { expect(SearchUIUtils.getSearchColumnTranslationKey(CONST.SEARCH.TABLE_COLUMNS.VENDOR)).toBe('common.vendor'); - expect(SearchUIUtils.getSearchColumnTranslationKey(CONST.SEARCH.TABLE_COLUMNS.VENDOR, true)).toBe('common.supplier'); - }); - - test('Should leave other labels untouched when the supplier wording is on', () => { - expect(SearchUIUtils.getSearchColumnTranslationKey(CONST.SEARCH.TABLE_COLUMNS.CATEGORY, true)).toBe('common.category'); }); }); From 6f79486332705f6b2298afb08fb400f3f3861e4c Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Fri, 18 Sep 2026 21:57:50 +0530 Subject: [PATCH 7/7] Memoize the vendor column availability selector --- src/pages/Search/SearchColumnsPage.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/Search/SearchColumnsPage.tsx b/src/pages/Search/SearchColumnsPage.tsx index bc133ee88116..6af89ca55bd7 100644 --- a/src/pages/Search/SearchColumnsPage.tsx +++ b/src/pages/Search/SearchColumnsPage.tsx @@ -18,16 +18,18 @@ import type {Policy} from '@src/types/onyx'; import type {OnyxCollection} from 'react-native-onyx'; -import React from 'react'; +import React, {useCallback} from 'react'; function SearchColumnsPage() { const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); const {currentSearchKey} = useSearchQueryContext(); const {isBetaEnabled} = usePermissions(); const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); - const [isVendorColumnAvailable = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { - selector: (allPolicies: OnyxCollection) => hasVendorFeatureOnAnyPolicy(allPolicies, isVendorMatchingBetaEnabled), - }); + const isVendorColumnAvailableSelector = useCallback( + (allPolicies: OnyxCollection) => hasVendorFeatureOnAnyPolicy(allPolicies, isVendorMatchingBetaEnabled), + [isVendorMatchingBetaEnabled], + ); + const [isVendorColumnAvailable = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: isVendorColumnAvailableSelector}); const groupBy = searchAdvancedFiltersForm?.groupBy; const queryType = searchAdvancedFiltersForm?.type ?? CONST.SEARCH.DATA_TYPES.EXPENSE;