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
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8437,6 +8437,9 @@ const CONST = {
SPLIT_LIST_ITEM_EDIT_BUTTON: 'SplitListItem-EditButton',
LIST_HEADER_SELECT_ALL: 'SelectionList-ListHeader-SelectAll',
},
LIST_ITEM: {
INVITE_MEMBER_CHECKBOX: 'ListItem-InviteMemberCheckbox',
},
CONTEXT_MENU: {
REPLY_IN_THREAD: 'ContextMenu-ReplyInThread',
MARK_AS_UNREAD: 'ContextMenu-MarkAsUnread',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ function InviteMemberListItem<TItem extends ListItem>({
shiftVertical={variables.inviteMemberListItemTooltipShiftVertical}
shiftHorizontal={variables.inviteMemberListItemTooltipShiftHorizontal}
shouldHideOnNavigate
shouldHideOnScroll
wrapperStyle={styles.productTrainingTooltipWrapper}
uniqueID={`${sectionIndex}-${index}`}
>
Expand Down Expand Up @@ -160,6 +161,7 @@ function InviteMemberListItem<TItem extends ListItem>({
role={CONST.ROLE.BUTTON}
accessibilityLabel={item.text ?? ''}
style={[styles.ml2, styles.optionSelectCircle]}
sentryLabel={CONST.SENTRY_LABEL.LIST_ITEM.INVITE_MEMBER_CHECKBOX}
>
<SelectCircle
isChecked={item.isSelected ?? false}
Expand Down
2 changes: 2 additions & 0 deletions src/components/SelectionList/ListItem/ListItemRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function ListItemRenderer<TItem extends ListItem>({
isDisabled,
showTooltip,
canSelectMultiple,
canShowProductTrainingTooltip,
onLongPressRow,
shouldSingleExecuteRowSelect,
selectRow,
Expand Down Expand Up @@ -99,6 +100,7 @@ function ListItemRenderer<TItem extends ListItem>({
shouldSyncFocus={shouldSyncFocus}
wrapperStyle={wrapperStyle}
titleStyles={titleStyles}
canShowProductTrainingTooltip={canShowProductTrainingTooltip}
titleContainerStyles={titleContainerStyles}
errorRowStyles={errorRowStyles}
shouldUseDefaultRightHandSideCheckmark={shouldUseDefaultRightHandSideCheckmark}
Expand Down
5 changes: 4 additions & 1 deletion src/components/SelectionList/ListItem/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ type CommonListItemProps<TItem extends ListItem> = {

/** Whether to show the right caret icon */
shouldShowRightCaret?: boolean;

/** Whether product training tooltips can be displayed */
canShowProductTrainingTooltip?: boolean;
} & TRightHandSideComponent<TItem> &
WithSentryLabel;

Expand Down Expand Up @@ -310,7 +313,7 @@ type BaseListItemProps<TItem extends ListItem> = CommonListItemProps<TItem> & {
children?: ReactElement<ListItemProps<TItem>> | ((hovered: boolean) => ReactElement<ListItemProps<TItem>>);
shouldSyncFocus?: boolean;
hoverStyle?: StyleProp<ViewStyle>;
/** Errors that this user may contain */
/** Whether to show RBR */
shouldDisplayRBR?: boolean;
/** Test ID of the component. Used to locate this view in end-to-end tests. */
testID?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useKeyboardState from '@hooks/useKeyboardState';
import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useScrollEnabled from '@hooks/useScrollEnabled';
import useScrollEventEmitter from '@hooks/useScrollEventEmitter';
import useSingleExecution from '@hooks/useSingleExecution';
import {focusedItemRef} from '@hooks/useSyncFocus/useSyncFocusImplementation';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -44,9 +45,11 @@ function BaseSelectionListWithSections<TItem extends ListItem>({
onLayout,
onSelectRow,
onDismissError,
onScroll,
onScrollBeginDrag,
onEndReached,
onEndReachedThreshold,
customListHeaderContent,
customHeaderContent,
rightHandSideComponent,
listEmptyContent,
Expand All @@ -68,6 +71,7 @@ function BaseSelectionListWithSections<TItem extends ListItem>({
shouldScrollToFocusedIndex = true,
shouldSingleExecuteRowSelect = false,
shouldPreventDefaultFocusOnSelectRow = false,
canShowProductTrainingTooltip,
}: SelectionListWithSectionsProps<TItem>) {
const styles = useThemeStyles();
const isScreenFocused = useIsFocused();
Expand All @@ -80,6 +84,7 @@ function BaseSelectionListWithSections<TItem extends ListItem>({
const activeElementRole = useActiveElementRole();
const {isKeyboardShown} = useKeyboardState();
const {safeAreaPaddingBottomStyle} = useSafeAreaPaddings();
const triggerScrollEvent = useScrollEventEmitter();

const paddingBottomStyle = !isKeyboardShown && !footerContent && safeAreaPaddingBottomStyle;

Expand Down Expand Up @@ -319,6 +324,7 @@ function BaseSelectionListWithSections<TItem extends ListItem>({
rightHandSideComponent={rightHandSideComponent}
setFocusedIndex={setFocusedIndex}
singleExecution={singleExecution}
canShowProductTrainingTooltip={canShowProductTrainingTooltip}
shouldSyncFocus={!isTextInputFocusedRef.current && hasKeyBeenPressed.current}
shouldHighlightSelectedItem
shouldIgnoreFocus={shouldIgnoreFocus}
Expand All @@ -338,33 +344,36 @@ function BaseSelectionListWithSections<TItem extends ListItem>({
onLayout={onLayout}
>
{textInputComponent()}
{customHeaderContent}
{itemsCount === 0 && (showLoadingPlaceholder || showListEmptyContent) ? (
renderListEmptyContent()
) : (
<>
{customHeaderContent}
<FlashList
data={flattenedData}
renderItem={renderItem}
ref={listRef}
extraData={flattenedData.length}
getItemType={getItemType}
initialScrollIndex={initialScrollIndex ?? initialFocusedIndex}
keyExtractor={(item) => ('flatListKey' in item ? item.flatListKey : item.keyForList)}
onEndReached={onEndReached}
onEndReachedThreshold={onEndReachedThreshold}
onScrollBeginDrag={onScrollBeginDrag}
scrollEnabled={scrollEnabled}
indicatorStyle="white"
showsVerticalScrollIndicator
keyboardShouldPersistTaps="always"
ListFooterComponent={listFooterContent}
ListFooterComponentStyle={style?.listFooterContentStyle}
style={style?.listStyle}
contentContainerStyle={style?.contentContainerStyle}
maintainVisibleContentPosition={{disabled: true}}
/>
</>
<FlashList
data={flattenedData}
renderItem={renderItem}
ref={listRef}
extraData={flattenedData.length}
getItemType={getItemType}
initialScrollIndex={initialScrollIndex ?? initialFocusedIndex}
keyExtractor={(item) => ('flatListKey' in item ? item.flatListKey : item.keyForList)}
onEndReached={onEndReached}
onEndReachedThreshold={onEndReachedThreshold}
onScrollBeginDrag={onScrollBeginDrag}
scrollEnabled={scrollEnabled}
onScroll={() => {
onScroll?.();
triggerScrollEvent();
}}
indicatorStyle="white"
showsVerticalScrollIndicator
keyboardShouldPersistTaps="always"
ListHeaderComponent={customListHeaderContent}
ListFooterComponent={listFooterContent}
ListFooterComponentStyle={style?.listFooterContentStyle}
style={style?.listStyle}
contentContainerStyle={style?.contentContainerStyle}
maintainVisibleContentPosition={{disabled: true}}
/>
)}
{!!footerContent && (
<Footer<TItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ type SelectionListWithSectionsProps<TItem extends ListItem> = BaseSelectionListP

/** Callback to fire when the list layout changes */
onLayout?: (event: LayoutChangeEvent) => void;

/** Whether product training tooltips can be displayed */
canShowProductTrainingTooltip?: boolean;
};

type SelectionListWithSectionsHandle<TItem extends ListItem = ListItem> = {
Expand Down
6 changes: 3 additions & 3 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ type BaseSelectionListProps<TItem extends ListItem> = {
/** Whether tooltips should be shown */
shouldShowTooltips?: boolean;

/** Custom content to display in the header of list component. */
customListHeaderContent?: React.JSX.Element | null;

/** Called when a checkbox is pressed */
onCheckboxPress?: (item: TItem) => void;

Expand Down Expand Up @@ -129,9 +132,6 @@ type SelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> &
/** Custom header content to render instead of the default select all header */
customListHeader?: React.ReactNode;

/** Custom content to display in the header of list component. */
customListHeaderContent?: React.JSX.Element | null;

/** Custom component to render while data is loading */
customLoadingPlaceholder?: React.JSX.Element;

Expand Down
1 change: 0 additions & 1 deletion src/pages/iou/request/MoneyRequestAttendeeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,6 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde
listEmptyContent={<EmptySelectionListContent contentType={iouType} />}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
onEndReached={onListEndReached}
disableMaintainingScrollPosition
shouldSingleExecuteRowSelect
shouldShowTextInput
canSelectMultiple
Expand Down
102 changes: 58 additions & 44 deletions src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import FormHelpMessage from '@components/FormHelpMessage';
import MenuItem from '@components/MenuItem';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
import ReferralProgramCTA from '@components/ReferralProgramCTA';
// eslint-disable-next-line no-restricted-imports
import SelectionList from '@components/SelectionListWithSections';
import InviteMemberListItem from '@components/SelectionListWithSections/InviteMemberListItem';
import type {SelectionListHandle} from '@components/SelectionListWithSections/types';
import InviteMemberListItem from '@components/SelectionList/ListItem/InviteMemberListItem';
import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections';
import type {Section, SelectionListWithSectionsHandle} from '@components/SelectionList/SelectionListWithSections/types';
import useContactImport from '@hooks/useContactImport';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners';
Expand All @@ -34,8 +33,9 @@ import getPlatform from '@libs/getPlatform';
import goToSettings from '@libs/goToSettings';
import {isMovingTransactionFromTrackExpense} from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {Option, Section} from '@libs/OptionsListUtils';
import type {Option} from '@libs/OptionsListUtils';
import {formatSectionsFromSearchTerm, getHeaderMessage, getParticipantsOption, getPersonalDetailSearchTerms, getPolicyExpenseReportOption, isCurrentUser} from '@libs/OptionsListUtils';
import type {OptionWithKey} from '@libs/OptionsListUtils/types';
import {getActiveAdminWorkspaces, isPaidGroupPolicy as isPaidGroupPolicyUtil} from '@libs/PolicyUtils';
import type {OptionData} from '@libs/ReportUtils';
import {isInvoiceRoom} from '@libs/ReportUtils';
Expand Down Expand Up @@ -127,7 +127,7 @@ function MoneyRequestParticipantsSelector({
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);

const [textInputAutoFocus, setTextInputAutoFocus] = useState<boolean>(!isNative);
const selectionListRef = useRef<SelectionListHandle | null>(null);
const selectionListRef = useRef<SelectionListWithSectionsHandle | null>(null);
const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '';

const isPaidGroupPolicy = useMemo(() => isPaidGroupPolicyUtil(policy), [policy]);
Expand Down Expand Up @@ -279,7 +279,7 @@ function MoneyRequestParticipantsSelector({
* @returns {Array}
*/
const [sections, header] = useMemo(() => {
const newSections: Section[] = [];
const newSections: Array<Section<OptionWithKey>> = [];
if (!areOptionsInitialized || !didScreenTransitionEnd) {
return [newSections, ''];
}
Expand All @@ -297,34 +297,42 @@ function MoneyRequestParticipantsSelector({
);
// Just a temporary fix to satisfy the type checker
// Will be fixed when migrating to use new SelectionListWithSections
newSections.push({...formatResults.section, title: undefined, shouldShow: true});
newSections.push({...formatResults.section, sectionIndex: 0});

newSections.push({
title: translate('workspace.common.workspace'),
data: availableOptions.workspaceChats ?? [],
shouldShow: (availableOptions.workspaceChats ?? []).length > 0,
});

newSections.push({
title: translate('workspace.invoices.paymentMethods.personal'),
data: availableOptions.selfDMChat ? [availableOptions.selfDMChat] : [],
shouldShow: !!availableOptions.selfDMChat,
});

if (!isWorkspacesOnly) {
if ((availableOptions.workspaceChats ?? []).length > 0) {
newSections.push({
title: translate('common.recents'),
data: isPerDiemRequest ? availableOptions.recentReports.filter((report) => report.isPolicyExpenseChat) : availableOptions.recentReports,
shouldShow: (isPerDiemRequest ? availableOptions.recentReports.filter((report) => report.isPolicyExpenseChat) : availableOptions.recentReports).length > 0,
title: translate('workspace.common.workspace'),
data: availableOptions.workspaceChats ?? [],
sectionIndex: 1,
});
}

if (availableOptions.selfDMChat) {
newSections.push({
title: translate('common.contacts'),
data: availableOptions.personalDetails,
shouldShow: availableOptions.personalDetails.length > 0 && !isPerDiemRequest,
title: translate('workspace.invoices.paymentMethods.personal'),
data: availableOptions.selfDMChat ? [availableOptions.selfDMChat] : [],
sectionIndex: 2,
});
}

if (!isWorkspacesOnly) {
if ((isPerDiemRequest ? availableOptions.recentReports.filter((report) => report.isPolicyExpenseChat) : availableOptions.recentReports).length > 0) {
Comment on lines +318 to +319
Copy link
Contributor

Choose a reason for hiding this comment

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

@sharabai why did this if check change?

Copy link
Contributor Author

@sharabai sharabai Feb 25, 2026

Choose a reason for hiding this comment

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

@grgia the if check is same. It was moved to from inside the object from prop shouldShow to an outside beforehand if check.
Look at this line It's hard to spot it in github diff.

newSections.push({
title: translate('common.recents'),
data: isPerDiemRequest ? availableOptions.recentReports.filter((report) => report.isPolicyExpenseChat) : availableOptions.recentReports,
sectionIndex: 3,
});
}

if (availableOptions.personalDetails.length > 0 && !isPerDiemRequest) {
newSections.push({
title: translate('common.contacts'),
data: availableOptions.personalDetails,
sectionIndex: 4,
});
}
}

if (
!isWorkspacesOnly &&
availableOptions.userToInvite &&
Expand All @@ -347,11 +355,11 @@ function MoneyRequestParticipantsSelector({
? getPolicyExpenseReportOption(participant, currentUserAccountID, personalDetails, reportAttributesDerived)
: getParticipantsOption(participant, personalDetails);
}),
shouldShow: true,
sectionIndex: 5,
});
}

let headerMessage = '';
let headerMessage;
if (!showImportContacts) {
headerMessage = inputHelperText;
}
Expand Down Expand Up @@ -430,9 +438,9 @@ function MoneyRequestParticipantsSelector({
return length;
}, [areOptionsInitialized, sections]);

const shouldShowListEmptyContent = useMemo(() => optionLength === 0 && !showLoadingPlaceholder, [optionLength, showLoadingPlaceholder]);
const showListEmptyContent = useMemo(() => optionLength === 0 && !showLoadingPlaceholder, [optionLength, showLoadingPlaceholder]);

const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE && !shouldShowListEmptyContent;
const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE && !showListEmptyContent;

const initiateContactImportAndSetState = useCallback(() => {
setContactPermissionState(RESULTS.GRANTED);
Expand Down Expand Up @@ -561,6 +569,15 @@ function MoneyRequestParticipantsSelector({
},
}));

const textInputOptions = {
value: searchTerm,
label: translate('selectionList.nameEmailOrPhoneNumber'),
hint: offlineMessage,
onChangeText: setSearchTerm,
disableAutoFocus: !textInputAutoFocus,
headerMessage: header,
};

return (
<>
<ContactPermissionModal
Expand All @@ -570,19 +587,19 @@ function MoneyRequestParticipantsSelector({
setTextInputAutoFocus(true);
}}
/>
<SelectionList
onConfirm={handleConfirmSelection}
sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY}
<SelectionListWithSections
confirmButtonOptions={{
onConfirm: handleConfirmSelection,
}}
sections={sections}
ListItem={InviteMemberListItem}
textInputValue={searchTerm}
textInputLabel={translate('selectionList.nameEmailOrPhoneNumber')}
textInputHint={offlineMessage}
onChangeText={setSearchTerm}
textInputOptions={textInputOptions}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
onSelectRow={onSelectRow}
shouldSingleExecuteRowSelect
canShowProductTrainingTooltip={canShowManagerMcTest}
headerContent={
customListHeaderContent={importContactsButtonComponent}
customHeaderContent={

Choose a reason for hiding this comment

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

P2 Badge Keep import-contacts prompt visible for empty searches

Switching this prop to customHeaderContent makes the ImportContactButton render only in the non-empty branch of BaseSelectionListWithSections (src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx), unlike the previous headerContent behavior. On native, when contacts permission is denied and a non-matching query is entered, showImportContacts suppresses the text-input header message and the list is empty, so the import-contacts guidance disappears entirely in that no-results state.

Useful? React with 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

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

did you test this case @sharabai

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@grgia yep, the comment was useful and I did change the code.

<ImportContactButton
showImportContacts={contactState?.showImportUI ?? showImportContacts}
inputHelperText={inputHelperText}
Expand All @@ -591,14 +608,11 @@ function MoneyRequestParticipantsSelector({
}
footerContent={footerContent}
listEmptyContent={EmptySelectionListContentWithPermission}
listHeaderContent={importContactsButtonComponent}
showSectionTitleWithListHeaderContent
headerMessage={header}
showLoadingPlaceholder={showLoadingPlaceholder}
shouldShowTextInput
canSelectMultiple={isIOUSplit && isAllowedToSplit}
isLoadingNewOptions={!!isSearchingForReports}
shouldShowListEmptyContent={shouldShowListEmptyContent}
textInputAutoFocus={textInputAutoFocus}
showListEmptyContent={showListEmptyContent}
ref={selectionListRef}
onEndReached={onListEndReached}
/>
Expand Down
Loading