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
2 changes: 2 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8659,6 +8659,7 @@ const CONST = {
TAG_EMPTY_VALUE: 'none',
CATEGORY_EMPTY_VALUE: 'none',
CATEGORY_DEFAULT_VALUE: 'Uncategorized',
VENDOR_EMPTY_VALUE: 'none',
MERCHANT_EMPTY_VALUE: 'none',
SEARCH_ROUTER_ITEM_TYPE: {
CONTEXTUAL_SUGGESTION: 'contextualSuggestion',
Expand Down Expand Up @@ -8695,6 +8696,7 @@ const CONST = {
EXPORTER: 'exporter',
CATEGORY: 'category',
TAG: 'tag',
VENDOR: 'vendor',
TAX_RATE: 'tax-rate',
CARD_ID: 'card',
FEED: 'feed',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import TagSelector from './TagSelector';
import TaxRateSelector from './TaxRateSelector';
import TypeSelector from './TypeSelector';
import UserSelector from './UserSelector';
import VendorSelector from './VendorSelector';
import WorkspaceSelector from './WorkspaceSelector';

type FilterKeys = Exclude<SearchFilter['key'], SearchDateFilterKeys | SearchAmountFilterKeys | SearchTextFilterKeys | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_FIELD>;
Expand Down Expand Up @@ -196,6 +197,7 @@ function ListFilterContent({
case CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE:
case CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPORTED_TO:
case CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG:
case CONST.SEARCH.SYNTAX_FILTER_KEYS.VENDOR:
case CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY: {
const Component = {
[CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED]: FeedSelector,
Expand All @@ -205,6 +207,7 @@ function ListFilterContent({
[CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE]: TaxRateSelector,
[CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPORTED_TO]: ExportedToSelector,
[CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG]: TagSelector,
[CONST.SEARCH.SYNTAX_FILTER_KEYS.VENDOR]: VendorSelector,
[CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY]: CategorySelector,
}[baseFilterKey];
content = (
Expand Down
78 changes: 78 additions & 0 deletions src/components/Search/FilterComponents/VendorSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type {Filter, SearchFilterCommonProps} from '@components/Search/types';

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

import {getMatchingVendors, getVendorFeaturePolicyIDs} from '@libs/PolicyUtils';
import {getAllPolicyValues, sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

import React from 'react';

import MultiSelect from './MultiSelect';

type VendorSelectorProps = SearchFilterCommonProps<string[] | undefined> & {
policyID: Filter | undefined;
};

function VendorSelector({value = [], policyID, selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange}: VendorSelectorProps) {
const {translate, localeCompare} = useLocalize();
const {isBetaEnabled} = usePermissions();
const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING);

const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);

// Uses the same synced vendors the expense Vendor field offers.
const vendorNamesByPolicyID = Object.fromEntries(
getVendorFeaturePolicyIDs(allPolicies, isVendorMatchingBetaEnabled).map((id) => [
id,
getMatchingVendors(allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${id}`]).map((vendor) => vendor.name),
]),
);

const noVendorLabel = translate('search.noVendor');
const selectedVendorItems = value.map((vendor) => {
if (vendor === CONST.SEARCH.VENDOR_EMPTY_VALUE) {
return {text: noVendorLabel, value: vendor};
}
return {text: vendor, value: vendor};
});

// The workspace filter can hold workspace names as well as IDs, so it is resolved to the matching workspaces first.
const selectedPolicyIDs = policyID?.value?.length ? new Set(getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY, allPolicies).map((policy) => policy.id)) : undefined;
const vendorItems = [{text: noVendorLabel, value: CONST.SEARCH.VENDOR_EMPTY_VALUE as string}];
const uniqueVendorNames = new Set<string>(
Object.entries(vendorNamesByPolicyID)
.filter(([id]) => !selectedPolicyIDs || selectedPolicyIDs.has(id))
.flatMap(([, vendorNames]) => vendorNames),
);
vendorItems.push(
...Array.from(uniqueVendorNames)
.filter(Boolean)
.map((vendorName) => ({text: vendorName, value: vendorName}))
.toSorted((a, b) => sortOptionsWithEmptyValue(a.text, b.text, localeCompare)),
Comment thread
c3024 marked this conversation as resolved.
);

// Selected vendors that are not in the synced lists, such as a typed name or one from a workspace the filter leaves out, stay listed after "No vendor" so they can be deselected.
const vendorItemValues = new Set(vendorItems.map((item) => item.value));
const missingSelectedVendorItems = selectedVendorItems.filter((item) => !vendorItemValues.has(item.value)).toSorted((a, b) => localeCompare(a.text, b.text));
vendorItems.splice(1, 0, ...missingSelectedVendorItems);

return (
<MultiSelect
value={selectedVendorItems}
items={vendorItems}
isSearchable={vendorItems.length >= CONST.STANDARD_LIST_ITEM_LIMIT}
autoFocus={autoFocus}
selectionListTextInputStyle={selectionListTextInputStyle}
selectionListStyle={selectionListStyle}
footer={footer}
onChange={(vendors) => onChange(vendors.map((vendor) => vendor.value))}
/>
);
}

export default VendorSelector;
11 changes: 10 additions & 1 deletion src/hooks/useAdvancedSearchFilters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {isFilterableBankAccount} from '@libs/BankAccountUtils';
import {isPolicyFeatureEnabled} from '@libs/PolicyUtils';
import {hasVendorFeatureOnAnyPolicy, isPolicyFeatureEnabled} from '@libs/PolicyUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -14,6 +14,7 @@ import {emailSelector} from '@selectors/Session';

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

/**
Expand All @@ -38,6 +39,7 @@ const typeFiltersKeys = {
CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION,
CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY,
CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG,
CONST.SEARCH.SYNTAX_FILTER_KEYS.VENDOR,
CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE,
CONST.SEARCH.SYNTAX_FILTER_KEYS.ATTENDEE,
CONST.SEARCH.SYNTAX_FILTER_KEYS.REIMBURSABLE,
Expand Down Expand Up @@ -319,6 +321,10 @@ function useAdvancedSearchFilters(type: SearchDataTypes | undefined) {
const [shouldDisplayCardFilter] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {selector: shouldDisplayCardFilterSelector});
const [policies = getEmptyObject<NonNullable<OnyxCollection<Policy>>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: advancedSearchPoliciesSelector});
const [policyDerived] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policyDerivedSelector});
const {isBetaEnabled} = usePermissions();
const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING);
const isVendorFilterAvailableSelector = (allPolicies: OnyxCollection<Policy>) => hasVendorFeatureOnAnyPolicy(allPolicies, isVendorMatchingBetaEnabled);
const [isVendorFilterAvailable = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: isVendorFilterAvailableSelector});
const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);

const {workspaces} = useAdvancedSearchFiltersWorkspaces(policies);
Expand All @@ -343,6 +349,9 @@ function useAdvancedSearchFilters(type: SearchDataTypes | undefined) {
if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG && !shouldDisplayTagFilter) {
return;
}
if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.VENDOR && !isVendorFilterAvailable) {
return;
}
if (
(key === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID ||
key === CONST.SEARCH.SYNTAX_FILTER_KEYS.POSTED ||
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10441,6 +10441,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
noCategory: 'Keine Kategorie',
noMerchant: 'Kein Händler',
noTag: 'Kein Tag',
noVendor: 'Kein Lieferant',
expenseType: 'Ausgabenart',
receiptType: 'Belegart',
receiptTypeValues: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10652,6 +10652,7 @@ ${reportName}`,
noCategory: 'Χωρίς κατηγορία',
noMerchant: 'Χωρίς έμπορο',
noTag: 'Χωρίς ετικέτα',
noVendor: 'Χωρίς προμηθευτή',
expenseType: 'Τύπος εξόδου',
receiptType: 'Τύπος απόδειξης',
receiptTypeValues: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10708,6 +10708,7 @@ const translations = {
noCategory: 'No category',
noMerchant: 'No merchant',
noTag: 'No tag',
noVendor: 'No vendor',
expenseType: 'Expense type',
receiptType: 'Receipt type',
receiptTypeValues: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10264,6 +10264,7 @@ ${reportName}`,
noCategory: 'Sin categoría',
noMerchant: 'Sin comerciante',
noTag: 'Sin etiqueta',
noVendor: 'Sin proveedor',
expenseType: 'Tipo de gasto',
receiptType: 'Tipo de recibo',
receiptTypeValues: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10467,6 +10467,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
noCategory: 'Aucune catégorie',
noMerchant: 'Aucun commerçant',
noTag: 'Aucun tag',
noVendor: 'Aucun fournisseur',
expenseType: 'Type de dépense',
receiptType: 'Type de reçu',
receiptTypeValues: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10389,6 +10389,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
noCategory: 'Nessuna categoria',
noMerchant: 'Nessun esercente',
noTag: 'Nessun tag',
noVendor: 'Nessun fornitore',
expenseType: 'Tipo di spesa',
receiptType: 'Tipo di ricevuta',
receiptTypeValues: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10240,6 +10240,7 @@ ${reportName}`,
noCategory: 'カテゴリなし',
noMerchant: '店舗なし',
noTag: 'タグなし',
noVendor: 'ベンダーなし',
expenseType: '経費の種類',
receiptType: '領収書の種類',
receiptTypeValues: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10347,6 +10347,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
noCategory: 'Geen categorie',
noMerchant: 'Geen handelaar',
noTag: 'Geen tag',
noVendor: 'Geen leverancier',
expenseType: 'Onkostentype',
receiptType: 'Bontype',
receiptTypeValues: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10376,6 +10376,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
noCategory: 'Brak kategorii',
noMerchant: 'Brak sprzedawcy',
noTag: 'Brak tagu',
noVendor: 'Brak dostawcy',
expenseType: 'Typ wydatku',
receiptType: 'Typ paragonu',
receiptTypeValues: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10343,6 +10343,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
noCategory: 'Sem categoria',
noMerchant: 'Sem comerciante',
noTag: 'Sem tag',
noVendor: 'Sem fornecedor',
expenseType: 'Tipo de despesa',
receiptType: 'Tipo de recibo',
receiptTypeValues: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9971,6 +9971,7 @@ ${reportName}`,
noCategory: '无类别',
noMerchant: '无商家',
noTag: '无标签',
noVendor: '无供应商',
expenseType: '报销类型',
receiptType: '收据类型',
receiptTypeValues: {
Expand Down
16 changes: 15 additions & 1 deletion src/libs/PolicyUtils/vendor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,25 @@ function hasVendorFeature(policy: OnyxEntry<Policy>, isVendorMatchingBetaEnabled
}

/**
* Search spans every workspace at once, so the vendor column is offered when any workspace has the vendor feature.
* Search spans every workspace at once, so the vendor filter and column are offered when any workspace has the vendor feature.
*/
function hasVendorFeatureOnAnyPolicy(policies: OnyxCollection<Policy>, isVendorMatchingBetaEnabled: boolean): boolean {
return Object.values(policies ?? {}).some((policy) => hasVendorFeature(policy, isVendorMatchingBetaEnabled));
}

/**
* IDs of the workspaces that have the vendor feature, so the Search vendor filter only offers their vendor lists.
*/
function getVendorFeaturePolicyIDs(policies: OnyxCollection<Policy>, isVendorMatchingBetaEnabled: boolean): string[] {
const policyIDs: string[] = [];
for (const policy of Object.values(policies ?? {})) {
if (policy?.id && hasVendorFeature(policy, isVendorMatchingBetaEnabled)) {
policyIDs.push(policy.id);
}
}
return policyIDs;
}

/**
* Single source of truth for which connected integration scopes the vendor field for this workspace
* (QBO, Sage Intacct, Xero, Rillet, DualEntry, Business Central, Campfire, or Certinia) and what its vendor list looks like. Returns `undefined` when no
Expand Down Expand Up @@ -564,5 +577,6 @@ export {
isXeroVendorMatchingActive,
hasVendorFeature,
hasVendorFeatureOnAnyPolicy,
getVendorFeaturePolicyIDs,
isMatchingVendorListLoaded,
};
1 change: 1 addition & 0 deletions src/libs/SearchAutocompleteUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ function filterOutRangesWithCorrectValue(
case CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT:
case CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION:
case CONST.SEARCH.SYNTAX_FILTER_KEYS.TITLE:
case CONST.SEARCH.SYNTAX_FILTER_KEYS.VENDOR:
return range.value.length > 0;
case CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPORTED_TO:
return exportedToList.get().includes(range.value);
Expand Down
Loading
Loading