Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
fd9f06c
Stop scrolling to top when selecting an item
ChavdaSachin Jul 3, 2025
91decc1
Merge remote-tracking branch 'upstream/main' into fix-61414/stop-scro…
ChavdaSachin Jul 23, 2025
e521fc0
combine selected and non selected workspaces in a single section
ChavdaSachin Jul 23, 2025
1bf1112
combine selected and non selected items in a single section
ChavdaSachin Jul 23, 2025
6e48532
Merge main
ChavdaSachin Aug 11, 2025
6fda289
unify selected and non-selected options
ChavdaSachin Aug 11, 2025
e0477ff
unify selected and non-selected options
ChavdaSachin Aug 11, 2025
51cc4eb
unify selected and non-selected options
ChavdaSachin Aug 11, 2025
284a695
prettier
ChavdaSachin Aug 12, 2025
0ee3e16
unify selected and non-selected options
ChavdaSachin Aug 12, 2025
c69c856
unify selected and non-selected options
ChavdaSachin Aug 12, 2025
cd6e0d7
unify selected and non-selected options
ChavdaSachin Aug 12, 2025
c85ee14
prettier
ChavdaSachin Aug 12, 2025
e2394b6
remove scroll to top test
ChavdaSachin Aug 12, 2025
e25b9c7
remove scroll to top test
ChavdaSachin Aug 12, 2025
76fb58e
lint
ChavdaSachin Aug 12, 2025
4adf1a6
prettier
ChavdaSachin Aug 12, 2025
58b1e5e
stop separating selected and non selected options
ChavdaSachin Aug 19, 2025
f07cd20
stop separating selected and non selected options
ChavdaSachin Aug 20, 2025
3a40935
stop separating selected and non selected options
ChavdaSachin Aug 20, 2025
a71c0ac
stop separating selected and non selected options
ChavdaSachin Aug 20, 2025
b8eeb6c
stop separating selected and non selected options
ChavdaSachin Aug 20, 2025
c0e3024
lint
ChavdaSachin Aug 20, 2025
b5e49e4
merge main
ChavdaSachin Aug 20, 2025
ab1e6ae
lint
ChavdaSachin Aug 20, 2025
df92e4e
fix card feed test
ChavdaSachin Aug 20, 2025
5acd83f
prettier
ChavdaSachin Aug 20, 2025
11fd58a
fix card feed test
ChavdaSachin Aug 20, 2025
5a90f95
fix card feed test
ChavdaSachin Aug 20, 2025
5d25a51
do not animate for pending scroll ref
ChavdaSachin Aug 27, 2025
c0bf6c2
avoid clearing focus index for initial render
ChavdaSachin Aug 27, 2025
0b6de55
add initiallyFocusedOptionKey
ChavdaSachin Aug 27, 2025
2bcf843
add initiallyFocusedOptionKey
ChavdaSachin Aug 27, 2025
f6c2e9b
add initiallyFocusedOptionKey
ChavdaSachin Aug 27, 2025
50135d5
add new helper method getFirstSelectedItem
ChavdaSachin Aug 27, 2025
58d0aba
add initiallyFocusedOptionKey
ChavdaSachin Aug 27, 2025
3dac380
stop separating selected and non selected options
ChavdaSachin Aug 27, 2025
0cb26bd
stop separating selected and non selected options
ChavdaSachin Aug 27, 2025
0048c67
add initiallyFocusedOptionKey
ChavdaSachin Aug 27, 2025
638d154
stop separating selected and non selected options
ChavdaSachin Aug 27, 2025
30bfa4a
stop separating selected and non selected options
ChavdaSachin Aug 27, 2025
b583ed6
add initiallyFocusedOptionKey
ChavdaSachin Aug 27, 2025
21b29c4
prettier
ChavdaSachin Aug 27, 2025
9556171
lint/prettier
ChavdaSachin Aug 28, 2025
6731088
lint/prettier
ChavdaSachin Aug 28, 2025
9090550
prettier
ChavdaSachin Aug 28, 2025
a30f7c7
lint
ChavdaSachin Aug 28, 2025
38c4e56
add initiallyFocusedOptionKey
ChavdaSachin Aug 28, 2025
11a6c0f
Stop scrolling to top when selecting an item
ChavdaSachin Aug 28, 2025
0976439
Optimization + pass initialNumToRender to prevent flicker after list …
ChavdaSachin Aug 28, 2025
d58363b
Merge remote-tracking branch 'upstream/main' into fix-61414/stop-scro…
ChavdaSachin Aug 28, 2025
c9d5291
Merge remote-tracking branch 'upstream/main' into fix-61414/stop-scro…
ChavdaSachin Aug 29, 2025
c561111
Merge Main
ChavdaSachin Sep 1, 2025
e62e394
Remove flaky check and lint
ChavdaSachin Sep 1, 2025
703528e
undo unwanted changes
ChavdaSachin Sep 2, 2025
d804b21
Prevent animation for initial scroll
ChavdaSachin Sep 2, 2025
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
60 changes: 23 additions & 37 deletions src/components/Search/SearchFiltersChatsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import {createOptionFromReport, filterAndOrderOptions, formatSectionsFromSearchTerm, getAlternateText, getSearchOptions} from '@libs/OptionsListUtils';
import {createOptionFromReport, filterAndOrderOptions, getAlternateText, getFirstSelectedItem, getSearchOptions} from '@libs/OptionsListUtils';
import type {Option, Section} from '@libs/OptionsListUtils';
import type {OptionData} from '@libs/ReportUtils';
import Navigation from '@navigation/Navigation';
import variables from '@styles/variables';
import {searchInServer} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -67,61 +68,44 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen
return getSearchOptions(options, undefined, false);
}, [areOptionsInitialized, isScreenTransitionEnd, options]);

const defaultOptionsModified = useMemo(() => {
return {
...defaultOptions,
recentReports: defaultOptions.recentReports.map((item) => (selectedReportIDs.includes(item.reportID) ? {...item, isSelected: true} : item)),
};
}, [defaultOptions, selectedReportIDs]);

const chatOptions = useMemo(() => {
return filterAndOrderOptions(defaultOptions, cleanSearchTerm, {
return filterAndOrderOptions(defaultOptionsModified, cleanSearchTerm, {
selectedOptions,
excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT,
});
}, [defaultOptions, cleanSearchTerm, selectedOptions]);
}, [cleanSearchTerm, selectedOptions, defaultOptionsModified]);

const {sections, headerMessage} = useMemo(() => {
const {sections, headerMessage, firstKeyForList} = useMemo(() => {
const newSections: Section[] = [];
let firstKey = '';
if (!areOptionsInitialized) {
return {sections: [], headerMessage: undefined};
return {sections: [], headerMessage: undefined, firstKeyForList: firstKey};
}

const formattedResults = formatSectionsFromSearchTerm(
cleanSearchTerm,
selectedOptions,
chatOptions.recentReports,
chatOptions.personalDetails,
personalDetails,
false,
undefined,
reportAttributesDerived,
);

newSections.push(formattedResults.section);

const visibleReportsWhenSearchTermNonEmpty = chatOptions.recentReports.map((report) => (selectedReportIDs.includes(report.reportID) ? getSelectedOptionData(report) : report));
const visibleReportsWhenSearchTermEmpty = chatOptions.recentReports.filter((report) => !selectedReportIDs.includes(report.reportID));
const reportsFiltered = cleanSearchTerm === '' ? visibleReportsWhenSearchTermEmpty : visibleReportsWhenSearchTermNonEmpty;

newSections.push({
title: undefined,
data: reportsFiltered,
data: chatOptions.recentReports,
shouldShow: chatOptions.recentReports.length > 0,
});

const areResultsFound = didScreenTransitionEnd && formattedResults.section.data.length === 0 && reportsFiltered.length === 0;
if (!firstKey) {
firstKey = getFirstSelectedItem(chatOptions.recentReports);
}
const areResultsFound = didScreenTransitionEnd && chatOptions.recentReports.length === 0;
const message = areResultsFound ? translate('common.noResultsFound') : undefined;

return {
sections: newSections,
headerMessage: message,
firstKeyForList: firstKey,
};
}, [
areOptionsInitialized,
chatOptions.personalDetails,
chatOptions.recentReports,
cleanSearchTerm,
didScreenTransitionEnd,
personalDetails,
reportAttributesDerived,
selectedOptions,
selectedReportIDs,
translate,
]);
}, [areOptionsInitialized, chatOptions.recentReports, didScreenTransitionEnd, translate]);

useEffect(() => {
searchInServer(debouncedSearchTerm.trim());
Expand Down Expand Up @@ -180,6 +164,8 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen
onSelectRow={handleParticipantSelection}
isLoadingNewOptions={isLoadingNewOptions}
showLoadingPlaceholder={showLoadingPlaceholder}
initiallyFocusedOptionKey={firstKeyForList}
getItemHeight={() => variables.optionRowHeight}
/>
);
}
Expand Down
87 changes: 27 additions & 60 deletions src/components/Search/SearchFiltersParticipantsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import useOnyx from '@hooks/useOnyx';
import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import memoize from '@libs/memoize';
import {filterAndOrderOptions, filterSelectedOptions, formatSectionsFromSearchTerm, getValidOptions} from '@libs/OptionsListUtils';
import {filterAndOrderOptions, getFirstSelectedItem, getValidOptions} from '@libs/OptionsListUtils';
import type {Option, Section} from '@libs/OptionsListUtils';
import type {OptionData} from '@libs/ReportUtils';
import {getDisplayNameForParticipant} from '@libs/ReportUtils';
import Navigation from '@navigation/Navigation';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
Expand Down Expand Up @@ -46,7 +46,6 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}:
shouldInitialize: didScreenTransitionEnd,
});
const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {canBeMissing: false, initWithStoredValues: false});
const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: (val) => val?.reports});
const [selectedOptions, setSelectedOptions] = useState<OptionData[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const cleanSearchTerm = useMemo(() => searchTerm.trim().toLowerCase(), [searchTerm]);
Expand All @@ -64,95 +63,65 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}:
{
excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT,
includeCurrentUser: true,
includeSelectedOptions: true,
},
);
}, [areOptionsInitialized, options.personalDetails, options.reports]);

const unselectedOptions = useMemo(() => {
return filterSelectedOptions(defaultOptions, new Set(selectedOptions.map((option) => option.accountID)));
}, [defaultOptions, selectedOptions]);
const selectedAccountIDsSet = useMemo(() => new Set(selectedOptions.map(({accountID}) => accountID)), [selectedOptions]);
const defaultOptionsModified = useMemo(() => {
return {
...defaultOptions,
recentReports: defaultOptions.recentReports.map((item) => (selectedAccountIDsSet.has(item.accountID) ? {...item, isSelected: true} : item)),
personalDetails: defaultOptions.personalDetails.map((item) => (selectedAccountIDsSet.has(item.accountID) ? {...item, isSelected: true} : item)),
};
}, [defaultOptions, selectedAccountIDsSet]);

const chatOptions = useMemo(() => {
const filteredOptions = filterAndOrderOptions(unselectedOptions, cleanSearchTerm, {
const filteredOptions = filterAndOrderOptions(defaultOptionsModified, cleanSearchTerm, {
selectedOptions,
excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT,
maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
canInviteUser: false,
});

const {currentUserOption} = unselectedOptions;

// Ensure current user is not in personalDetails when they should be excluded
if (currentUserOption) {
filteredOptions.personalDetails = filteredOptions.personalDetails.filter((detail) => detail.accountID !== currentUserOption.accountID);
}

return filteredOptions;
}, [unselectedOptions, cleanSearchTerm, selectedOptions]);
}, [defaultOptionsModified, cleanSearchTerm, selectedOptions]);

const {sections, headerMessage} = useMemo(() => {
const {sections, headerMessage, firstKeyForList} = useMemo(() => {
const newSections: Section[] = [];
if (!areOptionsInitialized) {
return {sections: [], headerMessage: undefined};
}

const formattedResults = formatSectionsFromSearchTerm(
cleanSearchTerm,
selectedOptions,
chatOptions.recentReports,
chatOptions.personalDetails,
personalDetails,
true,
undefined,
reportAttributesDerived,
);

const selectedCurrentUser = formattedResults.section.data.find((option) => option.accountID === chatOptions.currentUserOption?.accountID);
let firstKey = '';

// If the current user is already selected, remove them from the recent reports and personal details
if (selectedCurrentUser) {
chatOptions.recentReports = chatOptions.recentReports.filter((report) => report.accountID !== selectedCurrentUser.accountID);
chatOptions.personalDetails = chatOptions.personalDetails.filter((detail) => detail.accountID !== selectedCurrentUser.accountID);
}

// If the current user is not selected, add them to the top of the list
if (!selectedCurrentUser && chatOptions.currentUserOption) {
const formattedName = getDisplayNameForParticipant({
accountID: chatOptions.currentUserOption.accountID,
shouldAddCurrentUserPostfix: true,
personalDetailsData: personalDetails,
});
chatOptions.currentUserOption.text = formattedName;

newSections.push({
title: '',
data: [chatOptions.currentUserOption],
shouldShow: true,
});
if (!areOptionsInitialized) {
return {sections: [], headerMessage: undefined, firstKeyForList: firstKey};
}

newSections.push(formattedResults.section);

newSections.push({
title: '',
data: chatOptions.recentReports,
shouldShow: chatOptions.recentReports.length > 0,
});
if (!firstKey) {
firstKey = getFirstSelectedItem(chatOptions.recentReports);
}

newSections.push({
title: '',
data: chatOptions.personalDetails,
shouldShow: chatOptions.personalDetails.length > 0,
});
if (!firstKey) {
firstKey = getFirstSelectedItem(chatOptions.personalDetails);
}

const noResultsFound = chatOptions.personalDetails.length === 0 && chatOptions.recentReports.length === 0 && !chatOptions.currentUserOption;
const message = noResultsFound ? translate('common.noResultsFound') : undefined;

return {
sections: newSections,
headerMessage: message,
firstKeyForList: firstKey,
};
}, [areOptionsInitialized, cleanSearchTerm, selectedOptions, chatOptions, personalDetails, reportAttributesDerived, translate]);
}, [areOptionsInitialized, chatOptions, translate]);

const resetChanges = useCallback(() => {
setSelectedOptions([]);
Expand Down Expand Up @@ -195,10 +164,6 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}:
return true;
}

if (selectedOption.reportID && selectedOption.reportID === option?.reportID) {
return true;
}

return false;
});

Expand Down Expand Up @@ -242,6 +207,8 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}:
onSelectRow={handleParticipantSelection}
isLoadingNewOptions={isLoadingNewOptions}
showLoadingPlaceholder={showLoadingPlaceholder}
initiallyFocusedOptionKey={firstKeyForList}
getItemHeight={() => variables.optionRowHeightCompact}
/>
);
}
Expand Down
35 changes: 12 additions & 23 deletions src/components/Search/SearchMultipleSelectionPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import MultiSelectListItem from '@components/SelectionList/MultiSelectListItem';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import {getFirstSelectedItem} from '@libs/OptionsListUtils';
import type {OptionData} from '@libs/ReportUtils';
import {sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils';
import variables from '@styles/variables';
import ROUTES from '@src/ROUTES';
import SearchFilterPageFooterButtons from './SearchFilterPageFooterButtons';

Expand Down Expand Up @@ -33,47 +35,32 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit
}, [initiallySelectedItems]);

const {sections, noResultsFound} = useMemo(() => {
const selectedItemsSection = selectedItems
.filter((item) => item?.name.toLowerCase().includes(debouncedSearchTerm?.toLowerCase()))
const itemsSection = items
.filter((item) => item?.name?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase()))
.sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare))
.map((item) => ({
text: item.name,
keyForList: item.name,
isSelected: true,
isSelected: selectedItems.some((selectedItem) => selectedItem.value === item.value),
value: item.value,
}));
const remainingItemsSection = items
.filter(
(item) =>
!selectedItems.some((selectedItem) => selectedItem.value.toString() === item.value.toString()) && item?.name?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase()),
)
.sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare))
.map((item) => ({
text: item.name,
keyForList: item.name,
isSelected: false,
value: item.value,
}));
const isEmpty = !selectedItemsSection.length && !remainingItemsSection.length;
const isEmpty = !itemsSection.length;
return {
sections: isEmpty
? []
: [
{
title: undefined,
data: selectedItemsSection,
shouldShow: selectedItemsSection.length > 0,
},
{
title: pickerTitle,
data: remainingItemsSection,
shouldShow: remainingItemsSection.length > 0,
data: itemsSection,
shouldShow: itemsSection.length > 0,
},
],
noResultsFound: isEmpty,
};
}, [selectedItems, items, pickerTitle, debouncedSearchTerm, localeCompare]);

const firstKey = getFirstSelectedItem(sections?.at(0)?.data);

const onSelectItem = useCallback(
(item: Partial<OptionData & SearchMultipleSelectionPickerItem>) => {
if (!item.text || !item.keyForList || !item.value) {
Expand Down Expand Up @@ -120,6 +107,8 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit
shouldShowTooltips
canSelectMultiple
ListItem={MultiSelectListItem}
initiallyFocusedOptionKey={firstKey}
getItemHeight={() => variables.optionRowHeightCompact}
/>
);
}
Expand Down
Loading
Loading