Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/components/Search/FilterComponents/InSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function InSelector({value = [], onChange}: InSelectorProps) {
policyCollection: allPolicies,
sortedActions,
conciergeReportID,
});
}).options;

const chatOptions = filterAndOrderOptions(defaultOptions, cleanSearchTerm, countryCode, loginList, currentUserEmail, currentUserAccountID, personalDetails, {
selectedOptions,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Search/SearchAutocompleteList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ function SearchAutocompleteList({
personalDetails,
sortedActions,
conciergeReportID,
});
}).options;
}, [
listOptions,
draftComments,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Search/SearchFiltersChatsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen
policyCollection: allPolicies,
sortedActions,
conciergeReportID,
});
}).options;

const chatOptions = filterAndOrderOptions(defaultOptions, cleanSearchTerm, countryCode, loginList, currentUserEmail, currentUserAccountID, personalDetails, {
selectedOptions,
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useAutocompleteSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ function useAutocompleteSuggestions({
personalDetails,
sortedActions,
conciergeReportID,
}).personalDetails.filter((participant) => participant.text && !alreadyAutocompletedKeys.has(participant.text.toLowerCase()));
}).options.personalDetails.filter((participant) => participant.text && !alreadyAutocompletedKeys.has(participant.text.toLowerCase()));

return participants.map((participant) => ({
filterKey: autocompleteKey,
Expand Down Expand Up @@ -277,7 +277,7 @@ function useAutocompleteSuggestions({
personalDetails,
sortedActions,
conciergeReportID,
}).recentReports.filter((chat) => {
}).options.recentReports.filter((chat) => {
if (!chat.text) {
return false;
}
Expand Down
14 changes: 9 additions & 5 deletions src/hooks/useSearchSelector/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,14 +211,10 @@ function useSearchSelectorBase({
const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: passthroughPolicyTagListSelector});
const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);

const onListEndReached = useDebounce(() => {
setMaxResults((previous) => previous + maxResultsPerPage);
}, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME);

const computedSearchTerm = getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode);
const trimmedSearchInput = debouncedSearchTerm.trim();

const baseOptions = (() => {
const {options: baseOptions, hasMore} = (() => {
if (!areOptionsInitialized) {
return getEmptyOptions();
}
Expand Down Expand Up @@ -340,6 +336,14 @@ function useSearchSelectorBase({
}
})();

const onListEndReached = useDebounce(() => {
if (!areOptionsInitialized || !hasMore) {
return;
}

setMaxResults((previous) => previous + maxResultsPerPage);
}, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME);

const isOptionSelected = (option: OptionData) => selectedOptions.some((selected) => doOptionsMatch(selected, option));

const searchOptions = {
Expand Down
62 changes: 40 additions & 22 deletions src/libs/OptionsListUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ import type {
Option,
OptionList,
Options,
OptionsResult,
OrderOptionsConfig,
OrderReportOptionsConfig,
PayeePersonalDetails,
Expand Down Expand Up @@ -1605,7 +1606,7 @@ function createFilteredOptionList(

// Step 2: Sort by lastVisibleActionCreated (most recent first) and limit to top N
// In search mode, skip sorting because we return all reports anyway - sorting is unnecessary
const sortedReports = isSearching ? reportsArray : optionsOrderBy(reportsArray, (report) => reportSortComparator(report, privateIsArchivedMap), maxRecentReports);
const sortedReports = isSearching ? reportsArray : optionsOrderBy(reportsArray, (report) => reportSortComparator(report, privateIsArchivedMap), maxRecentReports).options;

// Step 3: If search term is present, build report map with ONLY 1:1 DM reports
// This allows personal details to have valid 1:1 DM reportIDs for proper avatar display
Expand Down Expand Up @@ -1749,19 +1750,21 @@ function optionsOrderBy<T = SearchOptionData | PersonalDetailOptionData>(
limit?: number,
filter?: (option: T) => boolean | undefined,
reversed = false,
): T[] {
): {options: T[]; hasMore: boolean} {
const heap = reversed ? new MaxHeap<T>(comparator) : new MinHeap<T>(comparator);
let hasMore = false;

// If a limit is 0 or negative, return an empty array
if (limit !== undefined && limit <= 0) {
return [];
return {options: [], hasMore};
}

for (const option of options) {
if (filter && !filter(option)) {
continue;
}
if (limit !== undefined && heap.size() >= limit) {
hasMore = true;
const peekedValue = heap.peek();
if (!peekedValue) {
throw new Error('Heap is empty, cannot peek value');
Expand All @@ -1774,7 +1777,7 @@ function optionsOrderBy<T = SearchOptionData | PersonalDetailOptionData>(
heap.push(option);
}
}
return [...heap].reverse();
return {options: [...heap].reverse(), hasMore};
}

/**
Expand All @@ -1791,17 +1794,18 @@ function optionsOrderAndGroupBy<T = SearchOptionData>(
limit?: number,
filter?: (option: T) => boolean | undefined,
reversed = false,
): T[][] {
): {options: T[][]; hasMore: boolean} {
// Create a heap for each separator + one default heap (N+1 total)
const heaps: Array<MinHeap<T> | MaxHeap<T>> = [];
let hasMore = false;
for (let i = 0; i < separators.length; i++) {
heaps.push(reversed ? new MaxHeap<T>(comparator) : new MinHeap<T>(comparator));
}
const defaultHeap = reversed ? new MaxHeap<T>(comparator) : new MinHeap<T>(comparator);

// If limit is 0 or negative, return N+1 empty arrays
if (limit !== undefined && limit <= 0) {
return Array(separators.length + 1).map(() => []);
return {options: Array(separators.length + 1).map(() => []), hasMore};
}

// Process each option
Expand Down Expand Up @@ -1829,6 +1833,7 @@ function optionsOrderAndGroupBy<T = SearchOptionData>(

// Add to heap with limit logic (each heap has its own limit)
if (limit !== undefined && targetHeap.size() >= limit) {
hasMore = true;
const peekedValue = targetHeap.peek();
if (!peekedValue) {
throw new Error('Heap is empty, cannot peek value');
Expand All @@ -1850,7 +1855,7 @@ function optionsOrderAndGroupBy<T = SearchOptionData>(
}
results.push([...defaultHeap].reverse());

return results;
return {options: results, hasMore};
}

/**
Expand Down Expand Up @@ -2378,7 +2383,7 @@ function getValidOptions(
sortedActions,
...config
}: GetOptionsConfig = {},
): Options {
): OptionsResult {
// Gather shared configs:
// Hard exclusions: cannot be selected at all
const loginsToExclude: Record<string, boolean> = {
Expand Down Expand Up @@ -2416,6 +2421,7 @@ function getValidOptions(
let recentReportOptions: Array<SearchOption<Report>> = [];
let workspaceChats: Array<SearchOption<Report>> = [];
let selfDMChat: SearchOptionData | undefined;
let hasMore = false;

const searchTerms = processSearchString(searchString);
if (includeRecentReports) {
Expand Down Expand Up @@ -2487,7 +2493,10 @@ function getValidOptions(
};

let selfDMChats: Array<SearchOption<Report>>;
[selfDMChats, workspaceChats, recentReportOptions] = optionsOrderAndGroupBy([isSelfDMChat, isWorkspaceChat], options.reports, recentReportComparator, maxElements, filteringFunction);
const groupedOptions = optionsOrderAndGroupBy([isSelfDMChat, isWorkspaceChat], options.reports, recentReportComparator, maxElements, filteringFunction);
[selfDMChats, workspaceChats, recentReportOptions] = groupedOptions.options;

hasMore = hasMore || groupedOptions.hasMore;

if (selfDMChats.length > 0) {
selfDMChat = prepareReportOptionsForDisplay(
Expand Down Expand Up @@ -2618,7 +2627,10 @@ function getValidOptions(
const maxPersonalDetailsElements = maxElements
? Math.max(maxElements - recentReportOptions.length - workspaceChats.length - (!selfDMChat ? 1 : 0), MIN_PERSONAL_DETAILS_SLOTS)
: undefined;
personalDetailsOptions = optionsOrderBy(options.personalDetails, personalDetailsComparator, maxPersonalDetailsElements, filteringFunction, true);
const groupedPersonalDetails = optionsOrderBy(options.personalDetails, personalDetailsComparator, maxPersonalDetailsElements, filteringFunction, true);
personalDetailsOptions = groupedPersonalDetails.options;

hasMore = hasMore || groupedPersonalDetails.hasMore;

for (let i = 0; i < personalDetailsOptions.length; i++) {
const personalDetail = personalDetailsOptions.at(i);
Expand Down Expand Up @@ -2653,12 +2665,15 @@ function getValidOptions(
}

return {
personalDetails: personalDetailsOptions,
recentReports: recentReportOptions,
currentUserOption: currentUserRef.current,
userToInvite,
workspaceChats,
selfDMChat,
options: {
personalDetails: personalDetailsOptions,
recentReports: recentReportOptions,
currentUserOption: currentUserRef.current,
userToInvite,
workspaceChats,
selfDMChat,
},
hasMore,
};
}

Expand Down Expand Up @@ -2715,7 +2730,7 @@ function getSearchOptions({
allPolicyTags,
sortedActions,
conciergeReportID,
}: SearchOptionsConfig): Options {
}: SearchOptionsConfig): OptionsResult {
const optionList = getValidOptions(options, policyCollection, draftComments, loginList, currentUserAccountID, currentUserEmail, conciergeReportID, {
betas,
includeRecentReports,
Expand Down Expand Up @@ -3267,12 +3282,15 @@ function sortAlphabetically<T extends Partial<Record<TKey, string | undefined>>,
return items.sort((a, b) => localeCompare(a[key]?.toLowerCase() ?? '', b[key]?.toLowerCase() ?? ''));
}

function getEmptyOptions(): Options {
function getEmptyOptions(): OptionsResult {
return {
recentReports: [],
personalDetails: [],
userToInvite: null,
currentUserOption: null,
options: {
recentReports: [],
personalDetails: [],
userToInvite: null,
currentUserOption: null,
},
hasMore: false,
};
}

Expand Down
6 changes: 6 additions & 0 deletions src/libs/OptionsListUtils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,11 @@ type OrderReportOptionsConfig = {

type ReportAndPersonalDetailOptions = Pick<Options, 'recentReports' | 'personalDetails' | 'workspaceChats'>;

type OptionsResult = {
options: Options;
hasMore?: boolean;
};

export type {
FilterUserToInviteConfig,
GetOptionsConfig,
Expand All @@ -317,4 +322,5 @@ export type {
SelectionListSections,
SectionForSearchTerm,
IsValidReportsConfig,
OptionsResult,
};
6 changes: 3 additions & 3 deletions src/libs/PersonalDetailOptionsListUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ function getValidOptions(
return matchesSearchTerms(personalDetail, searchTerms);
};

const selectedOptions = optionsOrderBy(extendedOptions, personalDetailsComparator, maxElements, selectedFilteringFunction, true);
const selectedOptions = optionsOrderBy(extendedOptions, personalDetailsComparator, maxElements, selectedFilteringFunction, true).options;
// If we're including selected options from the search results, we only want to exclude them if the search input is empty
// This is because on certain pages, we show the selected options at the top when the search input is empty
// This prevents the issue of seeing the selected option twice if you have them as a recent chat and select them
Expand Down Expand Up @@ -369,7 +369,7 @@ function getValidOptions(
return true;
};

recentOptions = optionsOrderBy(options, recentReportComparator, recentMaxElements, filteringFunction);
recentOptions = optionsOrderBy(options, recentReportComparator, recentMaxElements, filteringFunction).options;
}

// Get valid personal details and check if we can find the current user:
Expand Down Expand Up @@ -397,7 +397,7 @@ function getValidOptions(
return matchesSearchTerms(personalDetail, searchTerms);
};

personalDetailsOptions = optionsOrderBy(options, personalDetailsComparator, maxElements, filteringFunction, true);
personalDetailsOptions = optionsOrderBy(options, personalDetailsComparator, maxElements, filteringFunction, true).options;

let userToInvite: OptionData | null = null;
if (includeUserToInvite) {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/NewChatPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function useOptions(reportAttributesDerived: ReportAttributesDerivedValue['repor
const personalDetails = listOptions?.personalDetails ?? [];
useGroupChatDraftParticipantSync(personalDetails, !isLoading, allPersonalDetails, loginList, currentUserEmail, currentUserAccountID, selectedOptions, setSelectedOptions);

const defaultOptions = getValidOptions(
const {options: defaultOptions} = getValidOptions(
{
reports,
personalDetails: personalDetails.concat(contacts),
Expand Down
4 changes: 2 additions & 2 deletions src/pages/Share/ShareTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,12 @@ function ShareTab({ref}: ShareTabProps) {
personalDetails,
sortedActions,
conciergeReportID,
})
}).options
: defaultListOptions;

let recentReportsOptions: OptionData[];
if (textInputValue.trim() === '') {
recentReportsOptions = optionsOrderBy(searchOptions.recentReports, recentReportComparator, 20);
recentReportsOptions = optionsOrderBy(searchOptions.recentReports, recentReportComparator, 20).options;
} else {
const orderedOptions = combineOrderingOfReportsAndPersonalDetails(searchOptions, textInputValue, {
sortByReportTypeInSearch: true,
Expand Down
4 changes: 2 additions & 2 deletions tests/perf-test/OptionsListUtils.perf-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ describe('OptionsListUtils', () => {
/* Testing getFilteredOptions */
test('[OptionsListUtils] getFilteredOptions with search value', async () => {
await waitForBatchedUpdates();
const formattedOptions = getValidOptions(
const {options: formattedOptions} = getValidOptions(
{reports: options.reports, personalDetails: options.personalDetails},
allPolicies,
{},
Expand All @@ -163,7 +163,7 @@ describe('OptionsListUtils', () => {
});
test('[OptionsListUtils] getFilteredOptions with empty search value', async () => {
await waitForBatchedUpdates();
const formattedOptions = getValidOptions(
const {options: formattedOptions} = getValidOptions(
{reports: options.reports, personalDetails: options.personalDetails},
allPolicies,
{},
Expand Down
20 changes: 10 additions & 10 deletions tests/ui/AssignCardFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ describe('AssignCardFeed', () => {
searchTerm: '',
debouncedSearchTerm: '',
setSearchTerm: jest.fn(),
searchOptions: getEmptyOptions(),
availableOptions: getEmptyOptions(),
searchOptions: getEmptyOptions().options,
availableOptions: getEmptyOptions().options,
selectedOptions: [],
selectedOptionsForDisplay: [],
setSelectedOptions: jest.fn(),
Expand Down Expand Up @@ -261,8 +261,8 @@ describe('AssignCardFeed', () => {
searchTerm: '',
debouncedSearchTerm: '',
setSearchTerm: jest.fn(),
searchOptions: getEmptyOptions(),
availableOptions: getEmptyOptions(),
searchOptions: getEmptyOptions().options,
availableOptions: getEmptyOptions().options,
selectedOptions: [],
selectedOptionsForDisplay: [],
setSelectedOptions: jest.fn(),
Expand Down Expand Up @@ -314,8 +314,8 @@ describe('AssignCardFeed', () => {
searchTerm: '',
debouncedSearchTerm: '',
setSearchTerm: jest.fn(),
searchOptions: getEmptyOptions(),
availableOptions: getEmptyOptions(),
searchOptions: getEmptyOptions().options,
availableOptions: getEmptyOptions().options,
selectedOptions: [],
selectedOptionsForDisplay: [],
setSelectedOptions: jest.fn(),
Expand Down Expand Up @@ -589,8 +589,8 @@ describe('AssignCardFeed', () => {
searchTerm: '',
debouncedSearchTerm: '',
setSearchTerm: jest.fn(),
searchOptions: getEmptyOptions(),
availableOptions: getEmptyOptions(),
searchOptions: getEmptyOptions().options,
availableOptions: getEmptyOptions().options,
selectedOptions: [],
selectedOptionsForDisplay: [],
setSelectedOptions: jest.fn(),
Expand Down Expand Up @@ -644,8 +644,8 @@ describe('AssignCardFeed', () => {
searchTerm: '',
debouncedSearchTerm: '',
setSearchTerm: jest.fn(),
searchOptions: getEmptyOptions(),
availableOptions: getEmptyOptions(),
searchOptions: getEmptyOptions().options,
availableOptions: getEmptyOptions().options,
selectedOptions: [],
selectedOptionsForDisplay: [],
setSelectedOptions: jest.fn(),
Expand Down
Loading
Loading