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

[Search v2] Add displaying advanced filter values and type/status #46022

Merged
3 changes: 3 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5239,6 +5239,9 @@ const CONST = {
DRAFTS: 'drafts',
FINISHED: 'finished',
},
TYPE: {
EXPENSE: 'expense',
},
Comment on lines +5254 to +5256
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB we should eventually move this to CONST.SEARCH.DATA_TYPES.EXPENSE and get rid of transactions and reports. We'll do so as part of v2.2.

TABLE_COLUMNS: {
RECEIPT: 'receipt',
DATE: 'date',
Expand Down
2 changes: 2 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ const ROUTES = {

SEARCH_ADVANCED_FILTERS_TYPE: 'search/filters/type',

SEARCH_ADVANCED_FILTERS_STATUS: 'search/filters/status',

SEARCH_REPORT: {
route: 'search/:query/view/:reportID',
getRoute: (query: string, reportID: string) => `search/${query}/view/${reportID}` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const SCREENS = {
ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP',
ADVANCED_FILTERS_DATE_RHP: 'Search_Advanced_Filters_Date_RHP',
ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP',
ADVANCED_FILTERS_STATUS_RHP: 'Search_Advanced_Filters_Status_RHP',
TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP',
BOTTOM_TAB: 'Search_Bottom_Tab',
},
Expand Down
6 changes: 4 additions & 2 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3568,6 +3568,7 @@ export default {
search: {
selectMultiple: 'Select multiple',
resultsAreLimited: 'Search results are limited.',
viewResults: 'View results',
searchResults: {
emptyResults: {
title: 'Nothing to show',
Expand All @@ -3585,9 +3586,10 @@ export default {
filtersHeader: 'Filters',
filters: {
date: {
before: 'Before',
after: 'After',
before: (date?: string) => `Before ${date ?? ''}`,
after: (date?: string) => `After ${date ?? ''}`,
},
status: 'Status',
},
},
genericErrorPage: {
Expand Down
6 changes: 4 additions & 2 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3625,6 +3625,7 @@ export default {
search: {
selectMultiple: 'Seleccionar varios',
resultsAreLimited: 'Los resultados de búsqueda están limitados.',
viewResults: 'Ver resultados',
searchResults: {
emptyResults: {
title: 'No hay nada que ver aquí',
Expand All @@ -3642,9 +3643,10 @@ export default {
filtersHeader: 'Filtros',
filters: {
date: {
before: 'Antes de',
after: 'Después de',
before: (date?: string) => `Antes de ${date ?? ''}`,
after: (date?: string) => `Después de ${date ?? ''}`,
},
status: 'Estado',
},
},
genericErrorPage: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator<Searc
[SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchAdvancedFiltersPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersDatePage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersTypePage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersStatusPage').default,
});

const RestrictedActionModalStackNavigator = createModalStackNavigator<SearchReportParamList>({
Expand Down
1 change: 1 addition & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,7 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS,
[SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_DATE,
[SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_TYPE,
[SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_STATUS,
},
},
[SCREENS.RIGHT_MODAL.RESTRICTED_ACTION]: {
Expand Down
22 changes: 19 additions & 3 deletions src/libs/actions/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,26 @@ function exportSearchItemsToCSV(query: string, reportIDList: Array<string | unde
}

/**
* Updates the form values for the advanced search form.
* Updates the form values for the advanced filters search form.
*/
function updateAdvancedFilters(values: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM>) {
function updateAdvancedFilters(values: Partial<FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM>>) {
Onyx.merge(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, values);
}

export {search, createTransactionThread, deleteMoneyRequestOnSearch, holdMoneyRequestOnSearch, unholdMoneyRequestOnSearch, exportSearchItemsToCSV, updateAdvancedFilters};
/**
* Clears all values for the advanced filters search form.
*/
function clearAdvancedFilters() {
Onyx.set(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, null);
}

export {
search,
createTransactionThread,
deleteMoneyRequestOnSearch,
holdMoneyRequestOnSearch,
unholdMoneyRequestOnSearch,
exportSearchItemsToCSV,
updateAdvancedFilters,
clearAdvancedFilters,
};
90 changes: 69 additions & 21 deletions src/pages/Search/AdvancedSearchFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,102 @@
import {Str} from 'expensify-common';
import React, {useMemo} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import useLocalize from '@hooks/useLocalize';
import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import Navigation from '@libs/Navigation/Navigation';
import * as SearchActions from '@userActions/Search';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
import type INPUT_IDS from '@src/types/form/SearchAdvancedFiltersForm';

function getFilterDisplayTitle(filters: Record<string, string>, fieldName: string) {
// This is temporary because the full parsing of search query is not yet done
// TODO once we have values from query, this value should be `filters[fieldName].value`
return fieldName;
// the values of dateBefore+dateAfter map to just a single 'date' field on advanced filters
type AvailableFilters = ValueOf<typeof INPUT_IDS> | 'date';
Copy link
Contributor

@rayane-djouah rayane-djouah Jul 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better if we define the available filter names as constants in the CONST file, like this:

in CONST.SEARCH add:

ADVANCED_SEARCH_FILTERS: {
    FIELDS_NAMES: {
          DATE: 'date',
          ...
    },
},

Then, use ValueOf<typeof CONST.ADVANCED_SEARCH_FILTERS.FIELDS_NAMES> and replace the hardcoded values with the constants.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you suggest I have both the INPUT_IDS and also ADVANCED_SEARCH_FILTERS and use them here?

Btw this is most likely the only case where there are 2 inputs that map to 1 filter, because the actual filter will be date > xxxx and date < yyyy.
I think all the other filters will map 1-to-1 filter name to input.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can define it as follows:

const FIELDS_NAMES = Object.entries(INPUT_IDS).reduce<Record<string, string>>((fields, [key, value]) => {
    if (key === INPUT_IDS.DATE_AFTER || key === INPUT_IDS.DATE_BEFORE) {
        fields.DATE = 'date';
    } else {
        fields[key] = value;
    }
    return fields;
}, {});

type AvailableFilters = ValueOf<typeof FIELDS_NAMES>;

My concern here is to avoid using hardcoded values when referencing getFilterDisplayTitle, we should be using FIELDS_NAMES const instead.

Copy link
Contributor Author

@Kicu Kicu Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rayane-djouah after we merged the other PR with new query/syntax - CONSTs were modified, and now I can use better more fitting consts specific to query here.
So I didn't do it your way, but there will be no hardcoded values used here anymore


function getFilterDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, fieldName: AvailableFilters, translate: LocaleContextProps['translate']) {
if (fieldName === 'date') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (fieldName === 'date') {
if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE) {

const {dateAfter, dateBefore} = filters;
let dateValue = '';
if (dateBefore) {
dateValue = translate('search.filters.date.before', dateBefore);
}
if (dateBefore && dateAfter) {
dateValue += ', ';
}
if (dateAfter) {
dateValue += translate('search.filters.date.after', dateAfter);
}

return dateValue;
}

const filterValue = filters[fieldName];
return filterValue ? Str.recapitalize(filterValue) : undefined;
}

function AdvancedSearchFilters() {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {singleExecution} = useSingleExecution();
const waitForNavigate = useWaitForNavigation();

const [searchAdvancedFilters = {}] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);

const advancedFilters = useMemo(
() => [
{
title: getFilterDisplayTitle({}, 'title'),
title: getFilterDisplayTitle(searchAdvancedFilters, 'type', translate),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, here we can use FIELDS_NAMES.TYPE instead of 'type'

description: 'common.type' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_TYPE,
},
{
title: getFilterDisplayTitle({}, 'date'),
title: getFilterDisplayTitle(searchAdvancedFilters, 'date', translate),
description: 'common.date' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_DATE,
},
{
title: getFilterDisplayTitle(searchAdvancedFilters, 'status', translate),
description: 'search.filters.status' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_STATUS,
},
],
[],
[searchAdvancedFilters, translate],
);

return (
<View>
{advancedFilters.map((item) => {
const onPress = singleExecution(waitForNavigate(() => Navigation.navigate(item.route)));

return (
<MenuItemWithTopDescription
key={item.description}
title={item.title}
description={translate(item.description)}
shouldShowRightIcon
onPress={onPress}
/>
);
})}
<View style={[styles.flex1, styles.justifyContentBetween]}>
<View>
{advancedFilters.map((item) => {
const onPress = singleExecution(waitForNavigate(() => Navigation.navigate(item.route)));

return (
<MenuItemWithTopDescription
key={item.description}
title={item.title}
description={translate(item.description)}
shouldShowRightIcon
onPress={onPress}
/>
);
})}
</View>
<FormAlertWithSubmitButton
buttonText={translate('search.viewResults')}
containerStyles={[styles.m4]}
onSubmit={() => {
// here set the selected filters as new query and redirect to SearchResults page
// waiting for: https://github.com/Expensify/App/issues/45028 and https://github.com/Expensify/App/issues/45027
SearchActions.clearAdvancedFilters();
Navigation.goBack();
Kicu marked this conversation as resolved.
Show resolved Hide resolved
}}
/>
Copy link
Contributor

@rayane-djouah rayane-djouah Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the "View results" button should not be disabled offline. based on OfflineUX_Patterns_Flowchart, No offline pattern is needed here.

Screenshot 2024-07-29 at 3 27 32 PM
Suggested change
/>
enabledWhenOffline
/>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can keep it as is for now so that we don't block other issues, but I think we should disable it offline because the Search API command only works while the user is online.

</View>
);
}
Expand Down
88 changes: 88 additions & 0 deletions src/pages/Search/SearchFiltersStatusPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, {useMemo} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import type {FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@navigation/Navigation';
import * as SearchActions from '@userActions/Search';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

function SearchFiltersStatusPage() {
const styles = useThemeStyles();
const {translate} = useLocalize();

const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);

const activeItem = searchAdvancedFiltersForm?.status;

const filterStatusItems = useMemo(
() => [
{
text: translate('common.all'),
value: CONST.SEARCH.TAB.ALL,
keyForList: CONST.SEARCH.TAB.ALL,
Kicu marked this conversation as resolved.
Show resolved Hide resolved
isSelected: activeItem === CONST.SEARCH.TAB.ALL,
},
{
text: translate('common.shared'),
value: CONST.SEARCH.TAB.SHARED,
keyForList: CONST.SEARCH.TAB.SHARED,
isSelected: activeItem === CONST.SEARCH.TAB.SHARED,
},
{
text: translate('common.drafts'),
value: CONST.SEARCH.TAB.DRAFTS,
keyForList: CONST.SEARCH.TAB.DRAFTS,
isSelected: activeItem === CONST.SEARCH.TAB.DRAFTS,
},
{
text: translate('common.finished'),
value: CONST.SEARCH.TAB.FINISHED,
keyForList: CONST.SEARCH.TAB.FINISHED,
isSelected: activeItem === CONST.SEARCH.TAB.FINISHED,
},
],
[translate, activeItem],
);

const updateStatus = (values: Partial<FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM>>) => {
SearchActions.updateAdvancedFilters(values);
Navigation.goBack();
Kicu marked this conversation as resolved.
Show resolved Hide resolved
};

return (
<ScreenWrapper
testID={SearchFiltersStatusPage.displayName}
shouldShowOfflineIndicatorInWideScreen
offlineIndicatorStyle={styles.mtAuto}
>
<FullPageNotFoundView shouldShow={false}>
<HeaderWithBackButton title={translate('search.filters.status')} />
<View style={[styles.flex1]}>
<SelectionList
sections={[{data: filterStatusItems}]}
onSelectRow={(item) => {
updateStatus({
status: item.value,
});
}}
initiallyFocusedOptionKey={activeItem}
shouldStopPropagation
ListItem={RadioListItem}
/>
</View>
</FullPageNotFoundView>
</ScreenWrapper>
);
}

SearchFiltersStatusPage.displayName = 'SearchFiltersStatusPage';

export default SearchFiltersStatusPage;
47 changes: 42 additions & 5 deletions src/pages/Search/SearchFiltersTypePage.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,44 @@
import React from 'react';
import React, {useMemo} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import type {FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Text from '@src/components/Text';
import Navigation from '@navigation/Navigation';
import * as SearchActions from '@userActions/Search';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

function SearchFiltersTypePage() {
const styles = useThemeStyles();
const {translate} = useLocalize();

const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);

const activeItem = searchAdvancedFiltersForm?.type ?? CONST.SEARCH.TYPE.EXPENSE;

const filterTypeItems = useMemo(
() => [
{
text: translate('common.expenses'),
value: CONST.SEARCH.TYPE.EXPENSE,
keyForList: CONST.SEARCH.TYPE.EXPENSE,
isSelected: activeItem === CONST.SEARCH.TYPE.EXPENSE,
},
],
[translate, activeItem],
);

const updateType = (values: Partial<FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM>>) => {
SearchActions.updateAdvancedFilters(values);
Navigation.goBack();
};
Kicu marked this conversation as resolved.
Show resolved Hide resolved

return (
<ScreenWrapper
testID={SearchFiltersTypePage.displayName}
Expand All @@ -19,9 +47,18 @@ function SearchFiltersTypePage() {
>
<FullPageNotFoundView shouldShow={false}>
<HeaderWithBackButton title={translate('common.type')} />
<View style={[styles.flex1, styles.ph3]}>
{/* temporary placeholder, will be implemented in https://github.com/Expensify/App/issues/45026 */}
<Text>Advanced filters Type form</Text>
<View style={[styles.flex1]}>
<SelectionList
sections={[{data: filterTypeItems}]}
onSelectRow={(item) => {
updateType({
type: item.value,
});
}}
initiallyFocusedOptionKey={activeItem}
shouldStopPropagation
ListItem={RadioListItem}
/>
</View>
</FullPageNotFoundView>
</ScreenWrapper>
Expand Down
Loading
Loading