Skip to content

Commit bc6f867

Browse files
authored
Merge pull request Expensify#62657 from Expensify/jsenyitko-payer-suggested-search
Re-enable "Pay" Suggested Search & Cleanup Logic
2 parents 53702a2 + 6672581 commit bc6f867

15 files changed

Lines changed: 303 additions & 98 deletions

File tree

src/components/Search/FilterDropdowns/DropdownButton.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,21 @@ function DropdownButton({label, value, viewportOffsetTop, PopoverComponent}: Dro
5151
vertical: 0,
5252
});
5353

54+
const onTriggerLayout = () => {
55+
triggerRef.current?.measureInWindow((x, y, _, height) => {
56+
setPopoverTriggerPosition({
57+
horizontal: x,
58+
vertical: y + height + PADDING_MODAL,
59+
});
60+
});
61+
};
62+
5463
/**
5564
* Toggle the overlay between open & closed, and re-calculate the
5665
* position of the trigger
5766
*/
5867
const toggleOverlay = () => {
59-
setIsOverlayVisible((previousValue) => {
60-
triggerRef.current?.measureInWindow((x, y, _, height) => {
61-
setPopoverTriggerPosition({
62-
horizontal: x,
63-
vertical: y + height + PADDING_MODAL,
64-
});
65-
});
66-
67-
return !previousValue;
68-
});
68+
setIsOverlayVisible((previousValue) => !previousValue);
6969
};
7070

7171
/**
@@ -96,6 +96,7 @@ function DropdownButton({label, value, viewportOffsetTop, PopoverComponent}: Dro
9696
ref={triggerRef}
9797
innerStyles={[isOverlayVisible && styles.buttonHoveredBG, {maxWidth: 256}]}
9898
onPress={toggleOverlay}
99+
onLayout={onTriggerLayout}
99100
>
100101
<CaretWrapper style={[styles.flex1, styles.mw100]}>
101102
<Text
@@ -128,7 +129,7 @@ function DropdownButton({label, value, viewportOffsetTop, PopoverComponent}: Dro
128129
height: CONST.POPOVER_DROPDOWN_MIN_HEIGHT,
129130
}}
130131
>
131-
<PopoverComponent closeOverlay={toggleOverlay} />
132+
{PopoverComponent({closeOverlay: toggleOverlay})}
132133
</PopoverWithMeasuredContent>
133134
</>
134135
);

src/components/Search/SearchPageHeader/SearchFiltersBar.tsx

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import type {PopoverComponentProps} from '@components/Search/FilterDropdowns/Dro
1515
import DropdownButton from '@components/Search/FilterDropdowns/DropdownButton';
1616
import type {MultiSelectItem} from '@components/Search/FilterDropdowns/MultiSelectPopup';
1717
import MultiSelectPopup from '@components/Search/FilterDropdowns/MultiSelectPopup';
18-
import type {SingleSelectItem} from '@components/Search/FilterDropdowns/SingleSelectPopup';
1918
import SingleSelectPopup from '@components/Search/FilterDropdowns/SingleSelectPopup';
2019
import UserSelectPopup from '@components/Search/FilterDropdowns/UserSelectPopup';
2120
import {useSearchContext} from '@components/Search/SearchContext';
@@ -36,7 +35,7 @@ import {getStatusOptions, getTypeOptions} from '@libs/SearchUIUtils';
3635
import CONST from '@src/CONST';
3736
import ONYXKEYS from '@src/ONYXKEYS';
3837
import ROUTES from '@src/ROUTES';
39-
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
38+
import type {SearchAdvancedFiltersForm} from '@src/types/form';
4039
import type {SearchHeaderOptionValue} from './SearchPageHeader';
4140

4241
type SearchFiltersBarProps = {
@@ -81,45 +80,47 @@ function SearchFiltersBar({queryJSON, headerButtonsOptions}: SearchFiltersBarPro
8180
return buildFilterFormValuesFromQuery(queryJSON, policyCategories, policyTagsLists, currencyList, personalDetails, allCards, reports, taxRates);
8281
}, [allCards, currencyList, personalDetails, policyCategories, policyTagsLists, queryJSON, reports, taxRates]);
8382

84-
// We need to create a stable key for filterFormValues so that we don't infinitely
85-
// re-render components that access all of the filterFormValues. This is due to the way
86-
// that react calculates diffs (it doesn't know how to compare objects).
87-
const filterFormValuesKey = JSON.stringify(filterFormValues);
83+
const updateFilterForm = useCallback(
84+
(values: Partial<SearchAdvancedFiltersForm>) => {
85+
const updatedFilterFormValues: Partial<SearchAdvancedFiltersForm> = {
86+
...filterFormValues,
87+
...values,
88+
};
89+
90+
// If the type has changed, reset the status so we dont have an invalid status selected
91+
if (updatedFilterFormValues.type !== filterFormValues.type) {
92+
updatedFilterFormValues.status = CONST.SEARCH.STATUS.EXPENSE.ALL;
93+
}
94+
95+
const filterString = buildQueryStringFromFilterFormValues(updatedFilterFormValues);
96+
const searchQueryJSON = buildSearchQueryJSON(filterString);
97+
const queryString = buildSearchQueryString(searchQueryJSON);
98+
99+
Navigation.setParams({q: queryString});
100+
},
101+
[filterFormValues],
102+
);
88103

89104
const openAdvancedFilters = useCallback(() => {
90105
updateAdvancedFilters(filterFormValues);
91106
Navigation.navigate(ROUTES.SEARCH_ADVANCED_FILTERS);
92-
93-
// Disable exhaustive deps because we use filterFormValuesKey as the dependency, which is a stable key based on filterFormValues
94-
// eslint-disable-next-line react-compiler/react-compiler
95-
// eslint-disable-next-line react-hooks/exhaustive-deps
96-
}, [filterFormValuesKey]);
107+
}, [filterFormValues]);
97108

98109
const typeComponent = useCallback(
99110
({closeOverlay}: PopoverComponentProps) => {
100111
const value = typeOptions.find((option) => option.value === type) ?? null;
101112

102-
const onChange = (item: SingleSelectItem<SearchDataTypes> | null) => {
103-
const hasTypeChanged = item?.value !== type;
104-
const newType = item?.value ?? CONST.SEARCH.DATA_TYPES.EXPENSE;
105-
// If the type has changed, reset the status so we dont have an invalid status selected
106-
const newStatus = hasTypeChanged ? CONST.SEARCH.STATUS.EXPENSE.ALL : status;
107-
const newGroupBy = hasTypeChanged ? undefined : groupBy;
108-
const query = buildSearchQueryString({...queryJSON, type: newType, status: newStatus, groupBy: newGroupBy});
109-
Navigation.setParams({q: query});
110-
};
111-
112113
return (
113114
<SingleSelectPopup
114115
label={translate('common.type')}
115116
value={value}
116117
items={typeOptions}
117118
closeOverlay={closeOverlay}
118-
onChange={onChange}
119+
onChange={(item) => updateFilterForm({type: item?.value ?? CONST.SEARCH.DATA_TYPES.EXPENSE})}
119120
/>
120121
);
121122
},
122-
[groupBy, queryJSON, status, translate, type, typeOptions],
123+
[translate, type, typeOptions, updateFilterForm],
123124
);
124125

125126
const statusComponent = useCallback(
@@ -130,8 +131,7 @@ function SearchFiltersBar({queryJSON, headerButtonsOptions}: SearchFiltersBarPro
130131

131132
const onChange = (selectedItems: Array<MultiSelectItem<SingularSearchStatus>>) => {
132133
const newStatus = selectedItems.length ? selectedItems.map((i) => i.value) : CONST.SEARCH.STATUS.EXPENSE.ALL;
133-
const query = buildSearchQueryString({...queryJSON, status: newStatus});
134-
Navigation.setParams({q: query});
134+
updateFilterForm({status: newStatus});
135135
};
136136

137137
return (
@@ -144,7 +144,7 @@ function SearchFiltersBar({queryJSON, headerButtonsOptions}: SearchFiltersBarPro
144144
/>
145145
);
146146
},
147-
[groupBy, queryJSON, status, translate, type],
147+
[groupBy, status, translate, type, updateFilterForm],
148148
);
149149

150150
const datePickerComponent = useCallback(
@@ -156,19 +156,13 @@ function SearchFiltersBar({queryJSON, headerButtonsOptions}: SearchFiltersBarPro
156156
};
157157

158158
const onChange = (selectedDates: DateSelectPopupValue) => {
159-
const newFilterFormValues = {
160-
...filterFormValues,
161-
...queryJSON,
159+
const dateFormValues = {
162160
dateAfter: selectedDates[CONST.SEARCH.DATE_MODIFIERS.AFTER] ?? undefined,
163161
dateBefore: selectedDates[CONST.SEARCH.DATE_MODIFIERS.BEFORE] ?? undefined,
164162
dateOn: selectedDates[CONST.SEARCH.DATE_MODIFIERS.ON] ?? undefined,
165163
};
166164

167-
const filterString = buildQueryStringFromFilterFormValues(newFilterFormValues);
168-
const newJSON = buildSearchQueryJSON(filterString);
169-
const queryString = buildSearchQueryString(newJSON);
170-
171-
Navigation.setParams({q: queryString});
165+
updateFilterForm(dateFormValues);
172166
};
173167

174168
return (
@@ -179,10 +173,7 @@ function SearchFiltersBar({queryJSON, headerButtonsOptions}: SearchFiltersBarPro
179173
/>
180174
);
181175
},
182-
// Disable exhaustive deps because we use filterFormValuesKey as the dependency, which is a stable key based on filterFormValues
183-
// eslint-disable-next-line react-compiler/react-compiler
184-
// eslint-disable-next-line react-hooks/exhaustive-deps
185-
[filterFormValuesKey, queryJSON],
176+
[filterFormValues.dateAfter, filterFormValues.dateBefore, filterFormValues.dateOn, updateFilterForm],
186177
);
187178

188179
const userPickerComponent = useCallback(
@@ -193,18 +184,11 @@ function SearchFiltersBar({queryJSON, headerButtonsOptions}: SearchFiltersBarPro
193184
<UserSelectPopup
194185
value={value}
195186
closeOverlay={closeOverlay}
196-
onChange={(selectedUsers) => {
197-
const newFilterFormValues = {...filterFormValues, from: selectedUsers};
198-
const queryString = buildQueryStringFromFilterFormValues(newFilterFormValues);
199-
Navigation.setParams({q: queryString});
200-
}}
187+
onChange={(selectedUsers) => updateFilterForm({from: selectedUsers})}
201188
/>
202189
);
203190
},
204-
// Disable exhaustive deps because we use filterFormValuesKey as the dependency, which is a stable key based on filterFormValues
205-
// eslint-disable-next-line react-compiler/react-compiler
206-
// eslint-disable-next-line react-hooks/exhaustive-deps
207-
[filterFormValuesKey],
191+
[filterFormValues.from, updateFilterForm],
208192
);
209193

210194
/**

src/components/Search/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
1616
import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll';
1717
import useThemeStyles from '@hooks/useThemeStyles';
1818
import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
19-
import {updateSearchResultsWithTransactionThreadReportID} from '@libs/actions/Search';
19+
import {openSearch, updateSearchResultsWithTransactionThreadReportID} from '@libs/actions/Search';
2020
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
2121
import Log from '@libs/Log';
2222
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
@@ -224,6 +224,10 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
224224
handleSearch({queryJSON, offset});
225225
}, [handleSearch, isOffline, offset, queryJSON]);
226226

227+
useEffect(() => {
228+
openSearch();
229+
}, []);
230+
227231
const {newSearchResultKey, handleSelectionListScroll} = useSearchHighlightAndScroll({
228232
searchResults,
229233
transactions,

src/libs/API/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,7 @@ const READ_COMMANDS = {
10141014
OPEN_PLAID_BANK_LOGIN: 'OpenPlaidBankLogin',
10151015
OPEN_PLAID_CARDS_BANK_LOGIN: 'OpenPlaidCardsBankLogin',
10161016
OPEN_PLAID_BANK_ACCOUNT_SELECTOR: 'OpenPlaidBankAccountSelector',
1017+
OPEN_SEARCH_PAGE: 'OpenSearchPage',
10171018
GET_OLDER_ACTIONS: 'GetOlderActions',
10181019
GET_NEWER_ACTIONS: 'GetNewerActions',
10191020
EXPAND_URL_PREVIEW: 'ExpandURLPreview',
@@ -1104,6 +1105,7 @@ type ReadCommandParameters = {
11041105
[READ_COMMANDS.OPEN_ONFIDO_FLOW]: null;
11051106
[READ_COMMANDS.OPEN_INITIAL_SETTINGS_PAGE]: null;
11061107
[READ_COMMANDS.OPEN_ENABLE_PAYMENTS_PAGE]: null;
1108+
[READ_COMMANDS.OPEN_SEARCH_PAGE]: null;
11071109
[READ_COMMANDS.BEGIN_SIGNIN]: Parameters.BeginSignInParams;
11081110
[READ_COMMANDS.SIGN_IN_WITH_SHORT_LIVED_AUTH_TOKEN]: Parameters.SignInWithShortLivedAuthTokenParams;
11091111
[READ_COMMANDS.SIGN_IN_WITH_SUPPORT_AUTH_TOKEN]: Parameters.SignInWithSupportAuthTokenParams;

src/libs/SearchQueryUtils.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import NAVIGATORS from '@src/NAVIGATORS';
77
import ONYXKEYS from '@src/ONYXKEYS';
88
import SCREENS from '@src/SCREENS';
99
import type {SearchAdvancedFiltersForm} from '@src/types/form';
10-
import FILTER_KEYS, {DATE_FILTER_KEYS} from '@src/types/form/SearchAdvancedFiltersForm';
10+
import FILTER_KEYS, {ALLOWED_TYPE_FILTERS, DATE_FILTER_KEYS} from '@src/types/form/SearchAdvancedFiltersForm';
1111
import type * as OnyxTypes from '@src/types/onyx';
1212
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
1313
import type {CardFeedNamesWithType} from './CardFeedUtils';
@@ -352,6 +352,21 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
352352
const {type, status, policyID, groupBy, ...otherFilters} = filterValues;
353353
const filtersString: string[] = [];
354354

355+
// When switching types/setting the type, ensure we aren't polluting our query with filters that are
356+
// only available for the previous type. Remove all filters that are not allowed for the new type
357+
if (type) {
358+
const allowedFilters: string[] = ALLOWED_TYPE_FILTERS[type];
359+
const providedFilterKeys = Object.keys(otherFilters) as Array<keyof typeof otherFilters>;
360+
361+
providedFilterKeys.forEach((filter) => {
362+
if (allowedFilters.includes(filter)) {
363+
return;
364+
}
365+
366+
otherFilters[filter] = undefined;
367+
});
368+
}
369+
355370
filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY}:${CONST.SEARCH.TABLE_COLUMNS.DATE}`);
356371
filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_ORDER}:${CONST.SEARCH.SORT_ORDER.DESC}`);
357372

@@ -360,7 +375,9 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
360375
filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE}:${sanitizedType}`);
361376
}
362377

363-
if (groupBy) {
378+
// If the type is not an expense, then we need to remove remove grouping, because we cannot
379+
// group other data types
380+
if (groupBy && type === CONST.SEARCH.DATA_TYPES.EXPENSE) {
364381
const sanitizedGroupBy = sanitizeSearchValue(groupBy);
365382
filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY}:${sanitizedGroupBy}`);
366383
}

src/libs/SearchUIUtils.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,11 +1199,11 @@ function createTypeMenuSections(session: OnyxTypes.Session | undefined, policies
11991199
];
12001200

12011201
// Begin adding conditional sections, based on the policies the user has access to
1202-
const showSubmitSuggestion = Object.values(policies).filter((p) => p?.type && p.type !== CONST.POLICY.TYPE.PERSONAL).length > 0;
1202+
const showSubmitSuggestion = Object.values(policies).filter((p) => isPaidGroupPolicy(p)).length > 0;
12031203

12041204
const showApproveSuggestion =
12051205
Object.values(policies).filter<OnyxTypes.Policy>((policy): policy is OnyxTypes.Policy => {
1206-
if (!policy || !email || policy.type === CONST.POLICY.TYPE.PERSONAL) {
1206+
if (!policy || !email || !isPaidGroupPolicy(policy)) {
12071207
return false;
12081208
}
12091209

@@ -1215,18 +1215,26 @@ function createTypeMenuSections(session: OnyxTypes.Session | undefined, policies
12151215
return isPolicyApprover || isSubmittedTo;
12161216
}).length > 0;
12171217

1218-
// TODO: This option will be enabled soon (removing the && false). We are waiting on BE changes to support this
1219-
// feature, but lets keep the code here for simplicity
1220-
// https://github.com/Expensify/Expensify/issues/505932
12211218
const showPaySuggestion =
1222-
Object.values(policies).filter<OnyxTypes.Policy>(
1223-
(policy): policy is OnyxTypes.Policy =>
1224-
!!policy &&
1225-
policy.role === CONST.POLICY.ROLE.ADMIN &&
1226-
policy.type !== CONST.POLICY.TYPE.PERSONAL &&
1227-
(policy.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES ||
1228-
policy.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL),
1229-
).length > 0 && false;
1219+
Object.values(policies).filter<OnyxTypes.Policy>((policy): policy is OnyxTypes.Policy => {
1220+
if (!policy || !isPaidGroupPolicy(policy)) {
1221+
return false;
1222+
}
1223+
1224+
const reimburser = policy.reimburser;
1225+
const isReimburser = reimburser === email;
1226+
const isAdmin = policy.role === CONST.POLICY.ROLE.ADMIN;
1227+
1228+
if (policy.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES) {
1229+
return reimburser ? isReimburser : isAdmin;
1230+
}
1231+
1232+
if (policy.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL) {
1233+
return isAdmin;
1234+
}
1235+
1236+
return false;
1237+
}).length > 0;
12301238

12311239
// TODO: This option will be enabled soon (removing the && false). We are waiting on changes to support this
12321240
// feature fully, but lets keep the code here for simplicity

src/libs/actions/Policy/Policy.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,7 @@ function setWorkspacePayer(policyID: string, reimburserEmail: string) {
660660
onyxMethod: Onyx.METHOD.MERGE,
661661
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
662662
value: {
663+
reimburser: reimburserEmail,
663664
achAccount: {reimburser: reimburserEmail},
664665
errorFields: {reimburser: null},
665666
pendingFields: {reimburser: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
@@ -779,6 +780,7 @@ function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueO
779780
value: {
780781
reimbursementChoice,
781782
isLoadingWorkspaceReimbursement: true,
783+
reimburser: reimburserEmail,
782784
achAccount: {reimburser: reimburserEmail},
783785
errorFields: {reimbursementChoice: null},
784786
pendingFields: {reimbursementChoice: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},

src/libs/actions/Search.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ function openSearchFiltersCardPage() {
246246
API.read(READ_COMMANDS.OPEN_SEARCH_FILTERS_CARD_PAGE, null, {optimisticData, successData, failureData});
247247
}
248248

249+
function openSearchPage() {
250+
API.read(READ_COMMANDS.OPEN_SEARCH_PAGE, null);
251+
}
252+
249253
function search({queryJSON, offset}: {queryJSON: SearchQueryJSON; offset?: number}) {
250254
const {optimisticData, finallyData, failureData} = getOnyxLoadingData(queryJSON.hash, queryJSON);
251255
const {flatFilters, ...queryJSONWithoutFlatFilters} = queryJSON;
@@ -444,5 +448,6 @@ export {
444448
handleActionButtonPress,
445449
submitMoneyRequestOnSearch,
446450
openSearchFiltersCardPage,
451+
openSearchPage as openSearch,
447452
getLastPolicyPaymentMethod,
448453
};

src/libs/actions/connections/NetSuiteCommands.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,15 @@ function updateNetSuiteOnyxData<TSettingName extends keyof Connections['netsuite
7373
settingValue: Partial<Connections['netsuite']['options']['config'][TSettingName]>,
7474
oldSettingValue: Partial<Connections['netsuite']['options']['config'][TSettingName]>,
7575
) {
76+
const exporterOptimisticData = settingName === CONST.NETSUITE_CONFIG.EXPORTER ? {exporter: settingValue} : {};
77+
const exporterErrorData = settingName === CONST.NETSUITE_CONFIG.EXPORTER ? {exporter: oldSettingValue} : {};
78+
7679
const optimisticData: OnyxUpdate[] = [
7780
{
7881
onyxMethod: Onyx.METHOD.MERGE,
7982
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
8083
value: {
84+
...exporterOptimisticData,
8185
connections: {
8286
netsuite: {
8387
options: {
@@ -98,6 +102,7 @@ function updateNetSuiteOnyxData<TSettingName extends keyof Connections['netsuite
98102
onyxMethod: Onyx.METHOD.MERGE,
99103
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
100104
value: {
105+
...exporterErrorData,
101106
connections: {
102107
netsuite: {
103108
options: {

0 commit comments

Comments
 (0)