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

Add card advanced filter for Search #46666

Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const ROUTES = {

SEARCH_ADVANCED_FILTERS_CATEGORY: 'search/filters/category',

SEARCH_ADVANCED_FILTERS_CARD: 'search/filters/card',

SEARCH_REPORT: {
route: 'search/view/:reportID',
getRoute: (reportID: string) => `search/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 @@ -36,6 +36,7 @@ const SCREENS = {
ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP',
ADVANCED_FILTERS_STATUS_RHP: 'Search_Advanced_Filters_Status_RHP',
ADVANCED_FILTERS_CATEGORY_RHP: 'Search_Advanced_Filters_Category_RHP',
ADVANCED_FILTERS_CARD_RHP: 'Search_Advanced_Filters_Card_RHP',
TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP',
BOTTOM_TAB: 'Search_Bottom_Tab',
},
Expand Down
103 changes: 103 additions & 0 deletions src/components/SelectionList/CardListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {Str} from 'expensify-common';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import Icon from '@components/Icon';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import SelectCircle from '@components/SelectCircle';
import TextWithTooltip from '@components/TextWithTooltip';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import type {BankIcon} from '@src/types/onyx/Bank';
import BaseListItem from './BaseListItem';
import type {BaseListItemProps, ListItem} from './types';

type CardListItemProps<TItem extends ListItem> = BaseListItemProps<TItem & {bankIcon?: BankIcon}>;

function CardListItem<TItem extends ListItem>({
item,
isFocused,
showTooltip,
isDisabled,
canSelectMultiple,
onSelectRow,
onCheckboxPress,
onDismissError,
rightHandSideComponent,
onFocus,
shouldSyncFocus,
}: CardListItemProps<TItem>) {
const styles = useThemeStyles();

const handleCheckboxPress = useCallback(() => {
if (onCheckboxPress) {
onCheckboxPress(item);
} else {
onSelectRow(item);
}
}, [item, onCheckboxPress, onSelectRow]);

return (
<BaseListItem
item={item}
wrapperStyle={[styles.flex1, styles.justifyContentBetween, styles.sidebarLinkInner, styles.userSelectNone, styles.peopleRow, isFocused && styles.sidebarLinkActive]}
isFocused={isFocused}
isDisabled={isDisabled}
showTooltip={showTooltip}
canSelectMultiple={canSelectMultiple}
onSelectRow={onSelectRow}
onDismissError={onDismissError}
rightHandSideComponent={rightHandSideComponent}
errors={item.errors}
pendingAction={item.pendingAction}
keyForList={item.keyForList}
onFocus={onFocus}
shouldSyncFocus={shouldSyncFocus}
>
<>
{item.bankIcon && (
<View style={[styles.mr3]}>
<Icon
src={item.bankIcon.icon}
width={item.bankIcon.iconWidth}
height={item.bankIcon.iconHeight}
additionalStyles={item.bankIcon.iconStyles}
/>
</View>
)}
<View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch, styles.optionRow]}>
<View style={[styles.flexRow, styles.alignItemsCenter]}>
<TextWithTooltip
shouldShowTooltip={showTooltip}
text={Str.removeSMSDomain(item.text ?? '')}
style={[
styles.optionDisplayName,
isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText,
item.isBold !== false && styles.sidebarLinkTextBold,
styles.pre,
item.alternateText ? styles.mb1 : null,
]}
/>
</View>
</View>
{canSelectMultiple && !item.isDisabled && (
<PressableWithFeedback
onPress={handleCheckboxPress}
disabled={isDisabled}
role={CONST.ROLE.BUTTON}
accessibilityLabel={item.text ?? ''}
style={[styles.ml2, styles.optionSelectCircle]}
>
<SelectCircle
isChecked={item.isSelected ?? false}
selectCircleStyles={styles.ml0}
/>
</PressableWithFeedback>
)}
</>
</BaseListItem>
);
}

CardListItem.displayName = 'CardListItem';

export default CardListItem;
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator<Searc
[SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersTypePage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersStatusPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersCategoryPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchFiltersCardPage').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 @@ -1018,6 +1018,7 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_TYPE,
[SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_STATUS,
[SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_CATEGORY,
[SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_CARD,
},
},
[SCREENS.RIGHT_MODAL.RESTRICTED_ACTION]: {
Expand Down
7 changes: 6 additions & 1 deletion src/libs/SearchUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,16 @@ function buildQueryStringFromFilters(filterValues: Partial<SearchAdvancedFilters
return `${CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS}:${filterValue as string}`;
}

if (filterKey === INPUT_IDS.CATEGORY && filterValues[filterKey]) {
if (filterKey === INPUT_IDS.CATEGORY && Array.isArray(filterValue) && filterValue.length > 0) {
const categories = filterValues[filterKey] ?? [];
return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY}:${categories.map(sanitizeString).join(',')}`;
}

if (filterKey === INPUT_IDS.CARD_ID && Array.isArray(filterValue) && filterValue.length > 0) {
const cardIDs = filterValues[filterKey] ?? [];
return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID}:${cardIDs.join(',')}`;
}

return undefined;
})
.filter(Boolean)
Expand Down
23 changes: 22 additions & 1 deletion src/pages/Search/AdvancedSearchFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
import type {CardList} from '@src/types/onyx';

function getFilterDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, fieldName: AdvancedFiltersKeys, translate: LocaleContextProps['translate']) {
if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE) {
Expand All @@ -40,6 +41,10 @@ function getFilterDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, fiel
const categories = filters[fieldName] ?? [];
return categories.join(', ');
}
if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID && filters[fieldName]) {
const cards = filters[fieldName] ?? [];
return cards.join(', ');
}

// Todo Once all Advanced filters are implemented this line can be cleaned up. See: https://github.com/Expensify/App/issues/45026
// @ts-expect-error this property access is temporarily an error, because not every SYNTAX_FILTER_KEYS is handled by form.
Expand All @@ -48,13 +53,24 @@ function getFilterDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, fiel
return filterValue ? Str.recapitalize(filterValue) : undefined;
}

function getFilterCardDisplayTitle(filters: Partial<SearchAdvancedFiltersForm>, cards: CardList) {
const filterValue = filters[CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID];
return filterValue
? Object.values(cards)
.filter((card) => filterValue.includes(card.cardID?.toString()))
.map((card) => card.bank)
.join(', ')
: 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 [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST);

const advancedFilters = useMemo(
() => [
Expand All @@ -78,8 +94,13 @@ function AdvancedSearchFilters() {
description: 'common.category' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_CATEGORY,
},
{
title: getFilterCardDisplayTitle(searchAdvancedFilters, cardList),
description: 'common.card' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_CARD,
},
],
[searchAdvancedFilters, translate],
[searchAdvancedFilters, translate, cardList],
);

const onFormSubmit = () => {
Expand Down
124 changes: 124 additions & 0 deletions src/pages/Search/SearchFiltersCardPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import Button from '@components/Button';
import type {FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import getBankIcon from '@components/Icon/BankIcons';
import type {BankName} from '@components/Icon/BankIconsUtils';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import CardListItem from '@components/SelectionList/CardListItem';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import type {CategorySection} from '@libs/OptionsListUtils';
import type {OptionData} from '@libs/ReportUtils';
import Navigation from '@navigation/Navigation';
import * as SearchActions from '@userActions/Search';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';

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

const [cardList] = useOnyx(ONYXKEYS.CARD_LIST);
const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
const currentCards = searchAdvancedFiltersForm?.cardID;
const [newCards, setNewCards] = useState(currentCards ?? []);

const sections = useMemo(() => {
const newSections: CategorySection[] = [];
const cards = Object.values(cardList ?? {})
.filter((card) => card.cardID)
.sort((a, b) => a.bank.localeCompare(b.bank))
.map((card) => {
const icon = getBankIcon({bankName: card.bank as BankName, isCard: true, styles});
Copy link
Member

Choose a reason for hiding this comment

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

This cast as BankName looks fishy and I'd rather we didn't have it. But I also did a search in the project and it seems that it is done similarly in a few places so maybe thats ok.
Leaving this comment for other people to see


return {
text: card.bank,
keyForList: card.cardID.toString(),
isSelected: newCards.includes(card.cardID.toString()),
bankIcon: icon,
};
});
newSections.push({
title: undefined,
data: cards,
shouldShow: cards.length > 0,
});
return newSections;
}, [cardList, styles, newCards]);

const updateCard = useCallback((values: Partial<FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM>>) => {
SearchActions.updateAdvancedFilters(values);
}, []);

const handleConfirmSelection = useCallback(() => {
updateCard({
cardID: newCards,
});
Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS);
}, [updateCard, newCards]);

const updateNewCards = useCallback(
(item: Partial<OptionData>) => {
if (!item.keyForList) {
return;
}
if (item.isSelected) {
setNewCards(newCards?.filter((card) => card !== item.keyForList));
} else {
setNewCards([...(newCards ?? []), item.keyForList]);
}
},
[newCards],
);

const footerContent = useMemo(
() => (
<Button
success
text={translate('common.save')}
pressOnEnter
onPress={handleConfirmSelection}
large
/>
),
[translate, handleConfirmSelection],
);

return (
<ScreenWrapper
testID={SearchFiltersCardPage.displayName}
shouldShowOfflineIndicatorInWideScreen
offlineIndicatorStyle={styles.mtAuto}
>
<FullPageNotFoundView shouldShow={false}>
<HeaderWithBackButton
title={translate('common.card')}
onBackButtonPress={() => {
Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS);
}}
/>
<View style={[styles.flex1]}>
<SelectionList
sections={sections}
onSelectRow={updateNewCards}
// headerMessage={noResultsFound ? translate('common.noResultsFound') : undefined}
footerContent={footerContent}
shouldStopPropagation
shouldShowTooltips
canSelectMultiple
ListItem={CardListItem}
/>
</View>
</FullPageNotFoundView>
</ScreenWrapper>
);
}

SearchFiltersCardPage.displayName = 'SearchFiltersCardPage';

export default SearchFiltersCardPage;
2 changes: 2 additions & 0 deletions src/types/form/SearchAdvancedFiltersForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const INPUT_IDS = {
DATE_BEFORE: 'dateBefore',
CATEGORY: 'category',
POLICY_ID: 'policyID',
CARD_ID: 'cardID',
} as const;

type InputID = ValueOf<typeof INPUT_IDS>;
Expand All @@ -21,6 +22,7 @@ type SearchAdvancedFiltersForm = Form<
[INPUT_IDS.STATUS]: string;
[INPUT_IDS.CATEGORY]: string[];
[INPUT_IDS.POLICY_ID]: string;
[INPUT_IDS.CARD_ID]: string[];
}
>;

Expand Down
Loading