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: 1 addition & 1 deletion src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ type SearchQueryAST = {
sortOrder: SortOrder;
groupBy?: SearchGroupBy;
filters: ASTNode;
policyID?: string;
policyID?: string[];
};

type SearchQueryJSON = {
Expand Down
24 changes: 23 additions & 1 deletion src/components/SelectionList/UserListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function UserListItem<TItem extends ListItem>({
shouldSyncFocus,
wrapperStyle,
pressableStyle,
shouldUseDefaultRightHandSideCheckmark,
}: UserListItemProps<TItem>) {
const styles = useThemeStyles();
const theme = useTheme();
Expand Down Expand Up @@ -87,7 +88,7 @@ function UserListItem<TItem extends ListItem>({
>
{(hovered?: boolean) => (
<>
{!!canSelectMultiple && (
{!shouldUseDefaultRightHandSideCheckmark && !!canSelectMultiple && (
<PressableWithFeedback
accessibilityLabel={item.text ?? ''}
role={CONST.ROLE.BUTTON}
Expand Down Expand Up @@ -156,6 +157,27 @@ function UserListItem<TItem extends ListItem>({
/>
</View>
)}
{!!shouldUseDefaultRightHandSideCheckmark && !!canSelectMultiple && (
<PressableWithFeedback
accessibilityLabel={item.text ?? ''}
role={CONST.ROLE.BUTTON}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
disabled={isDisabled || item.isDisabledCheckbox}
onPress={handleCheckboxPress}
style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled, styles.ml3]}
>
<View style={[StyleUtils.getCheckboxContainerStyle(20), StyleUtils.getMultiselectListStyles(!!item.isSelected, !!item.isDisabled)]}>
{!!item.isSelected && (
<Icon
src={Expensicons.Checkmark}
fill={theme.textLight}
height={14}
width={14}
/>
)}
</View>
</PressableWithFeedback>
)}
</>
)}
</BaseListItem>
Expand Down
12 changes: 6 additions & 6 deletions src/hooks/useWorkspaceList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ type UseWorkspaceListParams = {
policies: OnyxCollection<Policy>;
currentUserLogin: string | undefined;
shouldShowPendingDeletePolicy: boolean;
selectedPolicyID: string | undefined;
selectedPolicyIDs: string[] | undefined;
searchTerm: string;
additionalFilter?: (policy: OnyxEntry<Policy>) => boolean;
};

function useWorkspaceList({policies, currentUserLogin, selectedPolicyID, searchTerm, shouldShowPendingDeletePolicy, additionalFilter}: UseWorkspaceListParams) {
function useWorkspaceList({policies, currentUserLogin, selectedPolicyIDs, searchTerm, shouldShowPendingDeletePolicy, additionalFilter}: UseWorkspaceListParams) {
const usersWorkspaces = useMemo(() => {
if (!policies || isEmptyObject(policies)) {
return [];
Expand Down Expand Up @@ -54,16 +54,16 @@ function useWorkspaceList({policies, currentUserLogin, selectedPolicyID, searchT
],
keyForList: policy?.id,
isPolicyAdmin: isPolicyAdmin(policy),
isSelected: selectedPolicyID === policy?.id,
isSelected: policy?.id && selectedPolicyIDs ? selectedPolicyIDs.includes(policy.id) : false,
}));
}, [policies, shouldShowPendingDeletePolicy, currentUserLogin, additionalFilter, selectedPolicyID]);
}, [policies, shouldShowPendingDeletePolicy, currentUserLogin, additionalFilter, selectedPolicyIDs]);

const filteredAndSortedUserWorkspaces = useMemo<WorkspaceListItem[]>(
() =>
tokenizedSearch(usersWorkspaces, searchTerm, (policy) => [policy.text]).sort((policy1, policy2) =>
sortWorkspacesBySelected({policyID: policy1.policyID, name: policy1.text}, {policyID: policy2.policyID, name: policy2.text}, selectedPolicyID),
sortWorkspacesBySelected({policyID: policy1.policyID, name: policy1.text}, {policyID: policy2.policyID, name: policy2.text}, selectedPolicyIDs),
),
[searchTerm, usersWorkspaces, selectedPolicyID],
[searchTerm, usersWorkspaces, selectedPolicyIDs],
);

const sections = useMemo(() => {
Expand Down
6 changes: 3 additions & 3 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,18 @@
let activePolicyId: OnyxEntry<string>;
let isLoadingReportData = true;

Onyx.connect({

Check warning on line 62 in src/libs/PolicyUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => (allPolicies = value),
});

Onyx.connect({

Check warning on line 68 in src/libs/PolicyUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
callback: (value) => (activePolicyId = value),
});

Onyx.connect({

Check warning on line 73 in src/libs/PolicyUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.IS_LOADING_REPORT_DATA,
initWithStoredValues: false,
callback: (value) => (isLoadingReportData = value ?? false),
Expand Down Expand Up @@ -1144,11 +1144,11 @@
* @param workspace2 Details of the second workspace to be compared.
* @param selectedWorkspaceID ID of the selected workspace which needs to be at the beginning.
*/
const sortWorkspacesBySelected = (workspace1: WorkspaceDetails, workspace2: WorkspaceDetails, selectedWorkspaceID: string | undefined): number => {
if (workspace1.policyID === selectedWorkspaceID) {
const sortWorkspacesBySelected = (workspace1: WorkspaceDetails, workspace2: WorkspaceDetails, selectedWorkspaceIDs: string[] | undefined): number => {
if (workspace1.policyID && selectedWorkspaceIDs?.includes(workspace1?.policyID)) {
return -1;
}
if (workspace2.policyID === selectedWorkspaceID) {
if (workspace2.policyID && selectedWorkspaceIDs?.includes(workspace2.policyID)) {
return 1;
}
return workspace1.name?.toLowerCase().localeCompare(workspace2.name?.toLowerCase() ?? '') ?? 0;
Expand Down
39 changes: 31 additions & 8 deletions src/libs/SearchQueryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
} from '@components/Search/types';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import type {OnyxCollectionKey, OnyxCollectionValuesMapping} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
Expand Down Expand Up @@ -295,7 +296,7 @@ function getQueryHashes(query: SearchQueryJSON): {primaryHash: number; recentSea
orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY}:${query.sortBy}`;
orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_ORDER}:${query.sortOrder}`;
if (query.policyID) {
orderedQuery += ` ${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${query.policyID} `;
orderedQuery += ` ${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${Array.isArray(query.policyID) ? query.policyID.join(',') : query.policyID} `;
}
const primaryHash = hashText(orderedQuery, 2 ** 32);

Expand Down Expand Up @@ -334,6 +335,11 @@ function buildSearchQueryJSON(query: SearchQueryString) {
result.hash = primaryHash;
result.recentSearchHash = recentSearchHash;

if (result.policyID && typeof result.policyID === 'string') {
// Ensure policyID is always an array for consistency
result.policyID = [result.policyID];
}

return result;
} catch (e) {
console.error(`Error when parsing SearchQuery: "${query}"`, e);
Expand Down Expand Up @@ -364,7 +370,7 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
}

if (queryJSON?.policyID) {
queryParts.push(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${queryJSON.policyID}`);
queryParts.push(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${Array.isArray(queryJSON.policyID) ? queryJSON.policyID.join(',') : queryJSON.policyID}`);
}

if (!queryJSON) {
Expand Down Expand Up @@ -431,8 +437,8 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
}

if (policyID) {
const sanitizedPolicyID = sanitizeSearchValue(policyID);
filtersString.push(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${sanitizedPolicyID}`);
const sanitizedPolicyIDs = Array.isArray(policyID) ? policyID.map((id) => sanitizeSearchValue(id)).join(',') : sanitizeSearchValue(policyID);
filtersString.push(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${sanitizedPolicyIDs}`);
}

const mappedFilters = Object.entries(otherFilters)
Expand Down Expand Up @@ -481,6 +487,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
filterKey === FILTER_KEYS.FEED ||
filterKey === FILTER_KEYS.IN ||
filterKey === FILTER_KEYS.ASSIGNEE ||
filterKey === FILTER_KEYS.POLICY_ID ||
filterKey === FILTER_KEYS.EXPORTER) &&
Array.isArray(filterValue) &&
filterValue.length > 0
Expand Down Expand Up @@ -510,6 +517,18 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
return filtersString.filter(Boolean).join(' ').trim();
}

function getAllPolicyValues<T extends OnyxCollectionKey>(
policyID: string[] | undefined,
key: T,
policyData: OnyxCollection<OnyxCollectionValuesMapping[T]>,
): Array<OnyxCollectionValuesMapping[T]> {
if (!policyData || !policyID) {
return [];
}

return policyID.map((id) => policyData?.[`${key}${id}`]).filter((data) => !!data) as Array<OnyxCollectionValuesMapping[T]>;
}

/**
* Generates object with search filter values, in a format that can be consumed by SearchAdvancedFiltersForm.
* Main usage of this is to generate the initial values for AdvancedFilters from existing query.
Expand Down Expand Up @@ -578,7 +597,9 @@ function buildFilterFormValuesFromQuery(
}
if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG) {
const tags = policyID
? getTagNamesFromTagsLists(policyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})
? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_TAGS, policyTags)
.map((tagList) => getTagNamesFromTagsLists(tagList ?? {}))
.flat()
: Object.values(policyTags ?? {})
.filter((item) => !!item)
.map((tagList) => getTagNamesFromTagsLists(tagList ?? {}))
Expand All @@ -589,7 +610,9 @@ function buildFilterFormValuesFromQuery(
}
if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY) {
const categories = policyID
? Object.values(policyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}).map((category) => category.name)
? getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_CATEGORIES, policyCategories)
.map((item) => Object.values(item ?? {}).map((category) => category.name))
.flat()
: Object.values(policyCategories ?? {})
.map((item) => Object.values(item ?? {}).map((category) => category.name))
.flat();
Expand Down Expand Up @@ -754,8 +777,7 @@ function buildUserReadableQueryString(
}

if (policyID) {
const workspace = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.name ?? policyID;
title += ` workspace:${sanitizeSearchValue(workspace)}`;
title += ` workspace:${policyID.map((id) => sanitizeSearchValue(policies?.[`${ONYXKEYS.COLLECTION.POLICY}${id}`]?.name ?? id)).join(',')}`;
}

for (const filterObject of filters) {
Expand Down Expand Up @@ -991,4 +1013,5 @@ export {
isDefaultExpensesQuery,
sortOptionsWithEmptyValue,
shouldHighlight,
getAllPolicyValues,
};
2 changes: 1 addition & 1 deletion src/pages/ReportChangeWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function ReportChangeWorkspacePage({report, route}: ReportChangeWorkspacePagePro
policies,
currentUserLogin: session?.email,
shouldShowPendingDeletePolicy: false,
selectedPolicyID: report.policyID,
selectedPolicyIDs: report.policyID ? [report.policyID] : undefined,
searchTerm: debouncedSearchTerm,
additionalFilter: (newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, policies),
});
Expand Down
25 changes: 18 additions & 7 deletions src/pages/Search/AdvancedSearchFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ import Navigation from '@libs/Navigation/Navigation';
import {createDisplayName} from '@libs/PersonalDetailsUtils';
import {getAllTaxRates, getCleanedTagName, getTagNamesFromTagsLists, isPolicyFeatureEnabled} from '@libs/PolicyUtils';
import {getReportName} from '@libs/ReportUtils';
import {buildCannedSearchQuery, buildQueryStringFromFilterFormValues, buildSearchQueryJSON, isCannedSearchQuery, isSearchDatePreset, sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils';
import {
buildCannedSearchQuery,
buildQueryStringFromFilterFormValues,
buildSearchQueryJSON,
getAllPolicyValues,
isCannedSearchQuery,
isSearchDatePreset,
sortOptionsWithEmptyValue,
} from '@libs/SearchQueryUtils';
import {getExpenseTypeTranslationKey, getStatusOptions} from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
Expand Down Expand Up @@ -195,7 +203,10 @@ const baseFilterConfig = {
};

function getFilterWorkspaceDisplayTitle(filters: SearchAdvancedFiltersForm, policies: WorkspaceListItem[]) {
return policies.filter((value) => value.policyID === filters.policyID).at(0)?.text;
return policies
.filter((value) => value.policyID && filters.policyID?.includes(value.policyID))
.map((value) => value.text)
.join(', ');
}

function getFilterCardDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, cards: CardList, translate: LocaleContextProps['translate']) {
Expand Down Expand Up @@ -434,9 +445,9 @@ function AdvancedSearchFilters() {
}),
),
});
const singlePolicyCategories = allPolicyCategories[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`];
const selectedPolicyCategories = getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_CATEGORIES, allPolicyCategories);
const [allPolicyTagLists = {}] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {canBeMissing: false});
const singlePolicyTagLists = allPolicyTagLists[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`];
const selectedPolicyTagLists = getAllPolicyValues(policyID, ONYXKEYS.COLLECTION.POLICY_TAGS, allPolicyTagLists);
const tagListsUnpacked = Object.values(allPolicyTagLists ?? {})
.filter((item): item is NonNullable<PolicyTagLists> => !!item)
.map(getTagNamesFromTagsLists)
Expand All @@ -448,7 +459,7 @@ function AdvancedSearchFilters() {
policies,
currentUserLogin,
shouldShowPendingDeletePolicy: false,
selectedPolicyID: undefined,
selectedPolicyIDs: undefined,
searchTerm: '',
});

Expand All @@ -465,8 +476,8 @@ function AdvancedSearchFilters() {
isFeatureEnabledInPolicies(policies, CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED);
const areTaxEnabled = isFeatureEnabledInPolicies(policies, CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED);

const shouldDisplayCategoryFilter = shouldDisplayFilter(nonPersonalPolicyCategoryCount, areCategoriesEnabled, !!singlePolicyCategories);
const shouldDisplayTagFilter = shouldDisplayFilter(tagListsUnpacked.length, areTagsEnabled, !!singlePolicyTagLists);
const shouldDisplayCategoryFilter = shouldDisplayFilter(nonPersonalPolicyCategoryCount, areCategoriesEnabled, !!selectedPolicyCategories);
const shouldDisplayTagFilter = shouldDisplayFilter(tagListsUnpacked.length, areTagsEnabled, !!selectedPolicyTagLists);
const shouldDisplayCardFilter = shouldDisplayFilter(Object.keys(allCards).length, areCardsEnabled);
const shouldDisplayTaxFilter = shouldDisplayFilter(Object.keys(taxRates).length, areTaxEnabled);
const shouldDisplayWorkspaceFilter = workspaces.some((section) => section.data.length !== 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {updateAdvancedFilters} from '@userActions/Search';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {PolicyCategory} from '@src/types/onyx';

function SearchFiltersCategoryPage() {
const styles = useThemeStyles();
Expand All @@ -23,22 +24,25 @@ function SearchFiltersCategoryPage() {
}
return {name: category, value: category};
});
// eslint-disable-next-line rulesdir/no-default-id-values
const policyID = searchAdvancedFiltersForm?.policyID ?? '-1';
const policyIDs = searchAdvancedFiltersForm?.policyID ?? [];
const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, {canBeMissing: true});
const singlePolicyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`];
const selectedPoliciesCategories: PolicyCategory[] = Object.keys(allPolicyCategories ?? {})
.filter((key) => policyIDs?.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`)?.includes(key))
?.map((key) => Object.values(allPolicyCategories?.[key] ?? {}))
.flat();

const categoryItems = useMemo(() => {
const items = [{name: translate('search.noCategory'), value: CONST.SEARCH.CATEGORY_EMPTY_VALUE as string}];
if (!singlePolicyCategories) {
const uniqueCategoryNames = new Set<string>();
const uniqueCategoryNames = new Set<string>();

if (!selectedPoliciesCategories) {
Object.values(allPolicyCategories ?? {}).map((policyCategories) => Object.values(policyCategories ?? {}).forEach((category) => uniqueCategoryNames.add(category.name)));
items.push(...Array.from(uniqueCategoryNames).map((categoryName) => ({name: categoryName, value: categoryName})));
} else {
items.push(...Object.values(singlePolicyCategories ?? {}).map((category) => ({name: category.name, value: category.name})));
selectedPoliciesCategories.forEach((category) => uniqueCategoryNames.add(category.name));
}
items.push(...Array.from(uniqueCategoryNames).map((categoryName) => ({name: categoryName, value: categoryName})));
return items;
}, [allPolicyCategories, singlePolicyCategories, translate]);
}, [allPolicyCategories, selectedPoliciesCategories, translate]);

const onSaveSelection = useCallback((values: string[]) => updateAdvancedFilters({category: values}), []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,30 @@ function SearchFiltersTagPage() {
}
return {name: getCleanedTagName(tag), value: tag};
});
const policyID = searchAdvancedFiltersForm?.policyID;
const policyIDs = searchAdvancedFiltersForm?.policyID ?? [];
const [allPolicyTagLists = {}] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {canBeMissing: true});
const singlePolicyTagLists = allPolicyTagLists[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`];
const selectedPoliciesTagLists = Object.keys(allPolicyTagLists ?? {})
.filter((key) => policyIDs?.map((policyID) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`)?.includes(key))
?.map((key) => getTagNamesFromTagsLists(allPolicyTagLists?.[key] ?? {}))
.flat();

const tagItems = useMemo(() => {
const items = [{name: translate('search.noTag'), value: CONST.SEARCH.TAG_EMPTY_VALUE as string}];
if (!singlePolicyTagLists) {
const uniqueTagNames = new Set<string>();
const uniqueTagNames = new Set<string>();

if (!selectedPoliciesTagLists) {
const tagListsUnpacked = Object.values(allPolicyTagLists ?? {}).filter((item) => !!item) as PolicyTagLists[];
tagListsUnpacked
.map(getTagNamesFromTagsLists)
.flat()
.forEach((tag) => uniqueTagNames.add(tag));
items.push(...Array.from(uniqueTagNames).map((tagName) => ({name: getCleanedTagName(tagName), value: tagName})));
} else {
items.push(...getTagNamesFromTagsLists(singlePolicyTagLists).map((name) => ({name: getCleanedTagName(name), value: name})));
selectedPoliciesTagLists.forEach((tag) => uniqueTagNames.add(tag));
}
items.push(...Array.from(uniqueTagNames).map((tagName) => ({name: getCleanedTagName(tagName), value: tagName})));

return items;
}, [allPolicyTagLists, singlePolicyTagLists, translate]);
}, [allPolicyTagLists, selectedPoliciesTagLists, translate]);

const updateTagFilter = useCallback((values: string[]) => updateAdvancedFilters({tag: values}), []);

Expand Down
Loading
Loading