From bdba824643b7542c00253f0d7e0b2f5d854da9ef Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 20 Sep 2024 15:41:22 +0700 Subject: [PATCH 1/7] fix: Search Save add Onyx optimistic and failure data --- src/ONYXKEYS.ts | 2 +- src/libs/actions/Search.ts | 71 ++++++++++++++++++++++++++++- src/pages/Search/SearchTypeMenu.tsx | 3 +- src/types/onyx/SaveSearch.ts | 6 ++- 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 68a9ca2f8502..e425bc69bcb8 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -860,7 +860,7 @@ type OnyxValuesMapping = { // ONYXKEYS.NVP_TRYNEWDOT is HybridApp onboarding data [ONYXKEYS.NVP_TRYNEWDOT]: OnyxTypes.TryNewDot; - [ONYXKEYS.SAVED_SEARCHES]: OnyxTypes.SaveSearch[]; + [ONYXKEYS.SAVED_SEARCHES]: OnyxTypes.SaveSearch; [ONYXKEYS.RECENTLY_USED_CURRENCIES]: string[]; [ONYXKEYS.ACTIVE_CLIENTS]: string[]; [ONYXKEYS.DEVICE_ID]: string; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index a4f0e59ef976..952763c1d313 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -55,11 +55,78 @@ function saveSearch({queryJSON, name}: {queryJSON: SearchQueryJSON; name?: strin const saveSearchName = name ?? queryJSON?.inputQuery ?? ''; const jsonQuery = JSON.stringify(queryJSON); - API.write(WRITE_COMMANDS.SAVE_SEARCH, {jsonQuery, name: saveSearchName}); + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.SAVED_SEARCHES}`, + value: { + [queryJSON.hash]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + name: saveSearchName, + query: queryJSON.inputQuery, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.SAVED_SEARCHES}`, + value: { + [queryJSON.hash]: null, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.SAVED_SEARCHES}`, + value: { + [queryJSON.hash]: { + pendingAction: null, + }, + }, + }, + ]; + API.write(WRITE_COMMANDS.SAVE_SEARCH, {jsonQuery, name: saveSearchName}, {optimisticData, failureData, successData}); } function deleteSavedSearch(hash: number) { - API.write(WRITE_COMMANDS.DELETE_SAVED_SEARCH, {hash}); + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.SAVED_SEARCHES}`, + value: { + [hash]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }, + }, + }, + ]; + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.SAVED_SEARCHES}`, + value: { + [hash]: null, + }, + }, + ]; + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.SAVED_SEARCHES}`, + value: { + [hash]: { + pendingAction: null, + }, + }, + }, + ]; + + API.write(WRITE_COMMANDS.DELETE_SAVED_SEARCH, {hash}, {optimisticData, failureData, successData}); } function search({queryJSON, offset}: {queryJSON: SearchQueryJSON; offset?: number}) { diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 7c8af2388f52..e5e44169635d 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -34,7 +34,7 @@ import type IconAsset from '@src/types/utils/IconAsset'; import SavedSearchItemThreeDotMenu from './SavedSearchItemThreeDotMenu'; import SearchTypeMenuNarrow from './SearchTypeMenuNarrow'; -type SavedSearchMenuItem = MenuItemBaseProps & { +type SavedSearchMenuItem = MenuItemWithLink & { key: string; hash: string; query: string; @@ -119,6 +119,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { }, rightComponent: , styles: [styles.alignItemsCenter], + pendingAction: item.pendingAction, }; if (!isNarrow) { diff --git a/src/types/onyx/SaveSearch.ts b/src/types/onyx/SaveSearch.ts index d8f8bf32f2a1..129e063f77a3 100644 --- a/src/types/onyx/SaveSearch.ts +++ b/src/types/onyx/SaveSearch.ts @@ -1,13 +1,15 @@ +import type * as OnyxCommon from './OnyxCommon'; + /** * Model of a single saved search */ -type SaveSearchItem = { +type SaveSearchItem = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Name of the saved search */ name: string; /** Query string for the saved search */ query: string; -}; +}>; /** * Model of saved searches From 6e59afcac8920ba482374472e1c679509be5abba Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 20 Sep 2024 15:56:31 +0700 Subject: [PATCH 2/7] fix lint --- src/pages/Search/SearchTypeMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index e5e44169635d..08340fe0279e 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -4,7 +4,6 @@ import {View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {ScrollView as RNScrollView, ScrollViewProps, TextStyle, ViewStyle} from 'react-native'; import {useOnyx} from 'react-native-onyx'; -import type {MenuItemBaseProps} from '@components/MenuItem'; import MenuItem from '@components/MenuItem'; import MenuItemList from '@components/MenuItemList'; import type {MenuItemWithLink} from '@components/MenuItemList'; @@ -179,6 +178,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { if (!savedSearches) { return []; } + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style return Object.entries(savedSearches).map(([key, item]) => createSavedSearchMenuItem(item as SaveSearchItem, key, shouldUseNarrowLayout)); }; From 9f7e4649a03700be35e91a8867422d1afead7082 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 20 Sep 2024 17:53:55 +0700 Subject: [PATCH 3/7] fix: add offline feedback in narrow --- src/components/PopoverMenu.tsx | 90 ++++++++++++----------- src/pages/Search/SearchTypeMenuNarrow.tsx | 7 +- 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 3b074bf772e6..e3a04903f5ca 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -14,6 +14,7 @@ import * as Browser from '@libs/Browser'; import * as Modal from '@userActions/Modal'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; +import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; import FocusableMenuItem from './FocusableMenuItem'; import FocusTrapForModal from './FocusTrap/FocusTrapForModal'; @@ -21,6 +22,7 @@ import * as Expensicons from './Icon/Expensicons'; import type {MenuItemProps} from './MenuItem'; import MenuItem from './MenuItem'; import type BaseModalProps from './Modal/types'; +import OfflineWithFeedback from './OfflineWithFeedback'; import PopoverWithMeasuredContent from './PopoverWithMeasuredContent'; import ScrollView from './ScrollView'; import Text from './Text'; @@ -48,6 +50,8 @@ type PopoverMenuItem = MenuItemProps & { /** Whether to close all modals */ shouldCloseAllModals?: boolean; + + pendingAction?: PendingAction; }; type PopoverModalProps = Pick; @@ -262,49 +266,53 @@ function PopoverMenu({ {renderHeaderText()} {enteredSubMenuIndexes.length > 0 && renderBackButtonItem()} {currentMenuItems.map((item, menuIndex) => ( - selectItem(menuIndex)} - 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}} - isLabelHoverable={item.isLabelHoverable} - floatRightAvatars={item.floatRightAvatars} - floatRightAvatarSize={item.floatRightAvatarSize} - shouldShowSubscriptRightAvatar={item.shouldShowSubscriptRightAvatar} - disabled={item.disabled} - onFocus={() => setFocusedIndex(menuIndex)} - success={item.success} - containerStyle={item.containerStyle} - shouldRenderTooltip={item.shouldRenderTooltip} - tooltipAnchorAlignment={item.tooltipAnchorAlignment} - tooltipShiftHorizontal={item.tooltipShiftHorizontal} - tooltipShiftVertical={item.tooltipShiftVertical} - tooltipWrapperStyle={item.tooltipWrapperStyle} - renderTooltipContent={item.renderTooltipContent} - numberOfLinesTitle={item.numberOfLinesTitle} - interactive={item.interactive} - isSelected={item.isSelected} - badgeText={item.badgeText} - /> + pendingAction={item.pendingAction} + > + selectItem(menuIndex)} + 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}} + isLabelHoverable={item.isLabelHoverable} + floatRightAvatars={item.floatRightAvatars} + floatRightAvatarSize={item.floatRightAvatarSize} + shouldShowSubscriptRightAvatar={item.shouldShowSubscriptRightAvatar} + disabled={item.disabled} + onFocus={() => setFocusedIndex(menuIndex)} + success={item.success} + containerStyle={item.containerStyle} + shouldRenderTooltip={item.shouldRenderTooltip} + tooltipAnchorAlignment={item.tooltipAnchorAlignment} + tooltipShiftHorizontal={item.tooltipShiftHorizontal} + tooltipShiftVertical={item.tooltipShiftVertical} + tooltipWrapperStyle={item.tooltipWrapperStyle} + renderTooltipContent={item.renderTooltipContent} + numberOfLinesTitle={item.numberOfLinesTitle} + interactive={item.interactive} + isSelected={item.isSelected} + badgeText={item.badgeText} + /> + ))} diff --git a/src/pages/Search/SearchTypeMenuNarrow.tsx b/src/pages/Search/SearchTypeMenuNarrow.tsx index 0158a15bfc41..0b55c32c2224 100644 --- a/src/pages/Search/SearchTypeMenuNarrow.tsx +++ b/src/pages/Search/SearchTypeMenuNarrow.tsx @@ -3,7 +3,7 @@ import {Animated, View} from 'react-native'; import type {TextStyle, ViewStyle} from 'react-native'; import Button from '@components/Button'; import Icon from '@components/Icon'; -import type {MenuItemBaseProps} from '@components/MenuItem'; +import type {MenuItemWithLink} from '@components/MenuItemList'; import PopoverMenu from '@components/PopoverMenu'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; @@ -26,7 +26,7 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {SearchTypeMenuItem} from './SearchTypeMenu'; -type SavedSearchMenuItem = MenuItemBaseProps & { +type SavedSearchMenuItem = MenuItemWithLink & { key: string; hash: string; query: string; @@ -121,8 +121,8 @@ function SearchTypeMenuNarrow({typeMenuItems, activeItemIndex, queryJSON, title, /> ), isSelected: currentSavedSearch?.hash === item.hash, + pendingAction: item.pendingAction, })); - const allMenuItems = []; allMenuItems.push(...popoverMenuItems); @@ -134,7 +134,6 @@ function SearchTypeMenuNarrow({typeMenuItems, activeItemIndex, queryJSON, title, }); allMenuItems.push(...savedSearchItems); } - return ( Date: Fri, 20 Sep 2024 18:03:34 +0700 Subject: [PATCH 4/7] remove disable eslint --- src/pages/Search/SearchTypeMenu.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 08340fe0279e..940fbf959582 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -178,8 +178,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { if (!savedSearches) { return []; } - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - return Object.entries(savedSearches).map(([key, item]) => createSavedSearchMenuItem(item as SaveSearchItem, key, shouldUseNarrowLayout)); + return Object.entries(savedSearches).map(([key, item]) => createSavedSearchMenuItem(item ?? ({} as SaveSearchItem), key, shouldUseNarrowLayout)); }; const renderSavedSearchesSection = useCallback( From 5429647be60d3693b325c999fd0e4afd5ed65375 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 20 Sep 2024 18:15:43 +0700 Subject: [PATCH 5/7] update type --- src/pages/Search/SearchTypeMenu.tsx | 2 +- src/types/onyx/SaveSearch.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 940fbf959582..adb680541882 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -178,7 +178,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { if (!savedSearches) { return []; } - return Object.entries(savedSearches).map(([key, item]) => createSavedSearchMenuItem(item ?? ({} as SaveSearchItem), key, shouldUseNarrowLayout)); + return Object.entries(savedSearches).map(([key, item]) => createSavedSearchMenuItem(item, key, shouldUseNarrowLayout)); }; const renderSavedSearchesSection = useCallback( diff --git a/src/types/onyx/SaveSearch.ts b/src/types/onyx/SaveSearch.ts index 129e063f77a3..6b3a903b1639 100644 --- a/src/types/onyx/SaveSearch.ts +++ b/src/types/onyx/SaveSearch.ts @@ -14,6 +14,6 @@ type SaveSearchItem = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** * Model of saved searches */ -type SaveSearch = Record; +type SaveSearch = Record; export type {SaveSearch, SaveSearchItem}; From 203403cbedf15b19a848e0742491888ef99a7f20 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 25 Sep 2024 15:32:42 +0700 Subject: [PATCH 6/7] fix: deleted item remains actionable --- src/pages/Search/SavedSearchItemThreeDotMenu.tsx | 11 ++++++++--- src/pages/Search/SearchTypeMenu.tsx | 8 +++++++- src/pages/Search/SearchTypeMenuNarrow.tsx | 2 ++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/pages/Search/SavedSearchItemThreeDotMenu.tsx b/src/pages/Search/SavedSearchItemThreeDotMenu.tsx index fdb06828901e..bd7a94bc1840 100644 --- a/src/pages/Search/SavedSearchItemThreeDotMenu.tsx +++ b/src/pages/Search/SavedSearchItemThreeDotMenu.tsx @@ -2,18 +2,23 @@ import React, {useRef, useState} from 'react'; import {View} from 'react-native'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; +import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; type SavedSearchItemThreeDotMenuProps = { menuItems: PopoverMenuItem[]; + isDisabledItem: boolean; }; -function SavedSearchItemThreeDotMenu({menuItems}: SavedSearchItemThreeDotMenuProps) { +function SavedSearchItemThreeDotMenu({menuItems, isDisabledItem}: SavedSearchItemThreeDotMenuProps) { const threeDotsMenuContainerRef = useRef(null); const [threeDotsMenuPosition, setThreeDotsMenuPosition] = useState({horizontal: 0, vertical: 0}); - + const styles = useThemeStyles(); return ( - + { diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 704ebbb5316e..947f81ac4e8d 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -116,9 +116,15 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { SearchActions.clearAllFilters(); Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: item?.query ?? ''})); }, - rightComponent: , + rightComponent: ( + + ), styles: [styles.alignItemsCenter], pendingAction: item.pendingAction, + disabled: item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, }; if (!isNarrow) { diff --git a/src/pages/Search/SearchTypeMenuNarrow.tsx b/src/pages/Search/SearchTypeMenuNarrow.tsx index 5c466b95d461..06bc016669df 100644 --- a/src/pages/Search/SearchTypeMenuNarrow.tsx +++ b/src/pages/Search/SearchTypeMenuNarrow.tsx @@ -118,10 +118,12 @@ function SearchTypeMenuNarrow({typeMenuItems, activeItemIndex, queryJSON, title, horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, }} + disabled={item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE} /> ), isSelected: currentSavedSearch?.hash === item.hash, pendingAction: item.pendingAction, + disabled: item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, })); const allMenuItems = []; allMenuItems.push(...popoverMenuItems); From f04c1e9533a695dbf7ed1776ffd7ed54beae30ee Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 25 Sep 2024 15:34:39 +0700 Subject: [PATCH 7/7] enable form when offline --- src/pages/Search/SavedSearchRenamePage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Search/SavedSearchRenamePage.tsx b/src/pages/Search/SavedSearchRenamePage.tsx index 2b227e581ac4..bc9a41597fa2 100644 --- a/src/pages/Search/SavedSearchRenamePage.tsx +++ b/src/pages/Search/SavedSearchRenamePage.tsx @@ -56,6 +56,7 @@ function SavedSearchRenamePage({route}: {route: {params: {q: string; name: strin submitButtonText={translate('common.save')} onSubmit={onSaveSearch} style={[styles.mh5, styles.flex1]} + enabledWhenOffline >