Skip to content

Commit e20d305

Browse files
authored
Merge pull request Expensify#65547 from parasharrajat/revert-65543-vit-revert-63706
Support for Multi workspace filter in search & remove duplicate tax rate selection
2 parents c470f91 + 947a778 commit e20d305

16 files changed

Lines changed: 223 additions & 90 deletions

src/components/Search/SearchMultipleSelectionPicker.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,19 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit
3535
const {sections, noResultsFound} = useMemo(() => {
3636
const selectedItemsSection = selectedItems
3737
.filter((item) => item?.name.toLowerCase().includes(debouncedSearchTerm?.toLowerCase()))
38-
.sort((a, b) => sortOptionsWithEmptyValue(a.value as string, b.value as string))
38+
.sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString()))
3939
.map((item) => ({
4040
text: item.name,
4141
keyForList: item.name,
4242
isSelected: true,
4343
value: item.value,
4444
}));
4545
const remainingItemsSection = items
46-
.filter((item) => selectedItems.some((selectedItem) => selectedItem.value === item.value) === false && item?.name?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase()))
47-
.sort((a, b) => sortOptionsWithEmptyValue(a.value as string, b.value as string))
46+
.filter(
47+
(item) =>
48+
!selectedItems.some((selectedItem) => selectedItem.value.toString() === item.value.toString()) && item?.name?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase()),
49+
)
50+
.sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString()))
4851
.map((item) => ({
4952
text: item.name,
5053
keyForList: item.name,

src/components/Search/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ type SearchQueryAST = {
142142
sortOrder: SortOrder;
143143
groupBy?: SearchGroupBy;
144144
filters: ASTNode;
145-
policyID?: string;
145+
policyID?: string[];
146146
};
147147

148148
type SearchQueryJSON = {

src/components/SelectionList/UserListItem.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function UserListItem<TItem extends ListItem>({
4141
shouldSyncFocus,
4242
wrapperStyle,
4343
pressableStyle,
44+
shouldUseDefaultRightHandSideCheckmark,
4445
}: UserListItemProps<TItem>) {
4546
const styles = useThemeStyles();
4647
const theme = useTheme();
@@ -87,7 +88,7 @@ function UserListItem<TItem extends ListItem>({
8788
>
8889
{(hovered?: boolean) => (
8990
<>
90-
{!!canSelectMultiple && (
91+
{!shouldUseDefaultRightHandSideCheckmark && !!canSelectMultiple && (
9192
<PressableWithFeedback
9293
accessibilityLabel={item.text ?? ''}
9394
role={CONST.ROLE.BUTTON}
@@ -156,6 +157,27 @@ function UserListItem<TItem extends ListItem>({
156157
/>
157158
</View>
158159
)}
160+
{!!shouldUseDefaultRightHandSideCheckmark && !!canSelectMultiple && (
161+
<PressableWithFeedback
162+
accessibilityLabel={item.text ?? ''}
163+
role={CONST.ROLE.BUTTON}
164+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
165+
disabled={isDisabled || item.isDisabledCheckbox}
166+
onPress={handleCheckboxPress}
167+
style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled, styles.ml3]}
168+
>
169+
<View style={[StyleUtils.getCheckboxContainerStyle(20), StyleUtils.getMultiselectListStyles(!!item.isSelected, !!item.isDisabled)]}>
170+
{!!item.isSelected && (
171+
<Icon
172+
src={Expensicons.Checkmark}
173+
fill={theme.textLight}
174+
height={14}
175+
width={14}
176+
/>
177+
)}
178+
</View>
179+
</PressableWithFeedback>
180+
)}
159181
</>
160182
)}
161183
</BaseListItem>

src/hooks/useWorkspaceList.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ type UseWorkspaceListParams = {
2121
policies: OnyxCollection<Policy>;
2222
currentUserLogin: string | undefined;
2323
shouldShowPendingDeletePolicy: boolean;
24-
selectedPolicyID: string | undefined;
24+
selectedPolicyIDs: string[] | undefined;
2525
searchTerm: string;
2626
additionalFilter?: (policy: OnyxEntry<Policy>) => boolean;
2727
};
2828

29-
function useWorkspaceList({policies, currentUserLogin, selectedPolicyID, searchTerm, shouldShowPendingDeletePolicy, additionalFilter}: UseWorkspaceListParams) {
29+
function useWorkspaceList({policies, currentUserLogin, selectedPolicyIDs, searchTerm, shouldShowPendingDeletePolicy, additionalFilter}: UseWorkspaceListParams) {
3030
const usersWorkspaces = useMemo(() => {
3131
if (!policies || isEmptyObject(policies)) {
3232
return [];
@@ -54,16 +54,16 @@ function useWorkspaceList({policies, currentUserLogin, selectedPolicyID, searchT
5454
],
5555
keyForList: policy?.id,
5656
isPolicyAdmin: isPolicyAdmin(policy),
57-
isSelected: selectedPolicyID === policy?.id,
57+
isSelected: policy?.id && selectedPolicyIDs ? selectedPolicyIDs.includes(policy.id) : false,
5858
}));
59-
}, [policies, shouldShowPendingDeletePolicy, currentUserLogin, additionalFilter, selectedPolicyID]);
59+
}, [policies, shouldShowPendingDeletePolicy, currentUserLogin, additionalFilter, selectedPolicyIDs]);
6060

6161
const filteredAndSortedUserWorkspaces = useMemo<WorkspaceListItem[]>(
6262
() =>
6363
tokenizedSearch(usersWorkspaces, searchTerm, (policy) => [policy.text]).sort((policy1, policy2) =>
64-
sortWorkspacesBySelected({policyID: policy1.policyID, name: policy1.text}, {policyID: policy2.policyID, name: policy2.text}, selectedPolicyID),
64+
sortWorkspacesBySelected({policyID: policy1.policyID, name: policy1.text}, {policyID: policy2.policyID, name: policy2.text}, selectedPolicyIDs),
6565
),
66-
[searchTerm, usersWorkspaces, selectedPolicyID],
66+
[searchTerm, usersWorkspaces, selectedPolicyIDs],
6767
);
6868

6969
const sections = useMemo(() => {

src/libs/PolicyUtils.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,9 @@ function getAllTaxRatesNamesAndKeys(): Record<string, string[]> {
595595
allTaxRates[taxRate.name] = [taxRateKey];
596596
return;
597597
}
598+
if (allTaxRates[taxRate.name].includes(taxRateKey)) {
599+
return;
600+
}
598601
allTaxRates[taxRate.name].push(taxRateKey);
599602
});
600603
});
@@ -1161,11 +1164,11 @@ function getSageIntacctCreditCards(policy?: Policy, selectedAccount?: string): S
11611164
* @param workspace2 Details of the second workspace to be compared.
11621165
* @param selectedWorkspaceID ID of the selected workspace which needs to be at the beginning.
11631166
*/
1164-
const sortWorkspacesBySelected = (workspace1: WorkspaceDetails, workspace2: WorkspaceDetails, selectedWorkspaceID: string | undefined): number => {
1165-
if (workspace1.policyID === selectedWorkspaceID) {
1167+
const sortWorkspacesBySelected = (workspace1: WorkspaceDetails, workspace2: WorkspaceDetails, selectedWorkspaceIDs: string[] | undefined): number => {
1168+
if (workspace1.policyID && selectedWorkspaceIDs?.includes(workspace1?.policyID)) {
11661169
return -1;
11671170
}
1168-
if (workspace2.policyID === selectedWorkspaceID) {
1171+
if (workspace2.policyID && selectedWorkspaceIDs?.includes(workspace2.policyID)) {
11691172
return 1;
11701173
}
11711174
return workspace1.name?.toLowerCase().localeCompare(workspace2.name?.toLowerCase() ?? '') ?? 0;

src/libs/SearchQueryUtils.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
} from '@components/Search/types';
1616
import CONST from '@src/CONST';
1717
import NAVIGATORS from '@src/NAVIGATORS';
18+
import type {OnyxCollectionKey, OnyxCollectionValuesMapping} from '@src/ONYXKEYS';
1819
import ONYXKEYS from '@src/ONYXKEYS';
1920
import SCREENS from '@src/SCREENS';
2021
import type {SearchAdvancedFiltersForm} from '@src/types/form';
@@ -295,7 +296,7 @@ function getQueryHashes(query: SearchQueryJSON): {primaryHash: number; recentSea
295296
orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY}:${query.sortBy}`;
296297
orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_ORDER}:${query.sortOrder}`;
297298
if (query.policyID) {
298-
orderedQuery += ` ${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${query.policyID} `;
299+
orderedQuery += ` ${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${Array.isArray(query.policyID) ? query.policyID.join(',') : query.policyID} `;
299300
}
300301
const primaryHash = hashText(orderedQuery, 2 ** 32);
301302

@@ -334,6 +335,11 @@ function buildSearchQueryJSON(query: SearchQueryString) {
334335
result.hash = primaryHash;
335336
result.recentSearchHash = recentSearchHash;
336337

338+
if (result.policyID && typeof result.policyID === 'string') {
339+
// Ensure policyID is always an array for consistency
340+
result.policyID = [result.policyID];
341+
}
342+
337343
return result;
338344
} catch (e) {
339345
console.error(`Error when parsing SearchQuery: "${query}"`, e);
@@ -364,7 +370,7 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
364370
}
365371

366372
if (queryJSON?.policyID) {
367-
queryParts.push(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${queryJSON.policyID}`);
373+
queryParts.push(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${Array.isArray(queryJSON.policyID) ? queryJSON.policyID.join(',') : queryJSON.policyID}`);
368374
}
369375

370376
if (!queryJSON) {
@@ -402,7 +408,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
402408
});
403409

404410
// We separate type and status filters from other filters to maintain hashes consistency for saved searches
405-
const {type, status, policyID, groupBy, ...otherFilters} = supportedFilterValues;
411+
const {type, status, groupBy, ...otherFilters} = supportedFilterValues;
406412
const filtersString: string[] = [];
407413

408414
filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY}:${CONST.SEARCH.TABLE_COLUMNS.DATE}`);
@@ -428,11 +434,6 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
428434
filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS}:${filterValueArray.map(sanitizeSearchValue).join(',')}`);
429435
}
430436

431-
if (policyID) {
432-
const sanitizedPolicyID = sanitizeSearchValue(policyID);
433-
filtersString.push(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${sanitizedPolicyID}`);
434-
}
435-
436437
const mappedFilters = Object.entries(otherFilters)
437438
.map(([filterKey, filterValue]) => {
438439
if (
@@ -479,6 +480,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
479480
filterKey === FILTER_KEYS.FEED ||
480481
filterKey === FILTER_KEYS.IN ||
481482
filterKey === FILTER_KEYS.ASSIGNEE ||
483+
filterKey === FILTER_KEYS.POLICY_ID ||
482484
filterKey === FILTER_KEYS.EXPORTER) &&
483485
Array.isArray(filterValue) &&
484486
filterValue.length > 0
@@ -508,6 +510,18 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
508510
return filtersString.filter(Boolean).join(' ').trim();
509511
}
510512

513+
function getAllPolicyValues<T extends OnyxCollectionKey>(
514+
policyID: string[] | undefined,
515+
key: T,
516+
policyData: OnyxCollection<OnyxCollectionValuesMapping[T]>,
517+
): Array<OnyxCollectionValuesMapping[T]> {
518+
if (!policyData || !policyID) {
519+
return [];
520+
}
521+
522+
return policyID.map((id) => policyData?.[`${key}${id}`]).filter((data) => !!data) as Array<OnyxCollectionValuesMapping[T]>;
523+
}
524+
511525
/**
512526
* Generates object with search filter values, in a format that can be consumed by SearchAdvancedFiltersForm.
513527
* Main usage of this is to generate the initial values for AdvancedFilters from existing query.
@@ -576,7 +590,9 @@ function buildFilterFormValuesFromQuery(
576590
}
577591
if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG) {
578592
const tags = policyID
579-
? getTagNamesFromTagsLists(policyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})
593+
? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_TAGS, policyTags)
594+
.map((tagList) => getTagNamesFromTagsLists(tagList ?? {}))
595+
.flat()
580596
: Object.values(policyTags ?? {})
581597
.filter((item) => !!item)
582598
.map((tagList) => getTagNamesFromTagsLists(tagList ?? {}))
@@ -587,7 +603,9 @@ function buildFilterFormValuesFromQuery(
587603
}
588604
if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY) {
589605
const categories = policyID
590-
? Object.values(policyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}).map((category) => category.name)
606+
? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_CATEGORIES, policyCategories)
607+
.map((item) => Object.values(item ?? {}).map((category) => category.name))
608+
.flat()
591609
: Object.values(policyCategories ?? {})
592610
.map((item) => Object.values(item ?? {}).map((category) => category.name))
593611
.flat();
@@ -739,9 +757,8 @@ function buildUserReadableQueryString(
739757
title += ` group-by:${groupBy}`;
740758
}
741759

742-
if (policyID) {
743-
const workspace = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.name ?? policyID;
744-
title += ` workspace:${sanitizeSearchValue(workspace)}`;
760+
if (policyID && policyID.length > 0) {
761+
title += ` workspace:${policyID.map((id) => sanitizeSearchValue(policies?.[`${ONYXKEYS.COLLECTION.POLICY}${id}`]?.name ?? id)).join(',')}`;
745762
}
746763

747764
for (const filterObject of filters) {
@@ -977,4 +994,5 @@ export {
977994
isDefaultExpensesQuery,
978995
sortOptionsWithEmptyValue,
979996
shouldHighlight,
997+
getAllPolicyValues,
980998
};

src/pages/ReportChangeWorkspacePage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function ReportChangeWorkspacePage({report, route}: ReportChangeWorkspacePagePro
6868
policies,
6969
currentUserLogin: session?.email,
7070
shouldShowPendingDeletePolicy: false,
71-
selectedPolicyID: report.policyID,
71+
selectedPolicyIDs: report.policyID ? [report.policyID] : undefined,
7272
searchTerm: debouncedSearchTerm,
7373
additionalFilter: (newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, policies),
7474
});

src/pages/Search/AdvancedSearchFilters.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,15 @@ import Navigation from '@libs/Navigation/Navigation';
2828
import {createDisplayName} from '@libs/PersonalDetailsUtils';
2929
import {getAllTaxRates, getCleanedTagName, getTagNamesFromTagsLists, isPolicyFeatureEnabled} from '@libs/PolicyUtils';
3030
import {getReportName} from '@libs/ReportUtils';
31-
import {buildCannedSearchQuery, buildQueryStringFromFilterFormValues, buildSearchQueryJSON, isCannedSearchQuery, isSearchDatePreset, sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils';
31+
import {
32+
buildCannedSearchQuery,
33+
buildQueryStringFromFilterFormValues,
34+
buildSearchQueryJSON,
35+
getAllPolicyValues,
36+
isCannedSearchQuery,
37+
isSearchDatePreset,
38+
sortOptionsWithEmptyValue,
39+
} from '@libs/SearchQueryUtils';
3240
import {getExpenseTypeTranslationKey, getStatusOptions} from '@libs/SearchUIUtils';
3341
import CONST from '@src/CONST';
3442
import type {TranslationPaths} from '@src/languages/types';
@@ -318,7 +326,10 @@ const typeFiltersKeys = {
318326
};
319327

320328
function getFilterWorkspaceDisplayTitle(filters: SearchAdvancedFiltersForm, policies: WorkspaceListItem[]) {
321-
return policies.filter((value) => value.policyID === filters.policyID).at(0)?.text;
329+
return policies
330+
.filter((value) => value.policyID && filters.policyID?.includes(value.policyID))
331+
.map((value) => value.text)
332+
.join(', ');
322333
}
323334

324335
function getFilterCardDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, cards: CardList, translate: LocaleContextProps['translate']) {
@@ -557,9 +568,9 @@ function AdvancedSearchFilters() {
557568
}),
558569
),
559570
});
560-
const singlePolicyCategories = allPolicyCategories[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`];
571+
const selectedPolicyCategories = getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_CATEGORIES, allPolicyCategories);
561572
const [allPolicyTagLists = getEmptyObject<NonNullable<OnyxCollection<PolicyTagLists>>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {canBeMissing: false});
562-
const singlePolicyTagLists = allPolicyTagLists[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`];
573+
const selectedPolicyTagLists = getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_TAGS, allPolicyTagLists);
563574
const tagListsUnpacked = Object.values(allPolicyTagLists ?? {})
564575
.filter((item): item is NonNullable<PolicyTagLists> => !!item)
565576
.map(getTagNamesFromTagsLists)
@@ -571,7 +582,7 @@ function AdvancedSearchFilters() {
571582
policies,
572583
currentUserLogin,
573584
shouldShowPendingDeletePolicy: false,
574-
selectedPolicyID: undefined,
585+
selectedPolicyIDs: undefined,
575586
searchTerm: '',
576587
});
577588

@@ -588,8 +599,8 @@ function AdvancedSearchFilters() {
588599
isFeatureEnabledInPolicies(policies, CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED);
589600
const areTaxEnabled = isFeatureEnabledInPolicies(policies, CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED);
590601

591-
const shouldDisplayCategoryFilter = shouldDisplayFilter(nonPersonalPolicyCategoryCount, areCategoriesEnabled, !!singlePolicyCategories);
592-
const shouldDisplayTagFilter = shouldDisplayFilter(tagListsUnpacked.length, areTagsEnabled, !!singlePolicyTagLists);
602+
const shouldDisplayCategoryFilter = shouldDisplayFilter(nonPersonalPolicyCategoryCount, areCategoriesEnabled, !!selectedPolicyCategories);
603+
const shouldDisplayTagFilter = shouldDisplayFilter(tagListsUnpacked.length, areTagsEnabled, !!selectedPolicyTagLists);
593604
const shouldDisplayCardFilter = shouldDisplayFilter(Object.keys(allCards).length, areCardsEnabled);
594605
const shouldDisplayTaxFilter = shouldDisplayFilter(Object.keys(taxRates).length, areTaxEnabled);
595606
const shouldDisplayWorkspaceFilter = workspaces.some((section) => section.data.length !== 0);

0 commit comments

Comments
 (0)