Skip to content

Commit

Permalink
Merge pull request #49694 from software-mansion-labs/kicu/49637-polic…
Browse files Browse the repository at this point in the history
…yid-search

Preserve policyID when navigating to canned search queries
  • Loading branch information
luacmartins authored Sep 30, 2024
2 parents a2e1008 + d9741fd commit 88beaa6
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 65 deletions.
5 changes: 3 additions & 2 deletions src/components/Search/SearchPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,9 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
}

const onPress = () => {
const values = SearchUtils.buildFilterFormValuesFromQuery(queryJSON);
SearchActions.updateAdvancedFilters(values);
const filterFormValues = SearchUtils.buildFilterFormValuesFromQuery(queryJSON);
SearchActions.updateAdvancedFilters(filterFormValues);

Navigation.navigate(ROUTES.SEARCH_ADVANCED_FILTERS);
};

Expand Down
88 changes: 45 additions & 43 deletions src/components/Search/SearchStatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,125 +17,126 @@ import type {TranslationPaths} from '@src/languages/types';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
import type IconAsset from '@src/types/utils/IconAsset';
import {useSearchContext} from './SearchContext';
import type {ChatSearchStatus, ExpenseSearchStatus, InvoiceSearchStatus, SearchQueryString, SearchStatus, TripSearchStatus} from './types';
import type {ChatSearchStatus, ExpenseSearchStatus, InvoiceSearchStatus, SearchStatus, TripSearchStatus} from './types';

type SearchStatusBarProps = {
type: SearchDataTypes;
status: SearchStatus;
policyID: string | undefined;
onStatusChange?: () => void;
};

const expenseOptions: Array<{key: ExpenseSearchStatus; icon: IconAsset; text: TranslationPaths; query: SearchQueryString}> = [
const expenseOptions: Array<{status: ExpenseSearchStatus; type: SearchDataTypes; icon: IconAsset; text: TranslationPaths}> = [
{
key: CONST.SEARCH.STATUS.EXPENSE.ALL,
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
status: CONST.SEARCH.STATUS.EXPENSE.ALL,
icon: Expensicons.All,
text: 'common.all',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.ALL),
},
{
key: CONST.SEARCH.STATUS.EXPENSE.DRAFTS,
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
status: CONST.SEARCH.STATUS.EXPENSE.DRAFTS,
icon: Expensicons.Pencil,
text: 'common.drafts',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.DRAFTS),
},
{
key: CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING,
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
status: CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING,
icon: Expensicons.Hourglass,
text: 'common.outstanding',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING),
},
{
key: CONST.SEARCH.STATUS.EXPENSE.APPROVED,
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
status: CONST.SEARCH.STATUS.EXPENSE.APPROVED,
icon: Expensicons.ThumbsUp,
text: 'iou.approved',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.APPROVED),
},
{
key: CONST.SEARCH.STATUS.EXPENSE.PAID,
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
status: CONST.SEARCH.STATUS.EXPENSE.PAID,
icon: Expensicons.MoneyBag,
text: 'iou.settledExpensify',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.PAID),
},
];

const invoiceOptions: Array<{key: InvoiceSearchStatus; icon: IconAsset; text: TranslationPaths; query: SearchQueryString}> = [
const invoiceOptions: Array<{type: SearchDataTypes; status: InvoiceSearchStatus; icon: IconAsset; text: TranslationPaths}> = [
{
key: CONST.SEARCH.STATUS.INVOICE.ALL,
type: CONST.SEARCH.DATA_TYPES.INVOICE,
status: CONST.SEARCH.STATUS.INVOICE.ALL,
icon: Expensicons.All,
text: 'common.all',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.INVOICE, CONST.SEARCH.STATUS.INVOICE.ALL),
},
{
key: CONST.SEARCH.STATUS.INVOICE.OUTSTANDING,
type: CONST.SEARCH.DATA_TYPES.INVOICE,
status: CONST.SEARCH.STATUS.INVOICE.OUTSTANDING,
icon: Expensicons.Hourglass,
text: 'common.outstanding',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.INVOICE, CONST.SEARCH.STATUS.INVOICE.OUTSTANDING),
},
{
key: CONST.SEARCH.STATUS.INVOICE.PAID,
type: CONST.SEARCH.DATA_TYPES.INVOICE,
status: CONST.SEARCH.STATUS.INVOICE.PAID,
icon: Expensicons.MoneyBag,
text: 'iou.settledExpensify',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.INVOICE, CONST.SEARCH.STATUS.INVOICE.PAID),
},
];

const tripOptions: Array<{key: TripSearchStatus; icon: IconAsset; text: TranslationPaths; query: SearchQueryString}> = [
const tripOptions: Array<{type: SearchDataTypes; status: TripSearchStatus; icon: IconAsset; text: TranslationPaths}> = [
{
key: CONST.SEARCH.STATUS.TRIP.ALL,
type: CONST.SEARCH.DATA_TYPES.TRIP,
status: CONST.SEARCH.STATUS.TRIP.ALL,
icon: Expensicons.All,
text: 'common.all',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.STATUS.TRIP.ALL),
},
{
key: CONST.SEARCH.STATUS.TRIP.CURRENT,
type: CONST.SEARCH.DATA_TYPES.TRIP,
status: CONST.SEARCH.STATUS.TRIP.CURRENT,
icon: Expensicons.Calendar,
text: 'search.filters.current',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.STATUS.TRIP.CURRENT),
},
{
key: CONST.SEARCH.STATUS.TRIP.PAST,
type: CONST.SEARCH.DATA_TYPES.TRIP,
status: CONST.SEARCH.STATUS.TRIP.PAST,
icon: Expensicons.History,
text: 'search.filters.past',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.STATUS.TRIP.PAST),
},
];

const chatOptions: Array<{key: ChatSearchStatus; icon: IconAsset; text: TranslationPaths; query: SearchQueryString}> = [
const chatOptions: Array<{type: SearchDataTypes; status: ChatSearchStatus; icon: IconAsset; text: TranslationPaths}> = [
{
key: CONST.SEARCH.STATUS.CHAT.ALL,
type: CONST.SEARCH.DATA_TYPES.CHAT,
status: CONST.SEARCH.STATUS.CHAT.ALL,
icon: Expensicons.All,
text: 'common.all',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.CHAT.ALL),
},
{
key: CONST.SEARCH.STATUS.CHAT.UNREAD,
type: CONST.SEARCH.DATA_TYPES.CHAT,
status: CONST.SEARCH.STATUS.CHAT.UNREAD,
icon: Expensicons.ChatBubbleUnread,
text: 'common.unread',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.CHAT.UNREAD),
},
{
key: CONST.SEARCH.STATUS.CHAT.SENT,
type: CONST.SEARCH.DATA_TYPES.CHAT,
status: CONST.SEARCH.STATUS.CHAT.SENT,
icon: Expensicons.Send,
text: 'common.sent',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.CHAT.SENT),
},
{
key: CONST.SEARCH.STATUS.CHAT.ATTACHMENTS,
type: CONST.SEARCH.DATA_TYPES.CHAT,
status: CONST.SEARCH.STATUS.CHAT.ATTACHMENTS,
icon: Expensicons.Document,
text: 'common.attachments',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.CHAT.ATTACHMENTS),
},
{
key: CONST.SEARCH.STATUS.CHAT.LINKS,
type: CONST.SEARCH.DATA_TYPES.CHAT,
status: CONST.SEARCH.STATUS.CHAT.LINKS,
icon: Expensicons.Paperclip,
text: 'common.links',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.CHAT.LINKS),
},
{
key: CONST.SEARCH.STATUS.CHAT.PINNED,
type: CONST.SEARCH.DATA_TYPES.CHAT,
status: CONST.SEARCH.STATUS.CHAT.PINNED,
icon: Expensicons.Pin,
text: 'search.filters.pinned',
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.CHAT.PINNED),
},
];

Expand All @@ -153,7 +154,7 @@ function getOptions(type: SearchDataTypes) {
}
}

function SearchStatusBar({type, status, onStatusChange}: SearchStatusBarProps) {
function SearchStatusBar({type, status, policyID, onStatusChange}: SearchStatusBarProps) {
const {singleExecution} = useSingleExecution();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
Expand All @@ -178,15 +179,16 @@ function SearchStatusBar({type, status, onStatusChange}: SearchStatusBarProps) {
{options.map((item, index) => {
const onPress = singleExecution(() => {
onStatusChange?.();
Navigation.setParams({q: item.query});
const query = SearchUtils.buildCannedSearchQuery({type: item.type, status: item.status, policyID});
Navigation.setParams({q: query});
});
const isActive = status === item.key;
const isActive = status === item.status;
const isFirstItem = index === 0;
const isLastItem = index === options.length - 1;

return (
<Button
key={item.key}
key={item.status}
onLayout={(e) => {
if (!isActive || isScrolledRef.current || !('left' in e.nativeEvent.layout)) {
return;
Expand Down
3 changes: 1 addition & 2 deletions src/hooks/useDeleteSavedSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import ConfirmModal from '@components/ConfirmModal';
import Navigation from '@libs/Navigation/Navigation';
import * as SearchUtils from '@libs/SearchUtils';
import * as SearchActions from '@userActions/Search';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import useLocalize from './useLocalize';

Expand All @@ -23,7 +22,7 @@ export default function useDeleteSavedSearch() {
SearchActions.clearAdvancedFilters();
Navigation.navigate(
ROUTES.SEARCH_CENTRAL_PANE.getRoute({
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.ALL),
query: SearchUtils.buildCannedSearchQuery(),
}),
);
};
Expand Down
24 changes: 21 additions & 3 deletions src/libs/SearchUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
*/
function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvancedFiltersForm>) {
// We separate type and status filters from other filters to maintain hashes consistency for saved searches
const {type, status, ...otherFilters} = filterValues;
const {type, status, policyID, ...otherFilters} = filterValues;
const filtersString: string[] = [];

if (type) {
Expand All @@ -598,6 +598,11 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS}:${sanitizedStatus}`);
}

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

const mappedFilters = Object.entries(otherFilters)
.map(([filterKey, filterValue]) => {
if ((filterKey === FILTER_KEYS.MERCHANT || filterKey === FILTER_KEYS.DESCRIPTION || filterKey === FILTER_KEYS.REPORT_ID) && filterValue) {
Expand Down Expand Up @@ -694,6 +699,9 @@ function buildFilterFormValuesFromQuery(queryJSON: SearchQueryJSON) {

filtersForm[FILTER_KEYS.TYPE] = queryJSON.type;
filtersForm[FILTER_KEYS.STATUS] = queryJSON.status;
if (queryJSON.policyID) {
filtersForm[FILTER_KEYS.POLICY_ID] = queryJSON.policyID;
}

return filtersForm;
}
Expand Down Expand Up @@ -781,8 +789,18 @@ function getSearchHeaderTitle(
return title;
}

function buildCannedSearchQuery(type: SearchDataTypes = CONST.SEARCH.DATA_TYPES.EXPENSE, status: SearchStatus = CONST.SEARCH.STATUS.EXPENSE.ALL): SearchQueryString {
return normalizeQuery(`type:${type} status:${status}`);
function buildCannedSearchQuery({
type = CONST.SEARCH.DATA_TYPES.EXPENSE,
status = CONST.SEARCH.STATUS.EXPENSE.ALL,
policyID,
}: {
type?: SearchDataTypes;
status?: SearchStatus;
policyID?: string;
} = {}): SearchQueryString {
const queryString = policyID ? `type:${type} status:${status} policyID:${policyID}` : `type:${type} status:${status}`;

return normalizeQuery(queryString);
}

function getOverflowMenu(itemName: string, hash: number, inputQuery: string, showDeleteModal: (hash: number) => void, isMobileMenu?: boolean, closeMenu?: () => void) {
Expand Down
13 changes: 7 additions & 6 deletions src/pages/Search/AdvancedSearchFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import {usePersonalDetails} from '@components/OnyxProvider';
import ScrollView from '@components/ScrollView';
import type {AdvancedFiltersKeys, SearchQueryJSON} from '@components/Search/types';
import type {AdvancedFiltersKeys} from '@components/Search/types';
import useLocalize from '@hooks/useLocalize';
import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -233,8 +233,8 @@ function AdvancedSearchFilters() {
const personalDetails = usePersonalDetails();
const currentType = searchAdvancedFilters?.type ?? CONST.SEARCH.DATA_TYPES.EXPENSE;

const queryString = useMemo(() => SearchUtils.buildQueryStringFromFilterFormValues(searchAdvancedFilters) || '', [searchAdvancedFilters]);
const queryJSON = useMemo(() => SearchUtils.buildSearchQueryJSON(queryString || SearchUtils.buildCannedSearchQuery()) ?? ({} as SearchQueryJSON), [queryString]);
const queryString = useMemo(() => SearchUtils.buildQueryStringFromFilterFormValues(searchAdvancedFilters), [searchAdvancedFilters]);
const queryJSON = useMemo(() => SearchUtils.buildSearchQueryJSON(queryString || SearchUtils.buildCannedSearchQuery()), [queryString]);

const applyFiltersAndNavigate = () => {
SearchActions.clearAllFilters();
Expand All @@ -248,7 +248,7 @@ function AdvancedSearchFilters() {

const onSaveSearch = () => {
const savedSearchKeys = Object.keys(savedSearches ?? {});
if (savedSearches && savedSearchKeys.includes(String(queryJSON.hash))) {
if (!queryJSON || (savedSearches && savedSearchKeys.includes(String(queryJSON.hash)))) {
// If the search is already saved, return early to prevent unnecessary API calls
Navigation.dismissModal();
return;
Expand Down Expand Up @@ -302,6 +302,8 @@ function AdvancedSearchFilters() {
};
});

const displaySearchButton = queryJSON && !SearchUtils.isCannedSearchQuery(queryJSON);

return (
<>
<ScrollView contentContainerStyle={[styles.flexGrow1, styles.justifyContentBetween]}>
Expand All @@ -323,8 +325,7 @@ function AdvancedSearchFilters() {
})}
</View>
</ScrollView>

{!SearchUtils.isCannedSearchQuery(queryJSON) && (
{displaySearchButton && (
<Button
text={translate('search.saveSearch')}
onPress={onSaveSearch}
Expand Down
1 change: 1 addition & 0 deletions src/pages/Search/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function SearchPage({route}: SearchPageProps) {
<SearchStatusBar
type={queryJSON.type}
status={queryJSON.status}
policyID={queryJSON.policyID}
/>
<Search queryJSON={queryJSON} />
</>
Expand Down
1 change: 1 addition & 0 deletions src/pages/Search/SearchPageBottomTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ function SearchPageBottomTab() {
<SearchStatusBar
type={queryJSON.type}
status={queryJSON.status}
policyID={queryJSON.policyID}
onStatusChange={() => {
topBarOffset.value = withTiming(variables.searchHeaderHeight, {duration: ANIMATION_DURATION_IN_MS});
}}
Expand Down
24 changes: 18 additions & 6 deletions src/pages/Search/SearchTypeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type SearchTypeMenuItem = {
title: string;
type: SearchDataTypes;
icon: IconAsset;
route?: Route;
getRoute: (policyID?: string) => Route;
};

function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) {
Expand All @@ -72,25 +72,37 @@ function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) {
title: translate('common.expenses'),
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
icon: Expensicons.Receipt,
route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery()}),
getRoute: (policyID?: string) => {
const query = SearchUtils.buildCannedSearchQuery({policyID});
return ROUTES.SEARCH_CENTRAL_PANE.getRoute({query});
},
},
{
title: translate('common.chats'),
type: CONST.SEARCH.DATA_TYPES.CHAT,
icon: Expensicons.ChatBubbles,
route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.TRIP.ALL)}),
getRoute: (policyID?: string) => {
const query = SearchUtils.buildCannedSearchQuery({type: CONST.SEARCH.DATA_TYPES.CHAT, status: CONST.SEARCH.STATUS.CHAT.ALL, policyID});
return ROUTES.SEARCH_CENTRAL_PANE.getRoute({query});
},
},
{
title: translate('workspace.common.invoices'),
type: CONST.SEARCH.DATA_TYPES.INVOICE,
icon: Expensicons.InvoiceGeneric,
route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.INVOICE, CONST.SEARCH.STATUS.INVOICE.ALL)}),
getRoute: (policyID?: string) => {
const query = SearchUtils.buildCannedSearchQuery({type: CONST.SEARCH.DATA_TYPES.INVOICE, status: CONST.SEARCH.STATUS.INVOICE.ALL, policyID});
return ROUTES.SEARCH_CENTRAL_PANE.getRoute({query});
},
},
{
title: translate('travel.trips'),
type: CONST.SEARCH.DATA_TYPES.TRIP,
icon: Expensicons.Suitcase,
route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.STATUS.TRIP.ALL)}),
getRoute: (policyID?: string) => {
const query = SearchUtils.buildCannedSearchQuery({type: CONST.SEARCH.DATA_TYPES.TRIP, status: CONST.SEARCH.STATUS.TRIP.ALL, policyID});
return ROUTES.SEARCH_CENTRAL_PANE.getRoute({query});
},
},
];

Expand Down Expand Up @@ -227,7 +239,7 @@ function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) {
{typeMenuItems.map((item, index) => {
const onPress = singleExecution(() => {
SearchActions.clearAllFilters();
Navigation.navigate(item.route);
Navigation.navigate(item.getRoute(queryJSON.policyID));
});

return (
Expand Down
Loading

0 comments on commit 88beaa6

Please sign in to comment.