Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import TopBar from '@components/Navigation/TopBar';
import type {SearchQueryJSON} from '@components/Search/types';

import useLocalize from '@hooks/useLocalize';
import useSearchTypeMenuSections from '@hooks/useSearchTypeMenuSections';

import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow';

import React from 'react';

import getSearchPageHeaderTitle from './getSearchPageHeaderTitle';

type SearchPageHeaderNarrowProps = {
queryJSON: SearchQueryJSON;
shouldShowLoadingBar: boolean;
Expand All @@ -15,6 +18,10 @@ type SearchPageHeaderNarrowProps = {

function SearchPageHeaderNarrow({queryJSON, shouldShowLoadingBar = false, isMobileSelectionModeEnabled}: SearchPageHeaderNarrowProps) {
const {translate} = useLocalize();
const {typeMenuSections, activeItemIndex, activeSavedSearch} = useSearchTypeMenuSections(queryJSON);
const selectedItem = activeItemIndex >= 0 ? typeMenuSections.flatMap((section) => section.menuItems).at(activeItemIndex) : undefined;

const title = getSearchPageHeaderTitle({translate, type: queryJSON.type, activeSavedSearch, selectedItem});

if (isMobileSelectionModeEnabled) {
return <SearchSelectedNarrow queryJSON={queryJSON} />;
Expand All @@ -23,7 +30,7 @@ function SearchPageHeaderNarrow({queryJSON, shouldShowLoadingBar = false, isMobi
return (
<TopBar
shouldShowLoadingBar={shouldShowLoadingBar}
breadcrumbLabel={translate('common.spend')}
breadcrumbLabel={title}
shouldDisplayHelpButton
/>
);
Expand Down
26 changes: 6 additions & 20 deletions src/components/Search/SearchPageHeader/SearchPageHeaderWide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,20 @@ import type {SearchQueryJSON} from '@components/Search/types';
import useLocalize from '@hooks/useLocalize';
import useSearchTypeMenuSections from '@hooks/useSearchTypeMenuSections';

import CONST from '@src/CONST';

import React from 'react';

import getSearchPageHeaderTitle from './getSearchPageHeaderTitle';

type SearchPageHeaderWideProps = {
queryJSON: SearchQueryJSON;
};

function SearchPageHeaderWide({queryJSON}: SearchPageHeaderWideProps) {
const {translate} = useLocalize();
const {typeMenuSections, activeItemIndex} = useSearchTypeMenuSections(queryJSON);
const selectedItem = typeMenuSections.flatMap((section) => section.menuItems).at(activeItemIndex);

let title = translate('common.spend');
if (activeItemIndex >= 0 && selectedItem) {
title = translate(selectedItem.translationPath);
} else {
const {type} = queryJSON;
if (type === CONST.SEARCH.DATA_TYPES.TASK) {
title = translate(`common.tasks`);
} else if (type === CONST.SEARCH.DATA_TYPES.TRIP) {
title = translate(`travel.trips`);
} else if (type === CONST.SEARCH.DATA_TYPES.INVOICE) {
title = translate(`workspace.common.invoices`);
} else if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
title = translate(`common.chats`);
}
}
const {typeMenuSections, activeItemIndex, activeSavedSearch} = useSearchTypeMenuSections(queryJSON);
const selectedItem = activeItemIndex >= 0 ? typeMenuSections.flatMap((section) => section.menuItems).at(activeItemIndex) : undefined;

const title = getSearchPageHeaderTitle({translate, type: queryJSON.type, activeSavedSearch, selectedItem});

return (
<TopBar
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type {LocaleContextProps} from '@components/LocaleContextProvider';

import type {SearchTypeMenuItem} from '@libs/SearchUIUtils';

import CONST from '@src/CONST';
import type {SaveSearchItem} from '@src/types/onyx/SaveSearch';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';

type SearchPageHeaderTitleDeps = {
translate: LocaleContextProps['translate'];

/** The `type` of the current search query, used for the data-type fallbacks */
type: SearchDataTypes | undefined;

/** The saved search the current query maps to, if any (from `useSearchTypeMenuSections`) */
activeSavedSearch: SaveSearchItem | undefined;

/** The matched suggested-search menu item, if any (only pass when `activeItemIndex >= 0`) */
selectedItem: SearchTypeMenuItem | undefined;
};

/**
* Resolves the Search page header title using a single priority chain shared by the wide and narrow headers so the two
* can't diverge:
* a. the active saved search's display name,
* b. the matched suggested-search label, then the data-type fallbacks (task / trip / invoice / chat),
* c. the generic "Spend" fallback.
*/
function getSearchPageHeaderTitle({translate, type, activeSavedSearch, selectedItem}: SearchPageHeaderTitleDeps): string {
// a. Active saved search display name. `name` is a display string, not a translation key, so it's used directly
// (matching the LHN, which renders `item.name` for the `name !== query` case).
if (activeSavedSearch?.name) {
return activeSavedSearch.name;
}

// b. Matched suggested search.
if (selectedItem) {
return translate(selectedItem.translationPath);
}

// b (cont.). Data-type fallbacks.
if (type === CONST.SEARCH.DATA_TYPES.TASK) {
return translate('common.tasks');
}
if (type === CONST.SEARCH.DATA_TYPES.TRIP) {
return translate('travel.trips');
}
if (type === CONST.SEARCH.DATA_TYPES.INVOICE) {
return translate('workspace.common.invoices');
}
if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
return translate('common.chats');
}

// c. Generic fallback.
return translate('common.spend');
}

export default getSearchPageHeaderTitle;
32 changes: 17 additions & 15 deletions src/hooks/useSearchTypeMenuSections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,21 +143,22 @@ const useSearchTypeMenuSections = (queryParams?: UseSearchTypeMenuSectionsParams
],
);

const activeItemIndex = useMemo(() => {
const isSavedSearchActive =
hash !== undefined &&
!!savedSearches &&
Object.entries(savedSearches).some(([key, item]) => {
if (Number(key) !== hash) {
return false;
}
if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && !isOffline) {
return false;
}
return true;
});
// The saved search the current query maps to (keyed by `hash`), derived from the existing `savedSearches`
// subscription. Undefined when there is no match or when the match is pending deletion (unless offline).
const activeSavedSearch = (() => {
if (hash === undefined || !savedSearches) {
return undefined;
}
const item = savedSearches[hash];
if (!item || (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && !isOffline)) {
return undefined;
}
return item;
})();

if (isSavedSearchActive) {
const activeItemIndex = (() => {
// A saved search is not part of `typeMenuSections`, so keep suggested-search focus off it.
if (activeSavedSearch) {
return -1;
}

Expand Down Expand Up @@ -193,14 +194,15 @@ const useSearchTypeMenuSections = (queryParams?: UseSearchTypeMenuSectionsParams
}

return -1;
}, [typeMenuSections, savedSearches, hash, similarSearchHash, sortBy, sortOrder, type, isOffline]);
})();

const activeKey = activeItemIndex < 0 ? undefined : typeMenuSections.flatMap((section) => section.menuItems).at(activeItemIndex)?.key;

return {
typeMenuSections,
activeItemIndex,
activeKey,
activeSavedSearch,
};
};

Expand Down
21 changes: 13 additions & 8 deletions src/pages/Search/SavedSearchRenamePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import type {SearchQueryJSON} from '@components/Search/types';
Expand All @@ -12,44 +13,48 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {saveSearch} from '@libs/actions/Search';
import Navigation from '@libs/Navigation/Navigation';
import {buildCannedSearchQuery, buildSearchQueryJSON} from '@libs/SearchQueryUtils';
import {getFieldRequiredErrors} from '@libs/ValidationUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import INPUT_IDS from '@src/types/form/SearchSavedSearchRenameForm';

import React, {useState} from 'react';
import React from 'react';

function SavedSearchRenamePage({route}: {route: {params: {q: string; name: string}}}) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {q, name} = route.params;
const [newName, setNewName] = useState(name);
const {inputCallbackRef} = useAutoFocusInput();

const applyFiltersAndNavigate = () => {
const applyFiltersAndNavigate = (newName: string) => {
Navigation.dismissModal();
Navigation.isNavigationReady().then(() => {
Navigation.navigate(
ROUTES.SEARCH_ROOT.getRoute({
query: q,
name: newName?.trim(),
name: newName,
}),
);
});
};

const onSaveSearch = () => {
const onSaveSearch = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM>) => {
const newName = values[INPUT_IDS.NAME].trim();
const queryJSON = buildSearchQueryJSON(q || buildCannedSearchQuery()) ?? ({} as SearchQueryJSON);

saveSearch({
queryJSON,
newName: newName?.trim() || q,
newName,
});

applyFiltersAndNavigate();
applyFiltersAndNavigate(newName);
};

const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM> =>
getFieldRequiredErrors(values, [INPUT_IDS.NAME], translate);

return (
<ScreenWrapper
testID="SavedSearchRenamePage"
Expand All @@ -62,6 +67,7 @@ function SavedSearchRenamePage({route}: {route: {params: {q: string; name: strin
formID={ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM}
submitButtonText={translate('common.save')}
onSubmit={onSaveSearch}
validate={validate}
style={[styles.mh5, styles.flex1]}
enabledWhenOffline
shouldHideFixErrorsAlert
Expand All @@ -72,7 +78,6 @@ function SavedSearchRenamePage({route}: {route: {params: {q: string; name: strin
label={translate('search.searchName')}
accessibilityLabel={translate('search.searchName')}
role={CONST.ROLE.PRESENTATION}
onChangeText={(renamedName) => setNewName(renamedName)}
ref={inputCallbackRef}
defaultValue={name}
/>
Expand Down
16 changes: 9 additions & 7 deletions src/pages/Search/SearchSavePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import type {LocalizedTranslate} from '@components/LocaleContextProvider';
import ScreenWrapper from '@components/ScreenWrapper';
Expand All @@ -25,14 +26,15 @@ import {saveSearch} from '@libs/actions/Search';
import Navigation from '@libs/Navigation/Navigation';
import {getCustomColumnDefault, getSearchColumnTranslationKey, mapFiltersFormToLabelValueList} from '@libs/SearchUIUtils';
import type {SearchFilter} from '@libs/SearchUIUtils';
import {getFieldRequiredErrors} from '@libs/ValidationUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
import INPUT_IDS from '@src/types/form/SearchSaveForm';
import {getEmptyObject} from '@src/types/utils/EmptyObject';

import React, {useState} from 'react';
import React from 'react';
import {View} from 'react-native';

type FilterValueProps = {
Expand Down Expand Up @@ -154,21 +156,22 @@ function SearchSavePage() {
const {translate, localeCompare} = useLocalize();
const {convertToDisplayStringWithoutCurrency} = useCurrencyListActions();
const [searchAdvancedFiltersForm = getEmptyObject<Partial<SearchAdvancedFiltersForm>>()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
const [name, setName] = useState('');

const {currentSearchQueryJSON} = useSearchQueryContext();

const onSaveSearch = () => {
const onSaveSearch = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_SAVE_FORM>) => {
if (!currentSearchQueryJSON) {
Navigation.goBack();
return;
}

const newName = name.trim() || currentSearchQueryJSON?.inputQuery;
saveSearch({queryJSON: currentSearchQueryJSON, newName});
saveSearch({queryJSON: currentSearchQueryJSON, newName: values[INPUT_IDS.NAME].trim()});
Navigation.goBack();
};

const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_SAVE_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.SEARCH_SAVE_FORM> =>
getFieldRequiredErrors(values, [INPUT_IDS.NAME], translate);

const appliedFilters = mapFiltersFormToLabelValueList(searchAdvancedFiltersForm, undefined, translate, localeCompare, convertToDisplayStringWithoutCurrency);
const appliedDisplays = getAppliedDisplays(searchAdvancedFiltersForm, currentSearchQueryJSON, translate);

Expand All @@ -184,6 +187,7 @@ function SearchSavePage() {
formID={ONYXKEYS.FORMS.SEARCH_SAVE_FORM}
submitButtonText={translate('search.saveView')}
onSubmit={onSaveSearch}
validate={validate}
style={[styles.mh5, styles.flex1]}
enabledWhenOffline
shouldHideFixErrorsAlert
Expand All @@ -193,8 +197,6 @@ function SearchSavePage() {
InputComponent={TextInput}
inputID={INPUT_IDS.NAME}
ref={inputCallbackRef}
value={name}
onChangeText={setName}
placeholder={translate('common.name')}
accessibilityLabel={translate('common.name')}
role={CONST.ROLE.PRESENTATION}
Expand Down
Loading