Skip to content
3 changes: 2 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,7 @@ const CONST = {
TABLE_REPORT_VIEW: 'tableReportView',
HELP_SIDE_PANEL: 'newDotHelpSidePanel',
RECEIPT_LINE_ITEMS: 'receiptLineItems',
LEFT_HAND_BAR: 'leftHandBar',
},
BUTTON_STATES: {
DEFAULT: 'default',
Expand Down Expand Up @@ -6442,7 +6443,6 @@ const CONST = {
STATUS: 'status',
SORT_BY: 'sortBy',
SORT_ORDER: 'sortOrder',
POLICY_ID: 'policyID',
GROUP_BY: 'groupBy',
},
SYNTAX_FILTER_KEYS: {
Expand All @@ -6467,6 +6467,7 @@ const CONST = {
PAID: 'paid',
EXPORTED: 'exported',
POSTED: 'posted',
POLICY_ID: 'policyID',
},
EMPTY_VALUE: 'none',
SEARCH_ROUTER_ITEM_TYPE: {
Expand Down
1 change: 1 addition & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const ROUTES = {
SEARCH_ADVANCED_FILTERS_PAID: 'search/filters/paid',
SEARCH_ADVANCED_FILTERS_EXPORTED: 'search/filters/exported',
SEARCH_ADVANCED_FILTERS_POSTED: 'search/filters/posted',
SEARCH_ADVANCED_FILTERS_WORKSPACE: 'search/filters/workspace',
SEARCH_REPORT: {
route: 'search/view/:reportID/:reportActionID?',
getRoute: ({
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const SCREENS = {
ADVANCED_FILTERS_TAG_RHP: 'Search_Advanced_Filters_Tag_RHP',
ADVANCED_FILTERS_FROM_RHP: 'Search_Advanced_Filters_From_RHP',
ADVANCED_FILTERS_TO_RHP: 'Search_Advanced_Filters_To_RHP',
ADVANCED_FILTERS_WORKSPACE_RHP: 'Search_Advanced_Filters_Workspace_RHP',
SAVED_SEARCH_RENAME_RHP: 'Search_Saved_Search_Rename_RHP',
ADVANCED_FILTERS_IN_RHP: 'Search_Advanced_Filters_In_RHP',
TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP',
Expand Down
2 changes: 1 addition & 1 deletion src/components/Navigation/BottomTabBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function BottomTabBar({selectedTab, isTooltipAllowed = false}: BottomTabBarProps
}
}
// when navigating to search we might have an activePolicyID set from workspace switcher
const query = activeWorkspaceID ? `${defaultCannedQuery} ${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${activeWorkspaceID}` : defaultCannedQuery;
const query = activeWorkspaceID ? `${defaultCannedQuery} ${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${activeWorkspaceID}` : defaultCannedQuery;
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query}));
});
}, [activeWorkspaceID, selectedTab]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {SearchQueryItem} from '@components/SelectionList/Search/SearchQuery
import type {SelectionListHandle} from '@components/SelectionList/types';
import HelpButton from '@components/SidePanel/HelpComponents/HelpButton';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -31,7 +32,14 @@ import Navigation from '@libs/Navigation/Navigation';
import {getAllTaxRates} from '@libs/PolicyUtils';
import type {OptionData} from '@libs/ReportUtils';
import {getAutocompleteQueryWithComma, getQueryWithoutAutocompletedPart} from '@libs/SearchAutocompleteUtils';
import {buildUserReadableQueryString, getQueryWithUpdatedValues, isDefaultExpensesQuery, sanitizeSearchValue} from '@libs/SearchQueryUtils';
import {
buildUserReadableQueryString,
buildUserReadableQueryStringWithPolicyID,
getQueryWithUpdatedValues,
isDefaultExpensesQuery,
isDefaultExpensesQueryWithPolicyIDCheck,
sanitizeSearchValue,
} from '@libs/SearchQueryUtils';
import StringUtils from '@libs/StringUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
Expand All @@ -54,12 +62,14 @@ type SearchPageHeaderInputProps = {

function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRouterList, onSearchRouterFocus, searchName, inputRightComponent}: SearchPageHeaderInputProps) {
const {translate} = useLocalize();
const {canUseLeftHandBar} = usePermissions();
const [showPopupButton, setShowPopupButton] = useState(true);
const styles = useThemeStyles();
const theme = useTheme();
const {shouldUseNarrowLayout: displayNarrowHeader} = useResponsiveLayout();
const personalDetails = usePersonalDetails();
const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const taxRates = useMemo(() => getAllTaxRates(), []);
const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST);
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST);
Expand All @@ -68,9 +78,11 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
return getCardFeedNamesWithType({workspaceCardFeeds, translate});
}, [translate, workspaceCardFeeds]);
const {inputQuery: originalInputQuery} = queryJSON;
const isDefaultQuery = isDefaultExpensesQuery(queryJSON);
const isDefaultQuery = canUseLeftHandBar ? isDefaultExpensesQueryWithPolicyIDCheck(queryJSON) : isDefaultExpensesQuery(queryJSON);
const [shouldUseAnimation, setShouldUseAnimation] = useState(false);
const queryText = buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType);
const queryText = canUseLeftHandBar
? buildUserReadableQueryStringWithPolicyID(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType, policies)
: buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType);

// The actual input text that the user sees
const [textInputValue, setTextInputValue] = useState(isDefaultQuery ? '' : queryText);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {SearchQueryJSON} from '@components/Search/types';
import ThreeDotsMenu from '@components/ThreeDotsMenu';
import useDeleteSavedSearch from '@hooks/useDeleteSavedSearch';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useSingleExecution from '@hooks/useSingleExecution';
import useTheme from '@hooks/useTheme';
Expand All @@ -20,7 +21,13 @@ import {getCardFeedNamesWithType} from '@libs/CardFeedUtils';
import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getAllTaxRates} from '@libs/PolicyUtils';
import {buildSearchQueryJSON, buildUserReadableQueryString, isCannedSearchQuery} from '@libs/SearchQueryUtils';
import {
buildSearchQueryJSON,
buildUserReadableQueryString,
buildUserReadableQueryStringWithPolicyID,
isCannedSearchQuery,
isCannedSearchQueryWithPolicyIDCheck,
} from '@libs/SearchQueryUtils';
import {createBaseSavedSearchMenuItem, createTypeMenuItems, getOverflowMenu as getOverflowMenuUtil} from '@libs/SearchUIUtils';
import variables from '@styles/variables';
import * as Expensicons from '@src/components/Icon/Expensicons';
Expand Down Expand Up @@ -58,6 +65,7 @@ function SearchTypeMenuPopover({queryJSON, searchName}: SearchTypeMenuNarrowProp
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST);
const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]);
const {unmodifiedPaddings} = useSafeAreaPaddings();
const {canUseLeftHandBar} = usePermissions();
const shouldGroupByReports = groupBy === CONST.SEARCH.GROUP_BY.REPORTS;
const cardFeedNamesWithType = useMemo(() => {
return getCardFeedNamesWithType({workspaceCardFeeds, translate});
Expand All @@ -78,9 +86,16 @@ function SearchTypeMenuPopover({queryJSON, searchName}: SearchTypeMenuNarrowProp
}, 100);
}, []);

const getBuildUserReadableQueryString = useCallback(() => {
if (canUseLeftHandBar) {
return buildUserReadableQueryStringWithPolicyID(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType, allPolicies);
}
return buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType);
}, [allCards, allPolicies, canUseLeftHandBar, cardFeedNamesWithType, personalDetails, queryJSON, reports, taxRates]);

const typeMenuItems = useMemo(() => createTypeMenuItems(allPolicies, session?.email), [allPolicies, session?.email]);
const isCannedQuery = isCannedSearchQuery(queryJSON);
const title = searchName ?? (isCannedQuery ? undefined : buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType));
const isCannedQuery = canUseLeftHandBar ? isCannedSearchQueryWithPolicyIDCheck(queryJSON) : isCannedSearchQuery(queryJSON);
const title = searchName ?? (isCannedQuery ? undefined : getBuildUserReadableQueryString());
const activeItemIndex = isCannedQuery ? typeMenuItems.findIndex((item) => item.type === queryJSON.type) : -1;

const getOverflowMenu = useCallback(
Expand All @@ -93,7 +108,11 @@ function SearchTypeMenuPopover({queryJSON, searchName}: SearchTypeMenuNarrowProp
let savedSearchTitle = item.name;
if (savedSearchTitle === item.query) {
const jsonQuery = buildSearchQueryJSON(item.query) ?? ({} as SearchQueryJSON);
savedSearchTitle = buildUserReadableQueryString(jsonQuery, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType);
if (canUseLeftHandBar) {
savedSearchTitle = buildUserReadableQueryStringWithPolicyID(jsonQuery, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType, allPolicies);
} else {
savedSearchTitle = buildUserReadableQueryString(jsonQuery, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType);
}
}
const isItemFocused = Number(key) === hash;
const baseMenuItem: SavedSearchMenuItem = createBaseSavedSearchMenuItem(item, key, index, savedSearchTitle, isItemFocused);
Expand Down Expand Up @@ -124,7 +143,7 @@ function SearchTypeMenuPopover({queryJSON, searchName}: SearchTypeMenuNarrowProp
shouldIconUseAutoWidthStyle: false,
};
},
[hash, getOverflowMenu, styles.textSupporting, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType],
[hash, getOverflowMenu, styles.textSupporting, canUseLeftHandBar, personalDetails, reports, taxRates, allCards, cardFeedNamesWithType, allPolicies],
);

const savedSearchItems = useMemo(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ type SearchFilterKey =
| ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS>
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID
| typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID
| typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY;

type UserFriendlyKey = ValueOf<typeof CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,7 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator<Searc
[SCREENS.SEARCH.ADVANCED_FILTERS_FROM_RHP]: () => require<ReactComponentModule>('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersFromPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP]: () => require<ReactComponentModule>('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersToPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP]: () => require<ReactComponentModule>('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersInPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_WORKSPACE_RHP]: () => require<ReactComponentModule>('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersWorkspacePage').default,
});

const SearchSavedSearchModalStackNavigator = createModalStackNavigator<SearchSavedSearchParamList>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const SEARCH_TO_RHP: string[] = [
SCREENS.SEARCH.ADVANCED_FILTERS_FROM_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_WORKSPACE_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP,
SCREENS.SEARCH.SAVED_SEARCH_RENAME_RHP,
];
Expand Down
1 change: 1 addition & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,7 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
[SCREENS.SEARCH.ADVANCED_FILTERS_FROM_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_FROM,
[SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_TO,
[SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_IN,
[SCREENS.SEARCH.ADVANCED_FILTERS_WORKSPACE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_WORKSPACE,
},
},
[SCREENS.RIGHT_MODAL.SEARCH_SAVED_SEARCH]: {
Expand Down
5 changes: 5 additions & 0 deletions src/libs/Permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ function canUseProhibitedExpenses(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.RECEIPT_LINE_ITEMS) || canUseAllBetas(betas);
}

function canUseLeftHandBar(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.LEFT_HAND_BAR) || canUseAllBetas(betas);
}

export default {
canUseDefaultRooms,
canUseLinkPreviews,
Expand All @@ -81,4 +85,5 @@ export default {
canUseHelpSidePanel,
canUseTalkToAISales,
canUseProhibitedExpenses,
canUseLeftHandBar,
};
99 changes: 97 additions & 2 deletions src/libs/SearchQueryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,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_ROOT_KEYS.POLICY_ID}:${query.policyID} `;
orderedQuery += ` ${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${query.policyID} `;
}
const primaryHash = hashText(orderedQuery, 2 ** 32);

Expand Down Expand Up @@ -305,6 +305,10 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
}
}

if (queryJSON?.policyID) {
queryParts.push(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${queryJSON.policyID}`);
}

if (!queryJSON) {
return queryParts.join(' ');
}
Expand Down Expand Up @@ -345,7 +349,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc

if (policyID) {
const sanitizedPolicyID = sanitizeSearchValue(policyID);
filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${sanitizedPolicyID}`);
filtersString.push(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${sanitizedPolicyID}`);
}

const mappedFilters = Object.entries(otherFilters)
Expand Down Expand Up @@ -588,6 +592,71 @@ function getFilterDisplayValue(
return filterValue;
}

/**
* A copy of `buildUserReadableQueryString` handling the policy ID, used if you have access to the leftHandBar beta.
* When this beta is no longer needed, this method will be renamed to `buildUserReadableQueryString` and will replace the old method.
* Formats a given `SearchQueryJSON` object into the human-readable string version of query.
* This format of query is the one which we want to display to users.
* We try to replace every numeric id value with a display version of this value,
* So: user IDs get turned into emails, report ids into report names etc.
*/
function buildUserReadableQueryStringWithPolicyID(
queryJSON: SearchQueryJSON,
PersonalDetails: OnyxTypes.PersonalDetailsList | undefined,
reports: OnyxCollection<OnyxTypes.Report>,
taxRates: Record<string, string[]>,
cardList: OnyxTypes.CardList,
cardFeedNamesWithType: CardFeedNamesWithType,
policies: OnyxCollection<OnyxTypes.Policy>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only use policies to get the name of specific policyID, so I think we should pass the policyName: string

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I implemented it similarly to other filters, we pass the entire collection and extract data for specific ids/ids. Also, it will be useful when we implement a multiselect workspace filter

) {
const {type, status, groupBy, policyID} = queryJSON;
const filters = queryJSON.flatFilters;

let title = `type:${type} status:${Array.isArray(status) ? status.join(',') : status}`;

if (groupBy) {
title += ` group-by:${groupBy}`;
}

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

for (const filterObject of filters) {
const key = filterObject.key;
const queryFilter = filterObject.filters;

let displayQueryFilters: QueryFilter[] = [];
if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) {
const taxRateIDs = queryFilter.map((filter) => filter.value.toString());
const taxRateNames = taxRateIDs
.map((id) => {
const taxRate = Object.entries(taxRates)
.filter(([, IDs]) => IDs.includes(id))
.map(([name]) => name);
return taxRate.length > 0 ? taxRate : id;
})
.flat();

const uniqueTaxRateNames = [...new Set(taxRateNames)];

displayQueryFilters = uniqueTaxRateNames.map((taxRate) => ({
operator: queryFilter.at(0)?.operator ?? CONST.SEARCH.SYNTAX_OPERATORS.AND,
value: taxRate,
}));
} else {
displayQueryFilters = queryFilter.map((filter) => ({
operator: filter.operator,
value: getFilterDisplayValue(key, filter.value.toString(), PersonalDetails, reports, cardList, cardFeedNamesWithType),
}));
}
title += buildFilterValuesString(getUserFriendlyKey(key), displayQueryFilters);
}

return title;
}

/**
* Formats a given `SearchQueryJSON` object into the human-readable string version of query.
* This format of query is the one which we want to display to users.
Expand Down Expand Up @@ -680,6 +749,20 @@ function buildCannedSearchQuery({
return buildSearchQueryString(normalizedQueryJSON);
}

/**
* A copy of `isCannedSearchQuery` handling the policy ID, used if you have access to the leftHandBar beta.
* When this beta is no longer needed, this method will be renamed to `isCannedSearchQuery` and will replace the old method.
*
* Returns whether a given search query is a Canned query.
*
* Canned queries are simple predefined queries, that are defined only using type and status and no additional filters.
* In addition, they can contain an optional policyID.
* For example: "type:trip status:all" is a canned query.
*/
function isCannedSearchQueryWithPolicyIDCheck(queryJSON: SearchQueryJSON) {
return !queryJSON.filters && !queryJSON.policyID;
}

/**
* Returns whether a given search query is a Canned query.
*
Expand All @@ -691,6 +774,15 @@ function isCannedSearchQuery(queryJSON: SearchQueryJSON) {
return !queryJSON.filters;
}

/**
* A copy of `isDefaultExpensesQuery` handling the policy ID, used if you have access to the leftHandBar beta.
* When this beta is no longer needed, this method will be renamed to `isDefaultExpensesQuery` and will replace the old method.
*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be done in this PR: #59943

*/
function isDefaultExpensesQueryWithPolicyIDCheck(queryJSON: SearchQueryJSON) {
return queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE && queryJSON.status === CONST.SEARCH.STATUS.EXPENSE.ALL && !queryJSON.filters && !queryJSON.groupBy && !queryJSON.policyID;
}

function isDefaultExpensesQuery(queryJSON: SearchQueryJSON) {
return queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE && queryJSON.status === CONST.SEARCH.STATUS.EXPENSE.ALL && !queryJSON.filters && !queryJSON.groupBy;
}
Expand Down Expand Up @@ -765,14 +857,17 @@ export {
buildSearchQueryJSON,
buildSearchQueryString,
buildUserReadableQueryString,
buildUserReadableQueryStringWithPolicyID,
getFilterDisplayValue,
buildQueryStringFromFilterFormValues,
buildFilterFormValuesFromQuery,
getPolicyIDFromSearchQuery,
buildCannedSearchQuery,
isCannedSearchQuery,
isCannedSearchQueryWithPolicyIDCheck,
sanitizeSearchValue,
getQueryWithUpdatedValues,
getUserFriendlyKey,
isDefaultExpensesQuery,
isDefaultExpensesQueryWithPolicyIDCheck,
};
Loading