Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Search Save add Onyx optimistic and failure data #49513

Merged
merged 10 commits into from
Sep 27, 2024
Merged
2 changes: 1 addition & 1 deletion src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,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;
Expand Down
90 changes: 49 additions & 41 deletions src/components/PopoverMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ 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';
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';
Expand Down Expand Up @@ -48,6 +50,8 @@ type PopoverMenuItem = MenuItemProps & {

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

pendingAction?: PendingAction;
};

type PopoverModalProps = Pick<ModalProps, 'animationIn' | 'animationOut' | 'animationInTiming'>;
Expand Down Expand Up @@ -262,49 +266,53 @@ function PopoverMenu({
{renderHeaderText()}
{enteredSubMenuIndexes.length > 0 && renderBackButtonItem()}
{currentMenuItems.map((item, menuIndex) => (
<FocusableMenuItem
<OfflineWithFeedback
// eslint-disable-next-line react/no-array-index-key
key={`${item.text}_${menuIndex}`}
icon={item.icon}
iconWidth={item.iconWidth}
iconHeight={item.iconHeight}
iconFill={item.iconFill}
contentFit={item.contentFit}
title={item.text}
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
titleStyle={StyleSheet.flatten([styles.flex1, item.titleStyle])}
shouldCheckActionAllowedOnPress={false}
description={item.description}
numberOfLinesDescription={item.numberOfLinesDescription}
onPress={() => 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}
>
<FocusableMenuItem
icon={item.icon}
iconWidth={item.iconWidth}
iconHeight={item.iconHeight}
iconFill={item.iconFill}
contentFit={item.contentFit}
title={item.text}
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
titleStyle={StyleSheet.flatten([styles.flex1, item.titleStyle])}
shouldCheckActionAllowedOnPress={false}
description={item.description}
numberOfLinesDescription={item.numberOfLinesDescription}
onPress={() => 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}
/>
</OfflineWithFeedback>
))}
</ScrollView>
</FocusTrapForModal>
Expand Down
71 changes: 69 additions & 2 deletions src/libs/actions/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,78 @@ function saveSearch({queryJSON, newName}: {queryJSON: SearchQueryJSON; newName?:
const saveSearchName = newName ?? queryJSON?.inputQuery ?? '';
const jsonQuery = JSON.stringify(queryJSON);

API.write(WRITE_COMMANDS.SAVE_SEARCH, {jsonQuery, newName: 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, newName: 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}) {
Expand Down
6 changes: 3 additions & 3 deletions src/pages/Search/SearchTypeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -34,7 +33,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;
Expand Down Expand Up @@ -119,6 +118,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
},
rightComponent: <SavedSearchItemThreeDotMenu menuItems={getOverflowMenu(item.name, Number(key), item.query)} />,
styles: [styles.alignItemsCenter],
pendingAction: item.pendingAction,
};

if (!isNarrow) {
Expand Down Expand Up @@ -178,7 +178,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
if (!savedSearches) {
return [];
}
return Object.entries(savedSearches).map(([key, item], index) => createSavedSearchMenuItem(item as SaveSearchItem, key, shouldUseNarrowLayout, index));
return Object.entries(savedSearches).map(([key, item], index) => createSavedSearchMenuItem(item, key, shouldUseNarrowLayout, index));
};

const renderSavedSearchesSection = useCallback(
Expand Down
7 changes: 3 additions & 4 deletions src/pages/Search/SearchTypeMenuNarrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -121,8 +121,8 @@ function SearchTypeMenuNarrow({typeMenuItems, activeItemIndex, queryJSON, title,
/>
),
isSelected: currentSavedSearch?.hash === item.hash,
pendingAction: item.pendingAction,
}));

const allMenuItems = [];
allMenuItems.push(...popoverMenuItems);

Expand All @@ -134,7 +134,6 @@ function SearchTypeMenuNarrow({typeMenuItems, activeItemIndex, queryJSON, title,
});
allMenuItems.push(...savedSearchItems);
}

return (
<View style={[styles.pb4, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween, styles.ph5, styles.gap2]}>
<PressableWithFeedback
Expand Down
8 changes: 5 additions & 3 deletions src/types/onyx/SaveSearch.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
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
*/
type SaveSearch = Record<number, SaveSearchItem | null>;
type SaveSearch = Record<number, SaveSearchItem>;

export type {SaveSearch, SaveSearchItem};
Loading