Skip to content

Commit

Permalink
Merge pull request #48566 from Expensify/lucien/search-savedSearches
Browse files Browse the repository at this point in the history
[Search v2.3] [App] Implement Saved Search feature
  • Loading branch information
luacmartins committed Sep 13, 2024
2 parents 89c0ca5 + b076f54 commit e257796
Show file tree
Hide file tree
Showing 32 changed files with 625 additions and 97 deletions.
1 change: 1 addition & 0 deletions assets/images/bookmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ const ONYXKEYS = {
/** The NVP containing all information related to educational tooltip in workspace chat */
NVP_WORKSPACE_TOOLTIP: 'workspaceTooltip',

/** Whether to hide save search rename tooltip */
NVP_SHOULD_HIDE_SAVED_SEARCH_RENAME_TOOLTIP: 'nvp_should_hide_saved_search_rename_tooltip',

/** Whether to hide gbr tooltip */
NVP_SHOULD_HIDE_GBR_TOOLTIP: 'nvp_should_hide_gbr_tooltip',

Expand Down Expand Up @@ -424,6 +427,9 @@ const ONYXKEYS = {
/** Stores the route to open after changing app permission from settings */
LAST_ROUTE: 'lastRoute',

/** Stores the information about the saved searches */
SAVED_SEARCHES: 'nvp_savedSearches',

/** Stores recently used currencies */
RECENTLY_USED_CURRENCIES: 'nvp_recentlyUsedCurrencies',

Expand Down Expand Up @@ -668,6 +674,8 @@ const ONYXKEYS = {
SAGE_INTACCT_DIMENSION_TYPE_FORM_DRAFT: 'sageIntacctDimensionTypeFormDraft',
SEARCH_ADVANCED_FILTERS_FORM: 'searchAdvancedFiltersForm',
SEARCH_ADVANCED_FILTERS_FORM_DRAFT: 'searchAdvancedFiltersFormDraft',
SEARCH_SAVED_SEARCH_RENAME_FORM: 'searchSavedSearchRenameForm',
SEARCH_SAVED_SEARCH_RENAME_FORM_DRAFT: 'searchSavedSearchRenameFormDraft',
TEXT_PICKER_MODAL_FORM: 'textPickerModalForm',
TEXT_PICKER_MODAL_FORM_DRAFT: 'textPickerModalFormDraft',
RULES_CUSTOM_NAME_MODAL_FORM: 'rulesCustomNameModalForm',
Expand Down Expand Up @@ -777,6 +785,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.RULES_REQUIRED_RECEIPT_AMOUNT_FORM]: FormTypes.RulesRequiredReceiptAmountForm;
[ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AMOUNT_FORM]: FormTypes.RulesMaxExpenseAmountForm;
[ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AGE_FORM]: FormTypes.RulesMaxExpenseAgeForm;
[ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm;
};

type OnyxFormDraftValuesMapping = {
Expand Down Expand Up @@ -842,6 +851,7 @@ type OnyxValuesMapping = {

// ONYXKEYS.NVP_TRYNEWDOT is HybridApp onboarding data
[ONYXKEYS.NVP_TRYNEWDOT]: OnyxTypes.TryNewDot;
[ONYXKEYS.SAVED_SEARCHES]: OnyxTypes.SaveSearch[];
[ONYXKEYS.RECENTLY_USED_CURRENCIES]: string[];
[ONYXKEYS.ACTIVE_CLIENTS]: string[];
[ONYXKEYS.DEVICE_ID]: string;
Expand Down Expand Up @@ -976,6 +986,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx;
[ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet;
[ONYXKEYS.LAST_ROUTE]: string;
[ONYXKEYS.NVP_SHOULD_HIDE_SAVED_SEARCH_RENAME_TOOLTIP]: boolean;
};

type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ const ROUTES = {
route: 'search',
getRoute: ({query}: {query: SearchQueryString}) => `search?q=${encodeURIComponent(query)}` as const,
},
SEARCH_SAVED_SEARCH_RENAME: {
route: 'search/saved-search/rename',
getRoute: ({name, jsonQuery}: {name: string; jsonQuery: SearchQueryString}) => `search/saved-search/rename?name=${name}&q=${jsonQuery}` as const,
},
SEARCH_ADVANCED_FILTERS: 'search/filters',
SEARCH_ADVANCED_FILTERS_DATE: 'search/filters/date',
SEARCH_ADVANCED_FILTERS_CURRENCY: 'search/filters/currency',
Expand Down
2 changes: 2 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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',
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',
BOTTOM_TAB: 'Search_Bottom_Tab',
Expand Down Expand Up @@ -172,6 +173,7 @@ const SCREENS = {
TRAVEL: 'Travel',
SEARCH_REPORT: 'SearchReport',
SEARCH_ADVANCED_FILTERS: 'SearchAdvancedFilters',
SEARCH_SAVED_SEARCH: 'SearchSavedSearch',
SETTINGS_CATEGORIES: 'SettingsCategories',
RESTRICTED_ACTION: 'RestrictedAction',
REPORT_EXPORT: 'Report_Export',
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import Bell from '@assets/images/bell.svg';
import BellSlash from '@assets/images/bellSlash.svg';
import Bill from '@assets/images/bill.svg';
import Bolt from '@assets/images/bolt.svg';
import Bookmark from '@assets/images/bookmark.svg';
import Box from '@assets/images/box.svg';
import Briefcase from '@assets/images/briefcase.svg';
import Bug from '@assets/images/bug.svg';
Expand Down Expand Up @@ -399,5 +400,6 @@ export {
Feed,
Table,
SpreadsheetComputer,
Bookmark,
Star,
};
33 changes: 31 additions & 2 deletions src/components/MenuItemList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, {useRef} from 'react';
import type {GestureResponderEvent, View} from 'react-native';
import type {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native';
import useSingleExecution from '@hooks/useSingleExecution';
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import CONST from '@src/CONST';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type IconAsset from '@src/types/utils/IconAsset';
import type {MenuItemProps} from './MenuItem';
import MenuItem from './MenuItem';
import OfflineWithFeedback from './OfflineWithFeedback';
Expand Down Expand Up @@ -36,9 +37,32 @@ type MenuItemListProps = {

/** Whether or not to use the single execution hook */
shouldUseSingleExecution?: boolean;

/** Any additional styles to apply for each item */
wrapperStyle?: StyleProp<ViewStyle>;

/** Icon to display on the left side of each item */
icon?: IconAsset;

/** Icon Width */
iconWidth?: number;

/** Icon Height */
iconHeight?: number;

/** Is this in the Pane */
isPaneMenu?: boolean;
};

function MenuItemList({menuItems = [], shouldUseSingleExecution = false}: MenuItemListProps) {
function MenuItemList({
menuItems = [],
shouldUseSingleExecution = false,
wrapperStyle = {},
icon = undefined,
iconWidth = undefined,
iconHeight = undefined,
isPaneMenu = false,
}: MenuItemListProps) {
const popoverAnchor = useRef<View>(null);
const {isExecuting, singleExecution} = useSingleExecution();

Expand Down Expand Up @@ -67,9 +91,14 @@ function MenuItemList({menuItems = [], shouldUseSingleExecution = false}: MenuIt
>
<MenuItem
key={menuItemProps.key ?? menuItemProps.title}
wrapperStyle={wrapperStyle}
onSecondaryInteraction={menuItemProps.link !== undefined ? (e) => secondaryInteraction(menuItemProps.link, e) : undefined}
ref={popoverAnchor}
shouldBlockSelection={!!menuItemProps.link}
icon={icon}
iconWidth={iconWidth}
iconHeight={iconHeight}
isPaneMenu={isPaneMenu}
// eslint-disable-next-line react/jsx-props-no-spreading
{...menuItemProps}
disabled={!!menuItemProps.disabled || isExecuting}
Expand Down
23 changes: 17 additions & 6 deletions src/components/PopoverMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import lodashIsEqual from 'lodash/isEqual';
import type {RefObject} from 'react';
import React, {useLayoutEffect, useState} from 'react';
import {StyleSheet, View} from 'react-native';
import {StyleSheet} from 'react-native';
import type {View} from 'react-native';
import type {ModalProps} from 'react-native-modal';
import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
Expand All @@ -20,6 +21,7 @@ import type {MenuItemProps} from './MenuItem';
import MenuItem from './MenuItem';
import type BaseModalProps from './Modal/types';
import PopoverWithMeasuredContent from './PopoverWithMeasuredContent';
import ScrollView from './ScrollView';
import Text from './Text';

type PopoverMenuItem = MenuItemProps & {
Expand All @@ -42,6 +44,9 @@ type PopoverMenuItem = MenuItemProps & {
* It is meant to be used in situations where, after clicking on the modal, another one is opened.
*/
shouldCallAfterModalHide?: boolean;

/** Whether to close all modals */
shouldCloseAllModals?: boolean;
};

type PopoverModalProps = Pick<ModalProps, 'animationIn' | 'animationOut' | 'animationInTiming'>;
Expand Down Expand Up @@ -142,9 +147,13 @@ function PopoverMenu({
setFocusedIndex(selectedSubMenuItemIndex);
} else if (selectedItem.shouldCallAfterModalHide && !Browser.isSafari()) {
onItemSelected(selectedItem, index);
Modal.close(() => {
selectedItem.onSelected?.();
});
Modal.close(
() => {
selectedItem.onSelected?.();
},
undefined,
selectedItem.shouldCloseAllModals,
);
} else {
onItemSelected(selectedItem, index);
selectedItem.onSelected?.();
Expand Down Expand Up @@ -247,7 +256,7 @@ function PopoverMenu({
restoreFocusType={restoreFocusType}
>
<FocusTrapForModal active={isVisible}>
<View style={isSmallScreenWidth ? {} : styles.createMenuContainer}>
<ScrollView style={isSmallScreenWidth ? {} : styles.createMenuContainer}>
{renderHeaderText()}
{enteredSubMenuIndexes.length > 0 && renderBackButtonItem()}
{currentMenuItems.map((item, menuIndex) => (
Expand All @@ -269,7 +278,9 @@ function PopoverMenu({
focused={focusedIndex === menuIndex}
displayInDefaultIconColor={item.displayInDefaultIconColor}
shouldShowRightIcon={item.shouldShowRightIcon}
shouldShowRightComponent={item.shouldShowRightComponent}
iconRight={item.iconRight}
rightComponent={item.rightComponent}
shouldPutLeftPaddingWhenNoIcon={item.shouldPutLeftPaddingWhenNoIcon}
label={item.label}
style={{backgroundColor: item.isSelected ? theme.activeComponentBG : undefined}}
Expand All @@ -293,7 +304,7 @@ function PopoverMenu({
badgeText={item.badgeText}
/>
))}
</View>
</ScrollView>
</FocusTrapForModal>
</PopoverWithMeasuredContent>
);
Expand Down
47 changes: 47 additions & 0 deletions src/hooks/useDeleteSavedSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, {useState} from 'react';
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';

export default function useDeleteSavedSearch() {
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const [hashToDelete, setHashToDelete] = useState(0);
const {translate} = useLocalize();

const showDeleteModal = (hash: number) => {
setIsDeleteModalVisible(true);
setHashToDelete(hash);
};

const handleDelete = () => {
SearchActions.deleteSavedSearch(hashToDelete);
setIsDeleteModalVisible(false);
SearchActions.clearAdvancedFilters();
Navigation.navigate(
ROUTES.SEARCH_CENTRAL_PANE.getRoute({
query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.ALL),
}),
);
};

function DeleteConfirmModal() {
return (
<ConfirmModal
title={translate('search.deleteSavedSearch')}
onConfirm={handleDelete}
onCancel={() => setIsDeleteModalVisible(false)}
isVisible={isDeleteModalVisible}
prompt={translate('search.deleteSavedSearchConfirm')}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
danger
/>
);
}

return {showDeleteModal, DeleteConfirmModal};
}
7 changes: 7 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ export default {
sent: 'Sent',
links: 'Links',
days: 'days',
rename: 'Rename',
},
location: {
useCurrent: 'Use current location',
Expand Down Expand Up @@ -4022,6 +4023,12 @@ export default {
buttonText: 'Book a trip',
},
},
saveSearch: 'Save search',
saveSearchTooltipText: 'You can rename your saved search',
deleteSavedSearch: 'Delete saved search',
deleteSavedSearchConfirm: 'Are you sure you want to delete this search?',
searchName: 'Search name',
savedSearchesMenuItemTitle: 'Saved',
groupedExpenses: 'grouped expenses',
bulkActions: {
delete: 'Delete',
Expand Down
7 changes: 7 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ export default {
comma: 'la coma',
semicolon: 'el punto y coma',
please: 'Por favor',
rename: 'Renombrar',
contactUs: 'contáctenos',
pleaseEnterEmailOrPhoneNumber: 'Por favor, escribe un email o número de teléfono',
fixTheErrors: 'corrige los errores',
Expand Down Expand Up @@ -4073,6 +4074,12 @@ export default {
buttonText: 'Reserva un viaje',
},
},
saveSearch: 'Guardar búsqueda',
saveSearchTooltipText: 'Puedes cambiar el nombre de tu búsqueda guardada',
savedSearchesMenuItemTitle: 'Guardadas',
searchName: 'Nombre de la búsqueda',
deleteSavedSearch: 'Eliminar búsqueda guardada',
deleteSavedSearchConfirm: '¿Estás seguro de que quieres eliminar esta búsqueda?',
groupedExpenses: 'gastos agrupados',
bulkActions: {
delete: 'Eliminar',
Expand Down
5 changes: 5 additions & 0 deletions src/libs/API/parameters/DeleteSavedSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type DeleteSavedSearchParams = {
hash: number;
};

export default DeleteSavedSearchParams;
8 changes: 8 additions & 0 deletions src/libs/API/parameters/SaveSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type {SearchQueryString} from '@components/Search/types';

type SaveSearchParams = {
jsonQuery: SearchQueryString;
name?: string;
};

export default SaveSearchParams;
2 changes: 2 additions & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ export type {default as EnablePolicyCompanyCardsParams} from './EnablePolicyComp
export type {default as ToggleCardContinuousReconciliationParams} from './ToggleCardContinuousReconciliationParams';
export type {default as CardDeactivateParams} from './CardDeactivateParams';
export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams';
export type {default as SaveSearchParams} from './SaveSearch';
export type {default as DeleteSavedSearchParams} from './DeleteSavedSearch';
export type {default as SetPolicyCategoryReceiptsRequiredParams} from './SetPolicyCategoryReceiptsRequiredParams';
export type {default as RemovePolicyCategoryReceiptsRequiredParams} from './RemovePolicyCategoryReceiptsRequiredParams';
export type {default as UpdateQuickbooksOnlineAutoCreateVendorParams} from './UpdateQuickbooksOnlineAutoCreateVendorParams';
Expand Down
4 changes: 4 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ const WRITE_COMMANDS = {
CREATE_ADMIN_ISSUED_VIRTUAL_CARD: 'CreateAdminIssuedVirtualCard',
ADD_DELEGATE: 'AddDelegate',
TOGGLE_CARD_CONTINUOUS_RECONCILIATION: 'ToggleCardContinuousReconciliation',
SAVE_SEARCH: 'SaveSearch',
DELETE_SAVED_SEARCH: 'DeleteSavedSearch',
UPDATE_CARD_SETTLEMENT_FREQUENCY: 'UpdateCardSettlementFrequency',
UPDATE_CARD_SETTLEMENT_ACCOUNT: 'UpdateCardSettlementAccount',
UPDATE_XERO_IMPORT_TRACKING_CATEGORIES: 'UpdateXeroImportTrackingCategories',
Expand Down Expand Up @@ -775,6 +777,8 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.CREATE_ADMIN_ISSUED_VIRTUAL_CARD]: Omit<Parameters.CreateExpensifyCardParams, 'feedCountry'>;
[WRITE_COMMANDS.ADD_DELEGATE]: Parameters.AddDelegateParams;
[WRITE_COMMANDS.TOGGLE_CARD_CONTINUOUS_RECONCILIATION]: Parameters.ToggleCardContinuousReconciliationParams;
[WRITE_COMMANDS.SAVE_SEARCH]: Parameters.SaveSearchParams;
[WRITE_COMMANDS.DELETE_SAVED_SEARCH]: Parameters.DeleteSavedSearchParams;
[WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_FREQUENCY]: Parameters.UpdateCardSettlementFrequencyParams;
[WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_ACCOUNT]: Parameters.UpdateCardSettlementAccountParams;
[WRITE_COMMANDS.SET_MISSING_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARD]: Parameters.SetMissingPersonalDetailsAndShipExpensifyCardParams;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
RoomMembersNavigatorParamList,
SearchAdvancedFiltersParamList,
SearchReportParamList,
SearchSavedSearchParamList,
SettingsNavigatorParamList,
SignInNavigatorParamList,
SplitDetailsNavigatorParamList,
Expand Down Expand Up @@ -557,6 +558,10 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator<Searc
[SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP]: () => require<ReactComponentModule>('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersInPage').default,
});

const SearchSavedSearchModalStackNavigator = createModalStackNavigator<SearchSavedSearchParamList>({
[SCREENS.SEARCH.SAVED_SEARCH_RENAME_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SavedSearchRenamePage').default,
});

const RestrictedActionModalStackNavigator = createModalStackNavigator<SearchReportParamList>({
[SCREENS.RESTRICTED_ACTION_ROOT]: () => require<ReactComponentModule>('../../../../pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage').default,
});
Expand Down Expand Up @@ -595,5 +600,6 @@ export {
SearchReportModalStackNavigator,
RestrictedActionModalStackNavigator,
SearchAdvancedFiltersModalStackNavigator,
SearchSavedSearchModalStackNavigator,
MissingPersonalDetailsModalStackNavigator,
};
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
name={SCREENS.RIGHT_MODAL.SEARCH_ADVANCED_FILTERS}
component={ModalStackNavigators.SearchAdvancedFiltersModalStackNavigator}
/>
<Stack.Screen
name={SCREENS.RIGHT_MODAL.SEARCH_SAVED_SEARCH}
component={ModalStackNavigators.SearchSavedSearchModalStackNavigator}
/>
<Stack.Screen
name={SCREENS.RIGHT_MODAL.MISSING_PERSONAL_DETAILS}
component={ModalStackNavigators.MissingPersonalDetailsModalStackNavigator}
Expand Down
Loading

0 comments on commit e257796

Please sign in to comment.