Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ function useMoneyRequestReportColumns({report, policy, transactions, reportActio
const isExpenseReportViewFromIOUReport = isIOUReport(report);
const shouldShowBillableColumn = isBillableEnabledOnPolicy(policy);
const shouldShowCommentsColumn = Object.values(reportActions ?? {}).some((action) => (action?.childVisibleActionCount ?? 0) > 0);
// The saved column list is account-wide, so drop the vendor column on reports whose workspace lacks the vendor feature.
// The saved column list is account-wide, so the vendor column only shows on reports whose workspace has the vendor feature.
const isVendorColumnAvailable = hasVendorFeature(policy, isBetaEnabled(CONST.BETAS.VENDOR_MATCHING));
const savedColumns = (reportDetailsColumns ?? []).filter(isReportDetailsCustomColumn).filter((column) => isVendorColumnAvailable || column !== CONST.SEARCH.TABLE_COLUMNS.VENDOR);
const savedColumns = (reportDetailsColumns ?? []).filter(isReportDetailsCustomColumn);
const columnsToShow = getColumnsToShow({
currentAccountID: currentUserDetails?.accountID,
data: transactions,
Expand All @@ -89,6 +89,7 @@ function useMoneyRequestReportColumns({report, policy, transactions, reportActio
shouldShowReimbursableColumn: hasNonReimbursableTransactions(transactions),
reportCurrency: report?.currency,
isPolicyTaxEnabled: isPolicyTaxEnabled(policy),
isVendorColumnAvailable,
});

const isAmountColumnWide = transactions.some((transaction) => isTransactionAmountTooLong(transaction));
Expand Down
3 changes: 3 additions & 0 deletions src/components/Search/FilterDropdowns/SortByPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelec
import type {ListItem} from '@components/SelectionList/types';

import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useIsVendorColumnAvailable from '@hooks/useIsVendorColumnAvailable';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -51,6 +52,7 @@ function SortByPopup({searchResults, queryJSON, groupBy, onSort, onSortOrderPres
const {clearSelectedTransactions} = useSearchSelectionActions();

const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: columnsSelector});
const isVendorColumnAvailable = useIsVendorColumnAvailable();

const searchDataType = shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type;
const currentColumns = !searchResults?.data
Expand All @@ -63,6 +65,7 @@ function SortByPopup({searchResults, queryJSON, groupBy, onSort, onSortOrderPres
groupBy: groupBy?.value,
sortBy: queryJSON.sortBy,
shouldShowViolationsColumn: queryHasViolationFilter(queryJSON),
isVendorColumnAvailable,
});
const sortableColumns = getSortByOptions(currentColumns, translate, searchDataType);
const sortOrder = queryJSON.sortOrder;
Expand Down
3 changes: 3 additions & 0 deletions src/components/Search/SearchList/ListItem/GroupHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {ExtendedTargetedEvent} from '@components/SelectionList/ListItem/typ
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useExpandCollapseAnimation from '@hooks/useExpandCollapseAnimation';
import useIsVendorColumnAvailable from '@hooks/useIsVendorColumnAvailable';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useOnyx from '@hooks/useOnyx';
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
Expand Down Expand Up @@ -106,6 +107,7 @@ function GroupHeader({
const expensifyIcons = useMemoizedLazyExpensifyIcons(['UpArrow', 'DownArrow']);
const currentUserDetails = useCurrentUserPersonalDetails();
const {policyForMovingExpensesID} = usePolicyForMovingExpenses();
const isVendorColumnAvailable = useIsVendorColumnAvailable();

const groupItem = item;
const isExpenseReportType = searchType === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
Expand Down Expand Up @@ -145,6 +147,7 @@ function GroupHeader({
type: snapshotSearchType,
shouldShowViolationsColumn: queryHasViolationFilter(groupItem.transactionsQueryJSON),
fallbackPolicyID: policyForMovingExpensesID,
isVendorColumnAvailable,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {useWideRHPActions} from '@components/WideRHPContextProvider';

import {useCurrencyListActions} from '@hooks/useCurrencyList';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useIsVendorColumnAvailable from '@hooks/useIsVendorColumnAvailable';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
Expand Down Expand Up @@ -95,6 +96,7 @@ function TransactionGroupListExpandedImpl({
const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasCompletedGuidedSetupFlowSelector});
const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: columnsSelector});
const isVendorColumnAvailable = useIsVendorColumnAvailable();
const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
const [policyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES);
const [policyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS);
Expand Down Expand Up @@ -147,6 +149,7 @@ function TransactionGroupListExpandedImpl({
type: transactionsSnapshot?.search.type,
fallbackPolicyID: policyForMovingExpensesID,
shouldShowViolationsColumn: queryHasViolationFilter(transactionsQueryJSON),
isVendorColumnAvailable,
});
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/components/Search/hooks/useSearchSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {SearchColumnType, SearchData, SearchQueryJSON} from '@components/Se
import useActionLoadingReportIDs from '@hooks/useActionLoadingReportIDs';
import {useCurrencyListActions} from '@hooks/useCurrencyList';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useIsVendorColumnAvailable from '@hooks/useIsVendorColumnAvailable';
import useLocalize from '@hooks/useLocalize';
import useMultipleSnapshots from '@hooks/useMultipleSnapshots';
import useNetwork from '@hooks/useNetwork';
Expand Down Expand Up @@ -127,6 +128,7 @@ function useSearchSnapshot({queryJSON, searchResults, newSearchResultKeys, trans
const [policyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS);
const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS);
const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: columnsSelector});
const isVendorColumnAvailable = useIsVendorColumnAvailable();
const [rules] = useOnyx(ONYXKEYS.COLLECTION.RULE);

// Inject an optimistically-created transaction the server has not indexed yet so its row mounts
Expand Down Expand Up @@ -397,6 +399,7 @@ function useSearchSnapshot({queryJSON, searchResults, newSearchResultKeys, trans
fallbackPolicyID: policyForMovingExpensesID,
sortBy: queryJSON.sortBy,
shouldShowViolationsColumn: queryHasViolationFilter(queryJSON),
isVendorColumnAvailable,
});
})();

Expand Down
27 changes: 27 additions & 0 deletions src/hooks/useIsVendorColumnAvailable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {hasVendorFeatureOnAnyPolicy} from '@libs/PolicyUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy} from '@src/types/onyx';

import type {OnyxCollection} from 'react-native-onyx';

import {useCallback} from 'react';

import useOnyx from './useOnyx';
import usePermissions from './usePermissions';

/**
* Whether the Vendor column can be shown on Search. Search spans every workspace, so the column is available as soon as
* any workspace has the vendor feature. Saved column lists outlive the feature, so every reader of the saved list
* re-checks this, not only the column picker.
*/
function useIsVendorColumnAvailable(): boolean {
const {isBetaEnabled} = usePermissions();
const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING);
const selector = useCallback((allPolicies: OnyxCollection<Policy>) => hasVendorFeatureOnAnyPolicy(allPolicies, isVendorMatchingBetaEnabled), [isVendorMatchingBetaEnabled]);
const [isVendorColumnAvailable = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector});
return isVendorColumnAvailable;
}

export default useIsVendorColumnAvailable;
9 changes: 7 additions & 2 deletions src/hooks/useSearchBulkActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ import useDefaultExpensePolicy from './useDefaultExpensePolicy';
import useDelegateAccountID from './useDelegateAccountID';
import useDeleteTransactions from './useDeleteTransactions';
import useDuplicateTransactionsAndViolations from './useDuplicateTransactionsAndViolations';
import useIsVendorColumnAvailable from './useIsVendorColumnAvailable';
import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
import useLocalize from './useLocalize';
import useNetwork from './useNetwork';
Expand Down Expand Up @@ -558,6 +559,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS);
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: columnsSelector});
const isVendorColumnAvailable = useIsVendorColumnAvailable();
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const {isBetaEnabled} = usePermissions();
const [personalDetails] = useAllPersonalDetails();
Expand Down Expand Up @@ -1021,7 +1023,9 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {

if (groupBy) {
const expensePermittedColumns: string[] = Object.values(CONST.SEARCH.TYPE_CUSTOM_COLUMNS.EXPENSE);
const expenseColumns: SearchColumnType[] = (visibleColumns ?? []).filter((column) => expensePermittedColumns.includes(column));
const expenseColumns: SearchColumnType[] = (visibleColumns ?? []).filter(
(column) => expensePermittedColumns.includes(column) && (isVendorColumnAvailable || column !== CONST.SEARCH.TABLE_COLUMNS.VENDOR),
);

columnsToExport = [CONST.SEARCH.TABLE_COLUMNS.TYPE, ...(expenseColumns.length > 0 ? expenseColumns : Object.values(CONST.SEARCH.TYPE_DEFAULT_COLUMNS.EXPENSE))];
// Grouped export skips getColumnsToShow(), so inject Violations when the query asks for it
Expand All @@ -1040,6 +1044,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
fallbackPolicyID: policyForMovingExpensesID,
sortBy: queryJSON?.sortBy,
shouldShowViolationsColumn: queryHasViolationFilter(queryJSON),
isVendorColumnAvailable,
});
}

Expand All @@ -1060,7 +1065,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
exportColumnLabels: JSON.stringify(exportColumnLabels),
};
},
[accountID, currentSearchKey, exportSearchData, exportSearchType, policyForMovingExpensesID, queryJSON, translate, visibleColumns],
[accountID, currentSearchKey, exportSearchData, exportSearchType, isVendorColumnAvailable, policyForMovingExpensesID, queryJSON, translate, visibleColumns],
);

const handleCSVExport = useCallback(
Expand Down
3 changes: 3 additions & 0 deletions src/hooks/useSearchOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {StyleProp, ViewStyle} from 'react-native';
import {useFocusEffect} from '@react-navigation/native';
import React, {useCallback, useEffect, useState} from 'react';

import useIsVendorColumnAvailable from './useIsVendorColumnAvailable';
import useOnyx from './useOnyx';
import usePolicyForMovingExpenses from './usePolicyForMovingExpenses';

Expand Down Expand Up @@ -67,6 +68,7 @@ function useSearchOverlay({
const session = useSession();
const accountID = session?.accountID ?? CONST.DEFAULT_NUMBER_ID;
const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: columnsSelector});
const isVendorColumnAvailable = useIsVendorColumnAvailable();
const {policyForMovingExpensesID} = usePolicyForMovingExpenses();

const [isSearchReady, setIsSearchReady] = useState(() => !hasDeferredWrite(CONST.DEFERRED_LAYOUT_WRITE_KEYS.SEARCH) && !Navigation.getIsFullscreenPreInsertedUnderRHP());
Expand Down Expand Up @@ -124,6 +126,7 @@ function useSearchOverlay({
fallbackPolicyID: policyForMovingExpensesID,
sortBy: queryJSON.sortBy,
shouldShowViolationsColumn: queryHasViolationFilter(queryJSON),
isVendorColumnAvailable,
});
})();

Expand Down
7 changes: 5 additions & 2 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6306,6 +6306,7 @@ function getColumnsToShow({
fallbackPolicyID,
sortBy,
shouldShowViolationsColumn = false,
isVendorColumnAvailable = true,
}: {
currentAccountID: number | undefined;
data: OnyxTypes.SearchResults['data'] | OnyxTypes.Transaction[];
Expand All @@ -6324,6 +6325,7 @@ function getColumnsToShow({
fallbackPolicyID?: string;
sortBy?: SearchSortBy;
shouldShowViolationsColumn?: boolean;
isVendorColumnAvailable?: boolean;
}): SearchColumnType[] {
const reportCustomColumns = new Set<SearchColumnType>([
CONST.SEARCH.TABLE_COLUMNS.SUBMITTER_USER_ID,
Expand Down Expand Up @@ -6541,7 +6543,8 @@ function getColumnsToShow({

// If the user has set custom columns for the search, we need to respect their preference and order
const allowedColumns: string[] = isExpenseReportView ? Object.values(CONST.SEARCH.REPORT_DETAILS_CUSTOM_COLUMNS) : Object.values(CONST.SEARCH.TYPE_CUSTOM_COLUMNS.EXPENSE);
const filteredVisibleColumns = visibleColumns.filter((column) => allowedColumns.includes(column));
// The saved list outlives the vendor feature, so Vendor is dropped once no workspace has the feature anymore.
const filteredVisibleColumns = visibleColumns.filter((column) => allowedColumns.includes(column) && (isVendorColumnAvailable || column !== CONST.SEARCH.TABLE_COLUMNS.VENDOR));
const isDefaultExpenseColumnSelection = arraysEqual(Object.values(CONST.SEARCH.TYPE_DEFAULT_COLUMNS.EXPENSE), filteredVisibleColumns);
const shouldUseCustomResult = !isDefaultExpenseColumnSelection && filteredVisibleColumns.length > 0;

Expand Down Expand Up @@ -6657,7 +6660,7 @@ function getColumnsToShow({
columns[CONST.SEARCH.TABLE_COLUMNS.CARD] = true;
}

if (transaction.comment?.vendor?.externalID) {
if (isVendorColumnAvailable && transaction.comment?.vendor?.externalID) {
columns[CONST.SEARCH.TABLE_COLUMNS.VENDOR] = true;
}

Expand Down
16 changes: 3 additions & 13 deletions src/pages/Search/SearchColumnsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,24 @@ import ColumnsSettingsList from '@components/ColumnsSettingsList';
import {useSearchQueryContext} from '@components/Search/SearchContext';
import type {SearchCustomColumnIds} from '@components/Search/types';

import useIsVendorColumnAvailable from '@hooks/useIsVendorColumnAvailable';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';

import Navigation from '@libs/Navigation/Navigation';
import {hasVendorFeatureOnAnyPolicy} from '@libs/PolicyUtils';
import {buildQueryStringFromFilterFormValues, getCurrentSearchQueryJSON, hasValuesIncludeViolationFilter} from '@libs/SearchQueryUtils';
import {getCustomColumnDefault, getCustomColumns, insertColumnBeforeTotalAmount} from '@libs/SearchUIUtils';

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, {useCallback} from 'react';
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 isVendorColumnAvailableSelector = useCallback(
(allPolicies: OnyxCollection<Policy>) => hasVendorFeatureOnAnyPolicy(allPolicies, isVendorMatchingBetaEnabled),
[isVendorMatchingBetaEnabled],
);
const [isVendorColumnAvailable = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: isVendorColumnAvailableSelector});
const isVendorColumnAvailable = useIsVendorColumnAvailable();

const groupBy = searchAdvancedFiltersForm?.groupBy;
const queryType = searchAdvancedFiltersForm?.type ?? CONST.SEARCH.DATA_TYPES.EXPENSE;
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/Search/SearchUIUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11306,6 +11306,34 @@ describe('SearchUIUtils', () => {
).toContain(CONST.SEARCH.TABLE_COLUMNS.VENDOR);
});

test('Should drop the vendor column from the saved list and the report view when the vendor feature is unavailable', () => {
// Given a saved Search column list that includes Vendor, and an expense that still carries a vendor
const transactionWithVendor = {...createRandomTransaction(3), 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];

// When no workspace has the vendor feature anymore
const searchColumns = SearchUIUtils.getColumnsToShow({
currentAccountID: 1,
data: [transactionWithVendor],
visibleColumns: pickedColumns,
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
isVendorColumnAvailable: false,
});
const reportColumns = SearchUIUtils.getColumnsToShow({
currentAccountID: 1,
data: [transactionWithVendor],
visibleColumns: [],
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
isExpenseReportView: true,
isVendorColumnAvailable: false,
});

// Then the Search table keeps the other saved columns but not Vendor, and the report view does not auto-show it either
expect(searchColumns).not.toContain(CONST.SEARCH.TABLE_COLUMNS.VENDOR);
expect(searchColumns).toContain(CONST.SEARCH.TABLE_COLUMNS.DATE);
expect(reportColumns).not.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,
Expand Down
Loading