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
239 changes: 37 additions & 202 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,14 @@
} from '@components/SelectionListWithSections/types';
import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
import {useWideRHPActions} from '@components/WideRHPContextProvider';
import useActionLoadingReportIDs from '@hooks/useActionLoadingReportIDs';
import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet';
import useCardFeedsForDisplay from '@hooks/useCardFeedsForDisplay';
import useConfirmModal from '@hooks/useConfirmModal';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useMultipleSnapshots from '@hooks/useMultipleSnapshots';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSearchData from '@hooks/useSearchData';
import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll';
import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -43,15 +39,12 @@
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
import {isSplitAction} from '@libs/ReportSecondaryActionUtils';
import {canEditFieldOfMoneyRequest, canHoldUnholdReportAction, canRejectReportAction, isOneTransactionReport, selectFilteredReportActions} from '@libs/ReportUtils';
import {canEditFieldOfMoneyRequest, canHoldUnholdReportAction, canRejectReportAction, isOneTransactionReport} from '@libs/ReportUtils';
import {buildCannedSearchQuery, buildSearchQueryString} from '@libs/SearchQueryUtils';
import {
createAndOpenSearchTransactionThread,
getColumnsToShow,
getListItem,
getSections,
getSortedSections,
getSuggestedSearches,
getWideAmountIndicators,
isGroupedItemArray,
isReportActionListItemType,
Expand All @@ -75,8 +68,7 @@
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import {columnsSelector} from '@src/selectors/AdvancedSearchFiltersForm';
import type {OutstandingReportsByPolicyIDDerivedValue, SaveSearch, Transaction} from '@src/types/onyx';
import type {OutstandingReportsByPolicyIDDerivedValue, Transaction} from '@src/types/onyx';
import type SearchResults from '@src/types/onyx/SearchResults';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import arraysEqual from '@src/utils/arraysEqual';
Expand Down Expand Up @@ -241,49 +233,51 @@
} = useSearchActionsContext();
const [offset, setOffset] = useState(0);

const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
const previousTransactions = usePrevious(transactions);
const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID);
const [violations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const {accountID, email, login} = useCurrentUserPersonalDetails();
const isActionLoadingSet = useActionLoadingReportIDs();
const [allReportMetadata] = useOnyx(ONYXKEYS.COLLECTION.REPORT_METADATA);
const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: columnsSelector});
const [customCardNames] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES);
const [cardList] = useOnyx(ONYXKEYS.CARD_LIST);

const isExpenseReportType = type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
const {markReportIDAsMultiTransactionExpense, unmarkReportIDAsMultiTransactionExpense} = useWideRHPActions();

const archivedReportsIdSet = useArchivedReportsIdSet();

const [exportReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {
canEvict: false,
// There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded
// we also need to check that the searchResults matches the type and status of the current search
const isDataLoaded = shouldUseLiveData || isSearchDataLoaded(searchResults, queryJSON);

selector: selectFilteredReportActions,
const {
sections: filteredData,
allDataLength,
filteredDataLength,
columns: currentColumns,
customCardNames,
violations,
searchKey,
savedSearchName,
transactions,
reportActions,
outstandingReportsByPolicyID,
introSelected,
accountID,
login,
searchDataType,
hasErrors,
shouldShowLoadingState,
shouldShowLoadingMoreItems,
hasLoadedAllTransactions,
} = useSearchData({
queryJSON,
searchResults,
isDataLoaded,
shouldUseLiveData,
searchRequestResponseStatusCode,
});

const [cardFeeds, cardFeedsResult] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER);
const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);

const {defaultCardFeed} = useCardFeedsForDisplay();
const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]);
const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.recentSearchHash === recentSearchHash)?.key, [suggestedSearches, recentSearchHash]);
const searchDataType = useMemo(() => (shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [shouldUseLiveData, searchResults?.search?.type]);
const {email} = useCurrentUserPersonalDetails();
const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, hash, offset === 0);

Check failure on line 272 in src/components/Search/index.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Argument of type 'string | undefined' is not assignable to parameter of type 'SearchKey | undefined'.

const previousTransactions = usePrevious(transactions);
const previousReportActions = usePrevious(reportActions);
const {translate, localeCompare, formatPhoneNumber} = useLocalize();
const {translate, localeCompare} = useLocalize();
const searchListRef = useRef<SelectionListHandle | null>(null);

const spanExistedOnMount = useRef(!!getSpan(CONST.TELEMETRY.SPAN_NAVIGATE_AFTER_EXPENSE_CREATE));

const savedSearchSelector = useCallback((searches: OnyxEntry<SaveSearch>) => searches?.[hash], [hash]);
const [savedSearch] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {selector: savedSearchSelector});

const handleDEWModalOpen = useCallback(() => {
if (onDEWModalOpen) {
onDEWModalOpen();
Expand All @@ -304,7 +298,7 @@

const clearTransactionsAndSetHashAndKey = useCallback(() => {
clearSelectedTransactions(hash);
setCurrentSearchHashAndKey(hash, recentSearchHash, searchKey);

Check failure on line 301 in src/components/Search/index.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Argument of type 'string | undefined' is not assignable to parameter of type 'SearchKey | undefined'.
setCurrentSearchQueryJSON(queryJSON);
}, [hash, recentSearchHash, searchKey, clearSelectedTransactions, setCurrentSearchHashAndKey, setCurrentSearchQueryJSON, queryJSON]);

Expand All @@ -321,9 +315,6 @@
const prevValidGroupBy = usePrevious(validGroupBy);
const isSearchResultsEmpty = !searchResults?.data || isSearchResultsEmptyUtil(searchResults, validGroupBy);

// When grouping by card, we need cardFeeds to display feed names
const isCardFeedsLoading = validGroupBy === CONST.SEARCH.GROUP_BY.CARD && cardFeedsResult?.status === 'loading';

useEffect(() => {
if (prevValidGroupBy === validGroupBy) {
return;
Expand Down Expand Up @@ -381,7 +372,7 @@
transactions,
previousTransactions,
queryJSON,
searchKey,

Check failure on line 375 in src/components/Search/index.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Type 'string | undefined' is not assignable to type 'SearchKey | undefined'.
offset,
shouldCalculateTotals,
reportActions,
Expand All @@ -389,152 +380,10 @@
shouldUseLiveData,
});

// There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded
// we also need to check that the searchResults matches the type and status of the current search
const isDataLoaded = shouldUseLiveData || isSearchDataLoaded(searchResults, queryJSON);

const hasErrors = Object.keys(searchResults?.errors ?? {}).length > 0 && !isOffline;

// For to-do searches, we never show loading state since the data is always available locally from Onyx
const shouldShowLoadingState =
!shouldUseLiveData &&
!isOffline &&
(!isDataLoaded ||
(!!searchResults?.search.isLoading && Array.isArray(searchResults?.data) && searchResults?.data.length === 0) ||
(hasErrors && searchRequestResponseStatusCode === null) ||
isCardFeedsLoading);
const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0;
const prevIsSearchResultEmpty = usePrevious(isSearchResultsEmpty);

const [baseFilteredData, filteredDataLength, allDataLength] = useMemo(() => {
if (searchResults === undefined || !isDataLoaded) {
return [[], 0, 0];
}

// Group-by option cannot be used for chats or tasks
const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT;
const isTask = type === CONST.SEARCH.DATA_TYPES.TASK;
if (validGroupBy && (isChat || isTask)) {
return [[], 0, 0];
}

const [filteredData1, allLength] = getSections({
type,
data: searchResults.data,
policies,
currentAccountID: accountID,
currentUserEmail: email ?? '',
translate,
formatPhoneNumber,
bankAccountList,
groupBy: validGroupBy,
reportActions: exportReportActions,
currentSearch: searchKey,
archivedReportsIDList: archivedReportsIdSet,
queryJSON,
isActionLoadingSet,
cardFeeds,
isOffline,
allTransactionViolations: violations,
customCardNames,
allReportMetadata,
cardList,
});
return [filteredData1, filteredData1.length, allLength];
}, [
searchKey,
isOffline,
exportReportActions,
validGroupBy,
isDataLoaded,
searchResults,
type,
archivedReportsIdSet,
translate,
formatPhoneNumber,
accountID,
queryJSON,
email,
isActionLoadingSet,
cardFeeds,
policies,
bankAccountList,
violations,
customCardNames,
allReportMetadata,
cardList,
]);

// For group-by views, each grouped item has a transactionsQueryJSON with a hash pointing to a separate snapshot
// containing its individual transactions. We collect these hashes and fetch their snapshots to enrich the grouped items.
const groupByTransactionHashes = useMemo(() => {
if (!validGroupBy) {
return [];
}
return (baseFilteredData as TransactionGroupListItemType[])
.map((item) => (item.transactionsQueryJSON?.hash ? String(item.transactionsQueryJSON.hash) : undefined))
.filter((hashValue): hashValue is string => !!hashValue);
}, [validGroupBy, baseFilteredData]);

const groupByTransactionSnapshots = useMultipleSnapshots(groupByTransactionHashes);

const filteredData = useMemo(() => {
if (!validGroupBy || isExpenseReportType) {
return baseFilteredData;
}

const enriched = (baseFilteredData as TransactionGroupListItemType[]).map((item) => {
const snapshot = item.transactionsQueryJSON?.hash ? groupByTransactionSnapshots[String(item.transactionsQueryJSON.hash)] : undefined;
if (!snapshot?.data) {
return item;
}

const [transactions1] = getSections({
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
data: snapshot.data,
currentAccountID: accountID,
currentUserEmail: email ?? '',
bankAccountList,
translate,
formatPhoneNumber,
isActionLoadingSet,
cardFeeds,
allReportMetadata,
cardList,
});
return {...item, transactions: transactions1 as TransactionListItemType[]};
});

return enriched;
}, [
validGroupBy,
isExpenseReportType,
baseFilteredData,
groupByTransactionSnapshots,
accountID,
email,
translate,
formatPhoneNumber,
isActionLoadingSet,
cardFeeds,
bankAccountList,
allReportMetadata,
cardList,
]);

const hasLoadedAllTransactions = useMemo(() => {
if (!validGroupBy) {
return true;
}
// For group-by views, check if all transactions in groups have been loaded
return (baseFilteredData as TransactionGroupListItemType[]).every((item) => {
const snapshot = item.transactionsQueryJSON?.hash || item.transactionsQueryJSON?.hash === 0 ? groupByTransactionSnapshots[String(item.transactionsQueryJSON.hash)] : undefined;
// If snapshot doesn't exist, the group hasn't been expanded yet (transactions not loaded)
// If snapshot exists and has hasMoreResults: true, not all transactions are loaded
return !!snapshot && !snapshot?.search?.hasMoreResults;
});
}, [validGroupBy, baseFilteredData, groupByTransactionSnapshots]);

useEffect(() => {
/** We only want to display the skeleton for the status filters the first time we load them for a specific data type */
setShouldShowFiltersBarLoading(shouldShowLoadingState && lastSearchType !== type);
Expand All @@ -558,7 +407,7 @@
return;
}

handleSearch({queryJSON, searchKey, offset, shouldCalculateTotals, prevReportsLength: filteredDataLength, isLoading: !!searchResults?.search?.isLoading});

Check failure on line 410 in src/components/Search/index.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Type 'string | undefined' is not assignable to type 'SearchKey | undefined'.

// We don't need to run the effect on change of isFocused.
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -577,7 +426,7 @@
}

shouldRetrySearchWithTotalsOrGroupedRef.current = false;
handleSearch({queryJSON, searchKey, offset, shouldCalculateTotals: true, prevReportsLength: filteredDataLength, isLoading: false});

Check failure on line 429 in src/components/Search/index.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Type 'string | undefined' is not assignable to type 'SearchKey | undefined'.
}, [filteredDataLength, handleSearch, offset, queryJSON, searchKey, searchResults?.search?.count, searchResults?.search?.isLoading, shouldCalculateTotals, validGroupBy]);

// When new data load, selectedTransactions is updated in next effect. We use this flag to whether selection is updated
Expand Down Expand Up @@ -934,7 +783,7 @@
}

if (isTransactionGroupListItemType(item) && !isTransactionReportGroupListItemType(item) && item.transactionsQueryJSON) {
handleSearch({queryJSON: item.transactionsQueryJSON, searchKey, offset: 0, shouldCalculateTotals: false, isLoading: false});

Check failure on line 786 in src/components/Search/index.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Type 'string | undefined' is not assignable to type 'SearchKey | undefined'.
return;
}

Expand Down Expand Up @@ -1015,13 +864,6 @@
],
);

const currentColumns = useMemo(() => {
if (!searchResults?.data) {
return [];
}
return getColumnsToShow(accountID, searchResults?.data, visibleColumns, false, searchDataType, validGroupBy);
}, [accountID, searchResults?.data, searchDataType, visibleColumns, validGroupBy]);

const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.get(),
Expand All @@ -1033,7 +875,7 @@
// If columns have changed, trigger an animation before settings columnsToShow to prevent
// new columns appearing before the fade out animation happens
useEffect(() => {
if ((previousColumns && currentColumns && arraysEqual(previousColumns, currentColumns)) || offset === 0 || isSmallScreenWidth) {
if (offset === 0 || isSmallScreenWidth || (previousColumns && currentColumns && arraysEqual(previousColumns, currentColumns))) {
setColumnsToShow(currentColumns);
return;
}
Expand Down Expand Up @@ -1280,14 +1122,7 @@

if (shouldShowChartView && isGroupedItemArray(sortedData)) {
cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_AFTER_EXPENSE_CREATE);
let chartTitle = translate(`search.chartTitles.${validGroupBy}`);
if (savedSearch) {
if (savedSearch.name !== savedSearch.query) {
chartTitle = savedSearch.name;
}
} else if (searchKey && suggestedSearches[searchKey]) {
chartTitle = translate(suggestedSearches[searchKey].translationPath);
}
const chartTitle = savedSearchName ?? translate(`search.chartTitles.${validGroupBy}`);

return (
<SearchScopeProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import getBase62ReportID from '@libs/getBase62ReportID';
import {getMoneyRequestSpendBreakdown} from '@libs/ReportUtils';
import {resolveExportedIconNames} from '@libs/SearchFormatUtils';
import {isScanning as isTransactionScanning} from '@libs/TransactionUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -197,7 +198,7 @@ function ExpenseReportListItemRow({
),
[CONST.SEARCH.TABLE_COLUMNS.EXPORTED_TO]: (
<View style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.EXPORTED_TO)]}>
<ExportedIconCell reportActions={reportActions} />
<ExportedIconCell exportedIconNames={resolveExportedIconNames(reportActions ?? [])} />
</View>
),
[CONST.SEARCH.TABLE_COLUMNS.ACTION]: (
Expand Down
Loading
Loading