Skip to content
Merged
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
67 changes: 28 additions & 39 deletions src/components/ApproverSelectionList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, {useMemo} from 'react';
import type {SectionListData} from 'react-native';
import useDebouncedState from '@hooks/useDebouncedState';
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -13,15 +12,14 @@ import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy} from '@src/types/onyx';
import type {Icon} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import BlockingView from './BlockingViews/BlockingView';
import FullPageNotFoundView from './BlockingViews/FullPageNotFoundView';
import HeaderWithBackButton from './HeaderWithBackButton';
import ScreenWrapper from './ScreenWrapper';
import SelectionList from './SelectionListWithSections';
import InviteMemberListItem from './SelectionListWithSections/InviteMemberListItem';
import type {Section} from './SelectionListWithSections/types';
import SelectionList from './SelectionList';
import InviteMemberListItem from './SelectionList/ListItem/InviteMemberListItem';
import type {ListItem} from './SelectionList/types';

type ApproverSelectionListPageProps = {
testID: string;
Expand All @@ -42,21 +40,13 @@ type ApproverSelectionListPageProps = {
onSelectApprover?: (approvers: SelectionListApprover[]) => void;
shouldShowLoadingPlaceholder?: boolean;
shouldEnableHeaderMaxHeight?: boolean;
shouldUpdateFocusedIndex?: boolean;
};

type SelectionListApprover = {
text: string;
alternateText: string;
keyForList: string;
isSelected: boolean;
login: string;
rightElement?: React.ReactNode;
icons: Icon[];
type SelectionListApprover = ListItem & {
value?: number;
};

type ApproverSection = SectionListData<SelectionListApprover, Section<SelectionListApprover>>;

function ApproverSelectionList({
testID,
headerTitle,
Expand All @@ -76,6 +66,7 @@ function ApproverSelectionList({
onSelectApprover,
shouldShowLoadingPlaceholder,
shouldEnableHeaderMaxHeight,
shouldUpdateFocusedIndex = true,
}: ApproverSelectionListPageProps) {
const styles = useThemeStyles();
const {translate, localeCompare} = useLocalize();
Expand All @@ -89,23 +80,16 @@ function ApproverSelectionList({
// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy) || shouldShowNotFoundViewProp;

const sections: ApproverSection[] = useMemo(() => {
const data = useMemo(() => {
const filteredApprovers =
debouncedSearchTerm !== ''
? tokenizedSearch(allApprovers, getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode), (option) => [option.text ?? '', option.login ?? ''])
: allApprovers;

const data = sortAlphabetically(filteredApprovers, 'text', localeCompare);
return [
{
title: undefined,
data,
shouldShow: true,
},
];
return sortAlphabetically(filteredApprovers, 'text', localeCompare);
}, [allApprovers, debouncedSearchTerm, countryCode, localeCompare]);

const shouldShowListEmptyContent = !debouncedSearchTerm && !sections.at(0)?.data.length && shouldShowListEmptyContentProp;
const shouldShowListEmptyContent = !debouncedSearchTerm && !data.length && shouldShowListEmptyContentProp;

const toggleApprover = (member: SelectionListApprover) => {
const isAlreadySelected = selectedMembers.some((selectedOption) => selectedOption.login === member.login);
Expand All @@ -122,8 +106,6 @@ function ApproverSelectionList({
}
};

const headerMessage = useMemo(() => (searchTerm && !sections.at(0)?.data?.length ? translate('common.noResultsFound') : ''), [searchTerm, sections, translate]);

const listEmptyContent = useMemo(
() => (
<BlockingView
Expand All @@ -140,6 +122,16 @@ function ApproverSelectionList({
[translate, listEmptyContentSubtitle, styles.textSupporting, styles.pb10, lazyIllustrations.TurtleInShell],
);

const textInputOptions = useMemo(
() => ({
label: shouldShowListEmptyContent ? undefined : translate('selectionList.findMember'),
value: searchTerm,
onChangeText: setSearchTerm,
headerMessage: searchTerm && !data?.length ? translate('common.noResultsFound') : '',
}),
[shouldShowListEmptyContent, translate, searchTerm, setSearchTerm, data?.length],
);

return (
<ScreenWrapper
testID={testID}
Expand All @@ -160,24 +152,21 @@ function ApproverSelectionList({
/>
{subtitle}
<SelectionList
canSelectMultiple={allowMultipleSelection}
sections={sections}
ListItem={InviteMemberListItem}
textInputLabel={shouldShowListEmptyContent ? undefined : translate('selectionList.findMember')}
textInputValue={searchTerm}
onChangeText={setSearchTerm}
headerMessage={headerMessage}
data={data}
onSelectRow={toggleApprover}
showScrollIndicator
ListItem={InviteMemberListItem}
textInputOptions={textInputOptions}
canSelectMultiple={allowMultipleSelection}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
listEmptyContent={listEmptyContent}
shouldShowListEmptyContent={shouldShowListEmptyContent}
initiallyFocusedOptionKey={initiallyFocusedOptionKey}
shouldUpdateFocusedIndex
showListEmptyContent={shouldShowListEmptyContent}
initiallyFocusedItemKey={initiallyFocusedOptionKey}
shouldShowTextInput={shouldShowTextInput}
addBottomSafeAreaPadding
showLoadingPlaceholder={shouldShowLoadingPlaceholder}
footerContent={footerContent}
addBottomSafeAreaPadding
shouldUpdateFocusedIndex={shouldUpdateFocusedIndex}
showScrollIndicator
/>
</FullPageNotFoundView>
</ScreenWrapper>
Expand Down
10 changes: 10 additions & 0 deletions src/components/SelectionList/BaseSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,16 @@ function BaseSelectionList<TItem extends ListItem>({
[data.length, scrollToIndex, setFocusedIndex],
);

const selectedItemIndex = useMemo(() => (initiallyFocusedItemKey ? data.findIndex(isItemSelected) : -1), [data, initiallyFocusedItemKey, isItemSelected]);

useEffect(() => {
if (selectedItemIndex === -1 || selectedItemIndex === focusedIndex || textInputOptions?.value) {
return;
}
setFocusedIndex(selectedItemIndex);
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [selectedItemIndex]);

const prevSearchValue = usePrevious(textInputOptions?.value);
const prevSelectedOptionsLength = usePrevious(dataDetails.selectedOptions.length);
const prevAllOptionsLength = usePrevious(data.length);
Expand Down
12 changes: 7 additions & 5 deletions src/pages/ReportAddApproverPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, {useCallback, useMemo, useState} from 'react';
import ApproverSelectionList from '@components/ApproverSelectionList';
import type {SelectionListApprover} from '@components/ApproverSelectionList';
import Badge from '@components/Badge';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import {FallbackAvatar} from '@components/Icon/Expensicons';
import Text from '@components/Text';
import type {SelectionListApprover} from '@components/WorkspaceMembersSelectionList';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
Expand Down Expand Up @@ -39,6 +39,8 @@ function ReportAddApproverPage({report, isLoadingReportData, policy}: ReportAddA
const {isBetaEnabled} = usePermissions();
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar'] as const);

const currentUserDetails = useCurrentUserPersonalDetails();
const hasViolations = hasViolationsReportUtils(report?.reportID, transactionViolations, currentUserDetails.accountID, currentUserDetails.login ?? '');

Expand Down Expand Up @@ -74,12 +76,12 @@ function ReportAddApproverPage({report, isLoadingReportData, policy}: ReportAddA
isSelected: selectedApproverEmail === email,
login: email,
value: accountID,
icons: [{source: avatar ?? FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}],
icons: [{source: avatar ?? icons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}],
rightElement: isAdmin ? <Badge text={translate('common.admin')} /> : undefined,
};
})
.filter((approver): approver is SelectionListApprover => !!approver);
}, [employeeList, report, policy, personalDetails, selectedApproverEmail, translate, formatPhoneNumber]);
}, [employeeList, report, policy, personalDetails, formatPhoneNumber, selectedApproverEmail, icons.FallbackAvatar, translate]);

const addApprover = useCallback(() => {
const employeeAccountID = allApprovers.find((approver) => approver.login === selectedApproverEmail)?.value;
Expand Down Expand Up @@ -113,7 +115,7 @@ function ReportAddApproverPage({report, isLoadingReportData, policy}: ReportAddA
}, [addApprover, selectedApproverEmail, styles.flexBasisAuto, styles.flexGrow0, styles.flexReset, styles.flexShrink0, translate]);

const toggleApprover = useCallback((approvers: SelectionListApprover[]) => {
setSelectedApproverEmail(approvers.length ? approvers.at(0)?.login : undefined);
setSelectedApproverEmail(approvers.at(0)?.login ?? undefined);
}, []);

// eslint-disable-next-line rulesdir/no-negated-variables
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {useNavigationState} from '@react-navigation/native';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import React, {useCallback, useMemo} from 'react';
import type {SelectionListApprover} from '@components/ApproverSelectionList';
import ApproverSelectionList from '@components/ApproverSelectionList';
import {FallbackAvatar} from '@components/Icon/Expensicons';
import Text from '@components/Text';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -29,29 +29,21 @@ type WorkspaceWorkflowsApprovalsApproverPageProps = WithPolicyAndFullscreenLoadi
function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsApproverPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar'] as const);
const [approvalWorkflow, approvalWorkflowMetadata] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW, {canBeMissing: true});
const isApprovalWorkflowLoading = isLoadingOnyxValue(approvalWorkflowMetadata);
const [currentApprovalWorkflow] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW, {canBeMissing: true});
const [personalDetailsByEmail] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
canBeMissing: true,
selector: personalDetailsByEmailSelector,
});
const [selectedApproverEmail, setSelectedApproverEmail] = useState<string | undefined>(undefined);

const approverIndex = Number(route.params.approverIndex) ?? 0;
const isInitialCreationFlow = approvalWorkflow?.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE && !route.params.backTo;
const defaultApprover = getDefaultApprover(policy);
const firstApprover = approvalWorkflow?.approvers?.[0]?.email ?? '';
const rhpRoutes = useNavigationState((state) => state.routes);

useEffect(() => {
const currentApprover = approvalWorkflow?.approvers[approverIndex];
if (!currentApprover) {
return;
}

setSelectedApproverEmail(currentApprover.email);
}, [approvalWorkflow?.approvers, approverIndex]);
const currentApprover = approvalWorkflow?.approvers[approverIndex];
const selectedApproverEmail = currentApprover?.email;

const employeeList = policy?.employeeList;
const approversFromWorkflow = approvalWorkflow?.approvers;
Expand Down Expand Up @@ -95,7 +87,7 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa
keyForList: email,
isSelected: selectedApproverEmail === email,
login: email,
icons: [{source: avatar ?? FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}],
icons: [{source: avatar ?? icons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}],
rightElement: (
<MemberRightIcon
role={employee.role}
Expand All @@ -109,15 +101,16 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa
}, [
isApprovalWorkflowLoading,
employeeList,
isDefault,
policy?.preventSelfApproval,
policy?.owner,
membersEmail,
approversFromWorkflow,
selectedApproverEmail,
isDefault,
approverIndex,
defaultApprover,
personalDetails,
icons.FallbackAvatar,
]);

const shouldShowListEmptyContent = !!approvalWorkflow && !isApprovalWorkflowLoading;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react';
import type {SelectionListApprover} from '@components/ApproverSelectionList';
import ApproverSelectionList from '@components/ApproverSelectionList';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import {FallbackAvatar} from '@components/Icon/Expensicons';
import Text from '@components/Text';
import useDeepCompareRef from '@hooks/useDeepCompareRef';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -34,6 +34,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat
const {translate} = useLocalize();
const [approvalWorkflow, approvalWorkflowResults] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW, {canBeMissing: true});
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false});
const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar'] as const);

const isLoadingApprovalWorkflow = isLoadingOnyxValue(approvalWorkflowResults);
const [selectedMembers, setSelectedMembers] = useState<SelectionListApprover[]>([]);
Expand Down Expand Up @@ -63,7 +64,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat
keyForList: member.email,
isSelected: true,
login: member.email,
icons: [{source: member.avatar ?? FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: Str.removeSMSDomain(member.displayName), id: accountID}],
icons: [{source: member.avatar ?? icons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: Str.removeSMSDomain(member.displayName), id: accountID}],
rightElement: (
<MemberRightIcon
role={policy?.employeeList?.[member.email]?.role}
Expand All @@ -74,7 +75,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat
};
}),
);
}, [approvalWorkflow?.members, policy?.employeeList, policy?.owner, personalDetailLogins, translate]);
}, [approvalWorkflow?.members, policy?.employeeList, policy?.owner, personalDetailLogins, translate, icons.FallbackAvatar]);

const approversEmail = useMemo(() => approvalWorkflow?.approvers.map((member) => member?.email), [approvalWorkflow?.approvers]);
const allApprovers = useMemo(() => {
Expand All @@ -95,7 +96,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat
keyForList: member.email,
isSelected: false,
login: member.email,
icons: [{source: member.avatar ?? FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: Str.removeSMSDomain(member.displayName), id: accountID}],
icons: [{source: member.avatar ?? icons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: Str.removeSMSDomain(member.displayName), id: accountID}],
rightElement: (
<MemberRightIcon
role={policy?.employeeList?.[member.email]?.role}
Expand All @@ -112,7 +113,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat
members.push(...availableMembers);

return members;
}, [selectedMembers, approvalWorkflow?.availableMembers, policy?.employeeList, policy?.owner, policy?.preventSelfApproval, personalDetailLogins, approversEmail]);
}, [selectedMembers, approvalWorkflow?.availableMembers, policy?.employeeList, policy?.owner, policy?.preventSelfApproval, personalDetailLogins, icons.FallbackAvatar, approversEmail]);

const goBack = useCallback(() => {
let backTo;
Expand All @@ -125,7 +126,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat
}, [isInitialCreationFlow, route.params.policyID, firstApprover, approvalWorkflow?.action]);

const nextStep = useCallback(() => {
const members: Member[] = selectedMembers.map((member) => ({displayName: member.text, avatar: member.icons.at(0)?.source, email: member.login}));
const members: Member[] = selectedMembers.map((member) => ({displayName: member.text ?? '', avatar: member.icons?.at(0)?.source, email: member.login ?? ''}));
setApprovalWorkflowMembers(members);

if (isInitialCreationFlow) {
Expand Down Expand Up @@ -180,6 +181,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat
footerContent={button}
shouldShowLoadingPlaceholder={isLoadingApprovalWorkflow}
shouldEnableHeaderMaxHeight
shouldUpdateFocusedIndex={false}
/>
</AccessOrNotFoundWrapper>
);
Expand Down
Loading