Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 41 additions & 13 deletions src/hooks/useSearchTypeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,30 @@ import ThreeDotsMenu from '@components/ThreeDotsMenu';
import {setSearchContext} from '@libs/actions/Search';
import Navigation from '@libs/Navigation/Navigation';
import {getAllTaxRates} from '@libs/PolicyUtils';
import {buildSearchQueryJSON, buildUserReadableQueryString, shouldSkipSuggestedSearchNavigation as shouldSkipSuggestedSearchNavigationForQuery} from '@libs/SearchQueryUtils';
import {
buildSearchQueryJSON,
buildSearchQueryString,
buildUserReadableQueryString,
shouldSkipSuggestedSearchNavigation as shouldSkipSuggestedSearchNavigationForQuery,
} from '@libs/SearchQueryUtils';
import type {SavedSearchMenuItem} from '@libs/SearchUIUtils';
import {createBaseSavedSearchMenuItem, getItemBadgeText, getOverflowMenu as getOverflowMenuUtil} from '@libs/SearchUIUtils';
import {createBaseSavedSearchMenuItem, getActiveSearchItemIndex, getItemBadgeText, getOverflowMenu as getOverflowMenuUtil, updateQueryStringOnSearchTypeChange} from '@libs/SearchUIUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import todosReportCountsSelector from '@src/selectors/Todos';
import type {Report} from '@src/types/onyx';
import {getEmptyObject} from '@src/types/utils/EmptyObject';
import {getEmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import useDeleteSavedSearch from './useDeleteSavedSearch';
import useFeedKeysWithAssignedCards from './useFeedKeysWithAssignedCards';
import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
import useLocalize from './useLocalize';
import useOnyx from './useOnyx';
import useSearchTypeMenuSections from './useSearchTypeMenuSections';
import useSingleExecution from './useSingleExecution';
import useStickySearchFilters from './useStickySearchFilters';
import useSuggestedSearchDefaultNavigation from './useSuggestedSearchDefaultNavigation';
import useTheme from './useTheme';
import useThemeStyles from './useThemeStyles';
Expand All @@ -48,7 +55,8 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {
const [reports = getEmptyObject<NonNullable<OnyxCollection<Report>>>()] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const taxRates = getAllTaxRates(allPolicies);
const [personalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST);
const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES);
const [savedSearches, savedSearchesMetadata] = useOnyx(ONYXKEYS.SAVED_SEARCHES);
const isLoadingSavedSearches = isLoadingOnyxValue(savedSearchesMetadata);
const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector});
const [reportCounts = CONST.EMPTY_TODOS_REPORT_COUNTS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {selector: todosReportCountsSelector});
const expensifyIcons = useMemoizedLazyExpensifyIcons([
Expand Down Expand Up @@ -181,14 +189,12 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {
translate,
]);

const activeItemIndex = useMemo(() => {
// If we have a suggested search, then none of the menu items are active
if (isSavedSearchActive) {
return -1;
}
const [activeItemIndex, isExploreSectionActive] = useMemo(
() => getActiveSearchItemIndex(flattenedMenuItems, similarSearchHash, isSavedSearchActive, queryJSON?.type),
[similarSearchHash, isSavedSearchActive, flattenedMenuItems, queryJSON?.type],
);

return flattenedMenuItems.findIndex((item) => item.similarSearchHash === similarSearchHash);
}, [similarSearchHash, isSavedSearchActive, flattenedMenuItems]);
const allSearchAdvancedFilters = useStickySearchFilters(isExploreSectionActive && !shouldShowSuggestedSearchSkeleton && !isLoadingSavedSearches);

const popoverMenuItems = useMemo(() => {
return typeMenuSections
Expand Down Expand Up @@ -223,7 +229,16 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {
shouldCallAfterModalHide: true,
onSelected: singleExecution(() => {
setSearchContext(false);
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: item.searchQuery}));
let queryString = item.searchQuery;

if (section.translationPath === 'common.explore' && !isEmptyObject(allSearchAdvancedFilters)) {
queryString = updateQueryStringOnSearchTypeChange(item.type, allSearchAdvancedFilters, queryJSON);
// If the focused menu item is not in the explore section, but the queryString matches the current one, it will fall back to the default value
if (!isExploreSectionActive && queryString === buildSearchQueryString(queryJSON)) {
queryString = item.searchQuery;
}
}
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: queryString}));
}),
});
}
Expand All @@ -232,7 +247,20 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {
return sectionItems;
})
.flat();
}, [typeMenuSections, translate, styles.textSupporting, savedSearchesMenuItems, activeItemIndex, theme.border, expensifyIcons, singleExecution, reportCounts]);
}, [
typeMenuSections,
translate,
styles.textSupporting,
savedSearchesMenuItems,
activeItemIndex,
expensifyIcons,
theme.border,
singleExecution,
reportCounts,
allSearchAdvancedFilters,
queryJSON,
isExploreSectionActive,
]);

const openMenu = useCallback(() => {
setIsPopoverVisible(true);
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useSearchTypeMenuSections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const useSearchTypeMenuSections = () => {
}, [pendingReportCreation, openCreateReportConfirmation]);

const isSuggestedSearchDataReady = useMemo(() => {
return Object.values(allPolicies ?? {}).some((policy) => policy?.employeeList !== undefined && policy?.exporter !== undefined);
return Object.values(allPolicies ?? {}).some((policy) => policy?.id !== undefined && policy?.employeeList !== undefined && policy?.exporter !== undefined);
}, [allPolicies]);

const typeMenuSections = useMemo(
Expand Down
69 changes: 69 additions & 0 deletions src/hooks/useStickySearchFilters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {useMemo} from 'react';
import ONYXKEYS from '@src/ONYXKEYS';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
import {getEmptyObject} from '@src/types/utils/EmptyObject';
import useOnyx from './useOnyx';

const allSearchAdvancedFilters: {current: Partial<SearchAdvancedFiltersForm>} = {current: {}};
const prevSearchAdvancedFiltersFormsByType: {current: Record<string, Partial<SearchAdvancedFiltersForm> | undefined>} = {current: {}};

/**
* Reset all values of the sticky search filters
*/
function resetStickySearchFiltersValues() {
allSearchAdvancedFilters.current = {};
prevSearchAdvancedFiltersFormsByType.current = {};
}

/**
* This hook helps retain all filter values and will only update the filters that have changed
*/
export default function useStickySearchFilters(shouldUpdate?: boolean) {
const [searchAdvancedFiltersForm = getEmptyObject<Partial<SearchAdvancedFiltersForm>>()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);

const currentAllSearchAdvancedFilters = useMemo(() => {
if (!shouldUpdate || !searchAdvancedFiltersForm.type) {
return allSearchAdvancedFilters.current;
}

const prevSearchAdvancedFiltersForm = prevSearchAdvancedFiltersFormsByType.current[searchAdvancedFiltersForm.type];
const allKeys = new Set([...Object.keys(searchAdvancedFiltersForm), ...Object.keys(prevSearchAdvancedFiltersForm ?? {})]) as Set<keyof typeof searchAdvancedFiltersForm>;
const changedKeys: Array<keyof typeof searchAdvancedFiltersForm> = [];
for (const key of allKeys) {
const currentValue = searchAdvancedFiltersForm[key];
const previousValue = prevSearchAdvancedFiltersForm?.[key];
if (Array.isArray(currentValue) && Array.isArray(previousValue)) {
if (currentValue.sort().join(',') === previousValue.sort().join(',')) {
continue;
}
} else if (Object.is(currentValue, previousValue)) {
continue;
}

changedKeys.push(key);
}

for (const key of changedKeys) {
if (!prevSearchAdvancedFiltersForm && allSearchAdvancedFilters.current[key]) {
continue;
}
(allSearchAdvancedFilters.current[key] as unknown) = searchAdvancedFiltersForm[key] ?? undefined;
}
allSearchAdvancedFilters.current = {...allSearchAdvancedFilters.current, type: searchAdvancedFiltersForm.type};
prevSearchAdvancedFiltersFormsByType.current[searchAdvancedFiltersForm.type] = searchAdvancedFiltersForm;

return allSearchAdvancedFilters.current;
// Here we only rely on `searchAdvancedFiltersForm`, without triggering when `shouldUpdate`,
// because `shouldUpdate` is just a flag indicating that an update can happen,
// and the actual update only occurs when `searchAdvancedFiltersForm` has truly been updated.
// And since `shouldUpdate` is a value derived from queryJSON data,
// when `searchAdvancedFiltersForm` is updated via useOnyx,
// `shouldUpdate` has already been updated beforehand,
// so there’s no concern about having an incorrect value.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchAdvancedFiltersForm]);

return currentAllSearchAdvancedFilters;
}

export {resetStickySearchFiltersValues};
81 changes: 81 additions & 0 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
import type * as OnyxTypes from '@src/types/onyx';
import type {ConnectionName} from '@src/types/onyx/Policy';
import type {SaveSearchItem} from '@src/types/onyx/SaveSearch';
Expand All @@ -79,6 +80,7 @@ import type {
SearchTransactionAction,
SearchWithdrawalIDGroup,
} from '@src/types/onyx/SearchResults';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
import arraysEqual from '@src/utils/arraysEqual';
import {hasSynchronizationErrorMessage} from './actions/connections';
Expand Down Expand Up @@ -4368,6 +4370,83 @@ function navigateToSearchRHP(route: {route: string; getRoute: (backTo?: string)
}
}

/**
* Returns the index of the active item in flattenedMenuItems by comparing similarSearchHash.
*
* Also returns a value indicating whether the item in the Explore section is active
*/
function getActiveSearchItemIndex(
flattenedMenuItems: SearchTypeMenuItem[],
similarSearchHash: number | undefined,
isSavedSearchActive: boolean,
queryType: string | undefined,
): [number, boolean] {
// If we have a suggested search, then none of the menu items are active
if (isSavedSearchActive) {
return [-1, false];
}

let activeItemIndex = flattenedMenuItems.findIndex((item) => item.similarSearchHash === similarSearchHash);
if (activeItemIndex === -1) {
activeItemIndex = flattenedMenuItems.findIndex((item) => {
if (queryType === CONST.SEARCH.DATA_TYPES.EXPENSE) {
return item.key === CONST.SEARCH.SEARCH_KEYS.EXPENSES;
}
if (queryType === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) {
return item.key === CONST.SEARCH.SEARCH_KEYS.REPORTS;
}
if (queryType === CONST.SEARCH.DATA_TYPES.CHAT) {
return item.key === CONST.SEARCH.SEARCH_KEYS.CHATS;
}
return false;
});
}
const activeItemKey = activeItemIndex !== -1 ? flattenedMenuItems.at(activeItemIndex)?.key : undefined;
const isExploreSectionActive =
!!activeItemKey && ([CONST.SEARCH.SEARCH_KEYS.EXPENSES, CONST.SEARCH.SEARCH_KEYS.REPORTS, CONST.SEARCH.SEARCH_KEYS.CHATS] as string[]).includes(activeItemKey);
return [activeItemIndex, isExploreSectionActive];
}

/**
* Rebuild the query string based on searchAdvancedFiltersForm and the new value of the search type.
* The filter values that are valid for the new search type will be preserved.
*/
function updateQueryStringOnSearchTypeChange(type: SearchDataTypes, searchAdvancedFiltersForm: Partial<SearchAdvancedFiltersForm>, queryJSON: SearchQueryJSON | undefined): string {
const updatedFilterFormValues: Partial<SearchAdvancedFiltersForm> = {
...searchAdvancedFiltersForm,
type,
};

// If the type has changed, reset the columns
if (type !== searchAdvancedFiltersForm.type) {
// Filter Status options for current type
const currentStatus = typeof updatedFilterFormValues.status === 'string' ? updatedFilterFormValues.status.split(',') : (updatedFilterFormValues.status ?? []);
const validStatusSet = new Set(getStatusOptions(() => '', type).map((option) => option.value)) as Set<string>;
updatedFilterFormValues.status = currentStatus.filter((value) => validStatusSet.has(value));
updatedFilterFormValues.status = isEmptyObject(updatedFilterFormValues.status) ? CONST.SEARCH.STATUS.EXPENSE.ALL : updatedFilterFormValues.status;

// Filter Has options for current type
const currentHas = updatedFilterFormValues.has;
const validHasSet = new Set(getHasOptions(() => '', type).map((option) => option.value)) as Set<string>;
updatedFilterFormValues.has = currentHas?.filter((value) => validHasSet.has(value));
updatedFilterFormValues.has = isEmptyObject(updatedFilterFormValues.has) ? undefined : updatedFilterFormValues.has;

updatedFilterFormValues.columns = [];
}

// Preserve the current sortBy and sortOrder from queryJSON when updating filters
const updatedQueryString = buildQueryStringFromFilterFormValues(updatedFilterFormValues, {
sortBy: queryJSON?.sortBy,
sortOrder: queryJSON?.sortOrder,
});

// We need to normalize the updatedQueryString using buildSearchQueryString.
const updatedQueryJSON = buildSearchQueryJSON(updatedQueryString);
const queryString = buildSearchQueryString(updatedQueryJSON);

return queryString;
}

function shouldShowDeleteOption(
selectedTransactions: Record<string, SelectedTransactionInfo>,
currentSearchResults: SearchResults['data'] | undefined,
Expand Down Expand Up @@ -4474,6 +4553,8 @@ export {
getCustomColumnDefault,
filterValidHasValues,
navigateToSearchRHP,
getActiveSearchItemIndex,
updateQueryStringOnSearchTypeChange,
shouldShowDeleteOption,
getToFieldValueForTransaction,
isTodoSearch,
Expand Down
3 changes: 3 additions & 0 deletions src/libs/actions/Delegate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import HybridAppModule from '@expensify/react-native-hybrid-app';
import Onyx from 'react-native-onyx';
import type {OnyxEntry, OnyxKey, OnyxUpdate} from 'react-native-onyx';
import {resetStickySearchFiltersValues} from '@hooks/useStickySearchFilters';
import * as API from '@libs/API';
import type {AddDelegateParams as APIAddDelegateParams, RemoveDelegateParams as APIRemoveDelegateParams, UpdateDelegateRoleParams as APIUpdateDelegateRoleParams} from '@libs/API/parameters';
import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
Expand Down Expand Up @@ -186,6 +187,7 @@ function connect({email, delegatedAccess, credentials, session, activePolicyID,
return;
}
clearPreservedSearchNavigatorStates();
resetStickySearchFiltersValues();
const restrictedToken = response.restrictedToken;
const policyID = activePolicyID;

Expand Down Expand Up @@ -275,6 +277,7 @@ function disconnect({stashedCredentials, stashedSession}: DisconnectParams) {
}

clearPreservedSearchNavigatorStates();
resetStickySearchFiltersValues();

const requesterEmail = response.requesterEmail;
const authToken = response.authToken;
Expand Down
2 changes: 2 additions & 0 deletions src/libs/actions/Session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import {InteractionManager} from 'react-native';
import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import {resetStickySearchFiltersValues} from '@hooks/useStickySearchFilters';
import {buildOldDotURL, openExternalLink} from '@libs/actions/Link';
import * as PersistedRequests from '@libs/actions/PersistedRequests';
import * as API from '@libs/API';
Expand Down Expand Up @@ -79,7 +80,7 @@
let isHybridAppSetupFinished = false;
let hasSwitchedAccountInHybridMode = false;

Onyx.connect({

Check warning on line 83 in src/libs/actions/Session/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
session = value ?? {};
Expand All @@ -104,25 +105,25 @@
});

let stashedSession: Session = {};
Onyx.connect({

Check warning on line 108 in src/libs/actions/Session/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.STASHED_SESSION,
callback: (value) => (stashedSession = value ?? {}),
});

let credentials: Credentials = {};
Onyx.connect({

Check warning on line 114 in src/libs/actions/Session/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CREDENTIALS,
callback: (value) => (credentials = value ?? {}),
});

let stashedCredentials: Credentials = {};
Onyx.connect({

Check warning on line 120 in src/libs/actions/Session/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.STASHED_CREDENTIALS,
callback: (value) => (stashedCredentials = value ?? {}),
});

let activePolicyID: OnyxEntry<string>;
Onyx.connect({

Check warning on line 126 in src/libs/actions/Session/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
callback: (newActivePolicyID) => {
activePolicyID = newActivePolicyID;
Expand Down Expand Up @@ -249,6 +250,7 @@
skipReauthentication: true,
};

resetStickySearchFiltersValues();
if (params.signedInWithSAML && params.authToken) {
return callSAMLSignOut(logOutParams, params.authToken);
}
Expand Down
12 changes: 11 additions & 1 deletion src/pages/Search/SearchAdvancedFiltersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import {resetStickySearchFiltersValues} from '@hooks/useStickySearchFilters';
import useThemeStyles from '@hooks/useThemeStyles';
import {clearAdvancedFilters} from '@libs/actions/Search';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -44,7 +45,16 @@ function SearchAdvancedFiltersPage() {
includeSafeAreaPaddingBottom
>
<HeaderWithBackButton title={translate('search.filtersHeader')}>
{shouldShowResetFilters && <TextLink onPress={clearAdvancedFilters}>{translate('search.resetFilters')}</TextLink>}
{shouldShowResetFilters && (
<TextLink
onPress={() => {
resetStickySearchFiltersValues();
clearAdvancedFilters();
}}
>
{translate('search.resetFilters')}
</TextLink>
)}
</HeaderWithBackButton>
<AdvancedSearchFilters />
</ScreenWrapper>
Expand Down
Loading
Loading