diff --git a/assets/images/bookmark.svg b/assets/images/bookmark.svg new file mode 100644 index 000000000000..d7c1a8397b37 --- /dev/null +++ b/assets/images/bookmark.svg @@ -0,0 +1 @@ + diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ed4f6a2dc563..38affd97c637 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -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', @@ -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', @@ -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', @@ -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 = { @@ -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; @@ -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; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 8333fb3c14ce..27504998c49c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -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', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b87c7703b376..8168afba89ab 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -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', @@ -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', diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 698dc33b4a03..6034a6026601 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -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'; @@ -397,5 +398,6 @@ export { Feed, Table, SpreadsheetComputer, + Bookmark, Star, }; diff --git a/src/components/MenuItemList.tsx b/src/components/MenuItemList.tsx index 623198498dd1..2e732c691140 100644 --- a/src/components/MenuItemList.tsx +++ b/src/components/MenuItemList.tsx @@ -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'; @@ -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; + + /** 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(null); const {isExecuting, singleExecution} = useSingleExecution(); @@ -67,9 +91,14 @@ function MenuItemList({menuItems = [], shouldUseSingleExecution = false}: MenuIt > 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} diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 9cb6febc65d1..f20d77d1f6c4 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -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'; @@ -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 & { @@ -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; @@ -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?.(); @@ -247,7 +256,7 @@ function PopoverMenu({ restoreFocusType={restoreFocusType} > - + {renderHeaderText()} {enteredSubMenuIndexes.length > 0 && renderBackButtonItem()} {currentMenuItems.map((item, menuIndex) => ( @@ -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}} @@ -293,7 +304,7 @@ function PopoverMenu({ badgeText={item.badgeText} /> ))} - + ); diff --git a/src/hooks/useDeleteSavedSearch.tsx b/src/hooks/useDeleteSavedSearch.tsx new file mode 100644 index 000000000000..ddd2b83f5bc9 --- /dev/null +++ b/src/hooks/useDeleteSavedSearch.tsx @@ -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 ( + setIsDeleteModalVisible(false)} + isVisible={isDeleteModalVisible} + prompt={translate('search.deleteSavedSearchConfirm')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + ); + } + + return {showDeleteModal, DeleteConfirmModal}; +} diff --git a/src/languages/en.ts b/src/languages/en.ts index 1bad9c2f169e..9dec1f9182f0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -396,6 +396,7 @@ export default { sent: 'Sent', links: 'Links', days: 'days', + rename: 'Rename', }, location: { useCurrent: 'Use current location', @@ -4005,6 +4006,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', diff --git a/src/languages/es.ts b/src/languages/es.ts index d27c7f952d6f..6cedfa79ade4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -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', @@ -4056,6 +4057,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', diff --git a/src/libs/API/parameters/DeleteSavedSearch.ts b/src/libs/API/parameters/DeleteSavedSearch.ts new file mode 100644 index 000000000000..23b20204bcd2 --- /dev/null +++ b/src/libs/API/parameters/DeleteSavedSearch.ts @@ -0,0 +1,5 @@ +type DeleteSavedSearchParams = { + hash: number; +}; + +export default DeleteSavedSearchParams; diff --git a/src/libs/API/parameters/SaveSearch.ts b/src/libs/API/parameters/SaveSearch.ts new file mode 100644 index 000000000000..e0ad38dd8363 --- /dev/null +++ b/src/libs/API/parameters/SaveSearch.ts @@ -0,0 +1,8 @@ +import type {SearchQueryString} from '@components/Search/types'; + +type SaveSearchParams = { + jsonQuery: SearchQueryString; + name?: string; +}; + +export default SaveSearchParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index e70ac63a1121..c780003c3e3f 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -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'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index bb48136485a0..81b748ce6edb 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -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', @@ -775,6 +777,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.CREATE_ADMIN_ISSUED_VIRTUAL_CARD]: Omit; [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; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 827f24182728..b41b58530a6b 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -22,6 +22,7 @@ import type { RoomMembersNavigatorParamList, SearchAdvancedFiltersParamList, SearchReportParamList, + SearchSavedSearchParamList, SettingsNavigatorParamList, SignInNavigatorParamList, SplitDetailsNavigatorParamList, @@ -557,6 +558,10 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersInPage').default, }); +const SearchSavedSearchModalStackNavigator = createModalStackNavigator({ + [SCREENS.SEARCH.SAVED_SEARCH_RENAME_RHP]: () => require('../../../../pages/Search/SavedSearchRenamePage').default, +}); + const RestrictedActionModalStackNavigator = createModalStackNavigator({ [SCREENS.RESTRICTED_ACTION_ROOT]: () => require('../../../../pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage').default, }); @@ -595,5 +600,6 @@ export { SearchReportModalStackNavigator, RestrictedActionModalStackNavigator, SearchAdvancedFiltersModalStackNavigator, + SearchSavedSearchModalStackNavigator, MissingPersonalDetailsModalStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 70bbd9f0182d..481df0b1c9ad 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -183,6 +183,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.SEARCH_ADVANCED_FILTERS} component={ModalStackNavigators.SearchAdvancedFiltersModalStackNavigator} /> + {displaySignIn && } + {isCustomSearchQuery && ( + { + Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery()})); + }} + > + {translate('common.cancel')} + + )} {displaySearch && ( > = SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP, + SCREENS.SEARCH.SAVED_SEARCH_RENAME_RHP, ], [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [ SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index c20fa0fb9369..2ca2db10a1a7 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1145,6 +1145,11 @@ const config: LinkingOptions['config'] = { [SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_IN, }, }, + [SCREENS.RIGHT_MODAL.SEARCH_SAVED_SEARCH]: { + screens: { + [SCREENS.SEARCH.SAVED_SEARCH_RENAME_RHP]: ROUTES.SEARCH_SAVED_SEARCH_RENAME.route, + }, + }, [SCREENS.RIGHT_MODAL.RESTRICTED_ACTION]: { screens: { [SCREENS.RESTRICTED_ACTION_ROOT]: ROUTES.RESTRICTED_ACTION.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 98c4f357480b..c6a4ce90c214 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -13,6 +13,7 @@ import type { import type {TupleToUnion, ValueOf} from 'type-fest'; import type {SearchQueryString} from '@components/Search/types'; import type {IOURequestType} from '@libs/actions/IOU'; +import type {SaveSearchParams} from '@libs/API/parameters'; import type CONST from '@src/CONST'; import type {Country, IOUAction, IOUType} from '@src/CONST'; import type NAVIGATORS from '@src/NAVIGATORS'; @@ -1181,6 +1182,7 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.RESTRICTED_ACTION]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SEARCH_ADVANCED_FILTERS]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.SEARCH_SAVED_SEARCH]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.MISSING_PERSONAL_DETAILS]: NavigatorScreenParams; }; @@ -1405,6 +1407,10 @@ type SearchAdvancedFiltersParamList = { [SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: Record; }; +type SearchSavedSearchParamList = { + [SCREENS.SEARCH.SAVED_SEARCH_RENAME_RHP]: SaveSearchParams; +}; + type RestrictedActionParamList = { [SCREENS.RESTRICTED_ACTION_ROOT]: { policyID: string; @@ -1487,6 +1493,7 @@ export type { TransactionDuplicateNavigatorParamList, SearchReportParamList, SearchAdvancedFiltersParamList, + SearchSavedSearchParamList, RestrictedActionParamList, MissingPersonalDetailsParamList, }; diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index feb7bd4d5857..24872b5d90eb 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -5,9 +5,11 @@ import ChatListItem from '@components/SelectionList/ChatListItem'; import ReportListItem from '@components/SelectionList/Search/ReportListItem'; import TransactionListItem from '@components/SelectionList/Search/TransactionListItem'; import type {ListItem, ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; +import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm'; @@ -17,6 +19,7 @@ import type {ListItemDataType, ListItemType, SearchDataTypes, SearchPersonalDeta import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import {translateLocal} from './Localize'; +import Navigation from './Navigation/Navigation'; import navigationRef from './Navigation/navigationRef'; import type {AuthScreensParamList, RootStackParamList, State} from './Navigation/types'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; @@ -761,6 +764,33 @@ function buildCannedSearchQuery(type: SearchDataTypes = CONST.SEARCH.DATA_TYPES. return normalizeQuery(`type:${type} status:${status}`); } +function getOverflowMenu(itemName: string, hash: number, inputQuery: string, showDeleteModal: (hash: number) => void, isMobileMenu?: boolean, closeMenu?: () => void) { + return [ + { + text: translateLocal('common.rename'), + onSelected: () => { + if (isMobileMenu && closeMenu) { + closeMenu(); + } + Navigation.navigate(ROUTES.SEARCH_SAVED_SEARCH_RENAME.getRoute({name: itemName, jsonQuery: inputQuery})); + }, + icon: Expensicons.Pencil, + shouldShowRightIcon: false, + shouldShowRightComponent: false, + shouldCallAfterModalHide: true, + }, + { + text: translateLocal('common.delete'), + onSelected: () => showDeleteModal(hash), + icon: Expensicons.Trashcan, + shouldShowRightIcon: false, + shouldShowRightComponent: false, + shouldCallAfterModalHide: true, + shouldCloseAllModals: true, + }, + ]; +} + /** * Returns whether a given search query is a Canned query. * @@ -792,4 +822,5 @@ export { buildCannedSearchQuery, isCannedSearchQuery, getExpenseTypeTranslationKey, + getOverflowMenu, }; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index e522aea788e4..a4f0e59ef976 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -51,6 +51,17 @@ function getOnyxLoadingData(hash: number): {optimisticData: OnyxUpdate[]; finall return {optimisticData, finallyData}; } +function saveSearch({queryJSON, name}: {queryJSON: SearchQueryJSON; name?: string}) { + const saveSearchName = name ?? queryJSON?.inputQuery ?? ''; + const jsonQuery = JSON.stringify(queryJSON); + + API.write(WRITE_COMMANDS.SAVE_SEARCH, {jsonQuery, name: saveSearchName}); +} + +function deleteSavedSearch(hash: number) { + API.write(WRITE_COMMANDS.DELETE_SAVED_SEARCH, {hash}); +} + function search({queryJSON, offset}: {queryJSON: SearchQueryJSON; offset?: number}) { const {optimisticData, finallyData} = getOnyxLoadingData(queryJSON.hash); const {flatFilters, ...queryJSONWithoutFlatFilters} = queryJSON; @@ -145,7 +156,12 @@ function clearAdvancedFilters() { Onyx.merge(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, values); } +function dismissSavedSearchRenameTooltip() { + Onyx.merge(ONYXKEYS.NVP_SHOULD_HIDE_SAVED_SEARCH_RENAME_TOOLTIP, true); +} + export { + saveSearch, search, createTransactionThread, deleteMoneyRequestOnSearch, @@ -155,4 +171,6 @@ export { updateAdvancedFilters, clearAllFilters, clearAdvancedFilters, + deleteSavedSearch, + dismissSavedSearchRenameTooltip, }; diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index ac31c4e7f3b9..fa59495d2c8f 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -1,14 +1,15 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {ValueOf} from 'react-native-gesture-handler/lib/typescript/typeUtils'; import type {OnyxCollection} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import {usePersonalDetails} from '@components/OnyxProvider'; import ScrollView from '@components/ScrollView'; -import type {AdvancedFiltersKeys} from '@components/Search/types'; +import type {AdvancedFiltersKeys, SearchQueryJSON} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -220,24 +221,33 @@ function AdvancedSearchFilters() { const {singleExecution} = useSingleExecution(); const waitForNavigate = useWaitForNavigation(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); - const [searchAdvancedFilters = {} as SearchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST); const taxRates = getAllTaxRates(); const personalDetails = usePersonalDetails(); const currentType = searchAdvancedFilters?.type ?? CONST.SEARCH.DATA_TYPES.EXPENSE; - const onFormSubmit = () => { - const query = SearchUtils.buildQueryStringFromFilterValues(searchAdvancedFilters); + const queryString = useMemo(() => SearchUtils.buildQueryStringFromFilterValues(searchAdvancedFilters) || '', [searchAdvancedFilters]); + const queryJSON = useMemo(() => SearchUtils.buildSearchQueryJSON(queryString || SearchUtils.buildCannedSearchQuery()) ?? ({} as SearchQueryJSON), [queryString]); + + const applyFiltersAndNavigate = () => { SearchActions.clearAllFilters(); Navigation.dismissModal(); Navigation.navigate( ROUTES.SEARCH_CENTRAL_PANE.getRoute({ - query, + query: queryString, }), ); }; + const onSaveSearch = () => { + SearchActions.saveSearch({ + queryJSON, + }); + + applyFiltersAndNavigate(); + }; + const filters = typeFiltersKeys[currentType].map((key) => { const onPress = singleExecution(waitForNavigate(() => Navigation.navigate(baseFilterConfig[key].route))); let filterTitle; @@ -296,10 +306,19 @@ function AdvancedSearchFilters() { })} + + {!SearchUtils.isCannedSearchQuery(queryJSON) && ( +