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
88 changes: 37 additions & 51 deletions src/pages/workspace/WorkspaceMembersPage.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import {useIsFocused} from '@react-navigation/native';
import {deepEqual} from 'fast-equals';
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import {InteractionManager, View} from 'react-native';
import type {ValueOf} from 'type-fest';
import Button from '@components/Button';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types';
import ConfirmModal from '@components/ConfirmModal';
import DecisionModal from '@components/DecisionModal';
// eslint-disable-next-line no-restricted-imports
import {Plus} from '@components/Icon/Expensicons';
import {LockedAccountContext} from '@components/LockedAccountModalProvider';
import MessagesRow from '@components/MessagesRow';
import {ModalActions} from '@components/Modal/Global/ModalContext';
import SearchBar from '@components/SearchBar';
import TableListItem from '@components/SelectionList/ListItem/TableListItem';
import type {ListItem, SelectionListHandle} from '@components/SelectionList/types';
import SelectionListWithModal from '@components/SelectionListWithModal';
import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader';
import Text from '@components/Text';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useConfirmModal from '@hooks/useConfirmModal';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useFilteredSelection from '@hooks/useFilteredSelection';
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
Expand Down Expand Up @@ -103,14 +103,13 @@
const employeeListDetails = useMemo(() => policy?.employeeList ?? ({} as PolicyEmployeeList), [policy?.employeeList]);
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const styles = useThemeStyles();
const {showConfirmModal} = useConfirmModal();
const StyleUtils = useStyleUtils();
const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false);
const {isOffline} = useNetwork();
const prevIsOffline = usePrevious(isOffline);
const accountIDs = useMemo(() => Object.values(policyMemberEmailsToAccountIDs ?? {}).map((accountID) => Number(accountID)), [policyMemberEmailsToAccountIDs]);
const prevAccountIDs = usePrevious(accountIDs);
const textInputRef = useRef<BaseTextInputRef>(null);
const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false);
const [isDownloadFailureModalVisible, setIsDownloadFailureModalVisible] = useState(false);
const isOfflineAndNoMemberDataAvailable = isEmptyObject(policy?.employeeList) && isOffline;
const {translate, formatPhoneNumber, localeCompare} = useLocalize();
Expand Down Expand Up @@ -202,14 +201,6 @@
getWorkspaceMembers();
}, [getWorkspaceMembers]);

useEffect(() => {
if (!removeMembersConfirmModalVisible || deepEqual(accountIDs, prevAccountIDs)) {
return;
}
setRemoveMembersConfirmModalVisible(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accountIDs]);

useEffect(() => {
const isReconnecting = prevIsOffline && !isOffline;
if (!isReconnecting) {
Expand All @@ -234,7 +225,7 @@
* Remove selected users from the workspace
* Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
*/
const removeUsers = () => {

Check warning on line 228 in src/pages/workspace/WorkspaceMembersPage.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

The 'removeUsers' function makes the dependencies of useCallback Hook (at line 289) change on every render. Move it inside the useCallback callback. Alternatively, wrap the definition of 'removeUsers' in its own useCallback() Hook
// Check if any of the members are approvers
const hasApprovers = selectedEmployees.some((email) => isApprover(policy, email));

Expand Down Expand Up @@ -266,8 +257,6 @@
}
}

setRemoveMembersConfirmModalVisible(false);

// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.runAfterInteractions(() => {
setSelectedEmployees([]);
Expand All @@ -278,9 +267,26 @@
/**
* Show the modal to confirm removal of the selected members
*/
const askForConfirmationToRemove = () => {
setRemoveMembersConfirmModalVisible(true);
};
const askForConfirmationToRemove = useCallback(async () => {
const result = await showConfirmModal({
danger: true,
title: translate('workspace.people.removeMembersTitle', {count: selectedEmployees.length}),
prompt: confirmModalPrompt,
confirmText: translate('common.remove'),
cancelText: translate('common.cancel'),
onModalHide: () => {
if (!textInputRef.current) {
return;
}
textInputRef.current.focus();
},
});
if (result.action !== ModalActions.CONFIRM) {
return;
}

removeUsers();
}, [confirmModalPrompt, removeUsers, selectedEmployees.length, showConfirmModal, translate]);

/**
* Add or remove all users passed from the selectedEmployees list
Expand Down Expand Up @@ -486,6 +492,7 @@
styles.cursorDefault,
styles.flex1,
styles.pr3,
translate,
styles.alignSelfStart,
styles.alignSelfEnd,
isControlPolicyWithWideLayout,
Expand Down Expand Up @@ -667,6 +674,16 @@
return options;
};

const showRequiresInternetModal = useCallback(() => {
showConfirmModal({
title: translate('common.youAppearToBeOffline'),
prompt: translate('common.thisFeatureRequiresInternet'),
confirmText: translate('common.buttonConfirm'),
shouldShowCancelButton: false,
shouldHandleNavigationBack: true,
});
}, [showConfirmModal, translate]);

const secondaryActions = useMemo(() => {
if (!isPolicyAdmin) {
return [];
Expand All @@ -682,7 +699,7 @@
return;
}
if (isOffline) {
close(() => setIsOfflineModalVisible(true));
close(showRequiresInternetModal);
return;
}
Navigation.navigate(ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(policyID));
Expand All @@ -694,7 +711,7 @@
text: translate('spreadsheet.downloadCSV'),
onSelected: () => {
if (isOffline) {
close(() => setIsOfflineModalVisible(true));
close(showRequiresInternetModal);
return;
}

Expand All @@ -713,7 +730,7 @@
];

return menuItems;
}, [icons.Download, icons.Table, policyID, translate, isOffline, isPolicyAdmin, isAccountLocked, showLockedAccountModal]);
}, [isPolicyAdmin, icons.Table, icons.Download, translate, isAccountLocked, isOffline, policyID, showLockedAccountModal, showRequiresInternetModal]);

const getHeaderButtons = () => {
if (!isPolicyAdmin) {
Expand Down Expand Up @@ -811,36 +828,6 @@
{() => (
<>
{shouldUseNarrowLayout && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>}
<ConfirmModal
isVisible={isOfflineModalVisible}
onConfirm={() => setIsOfflineModalVisible(false)}
title={translate('common.youAppearToBeOffline')}
prompt={translate('common.thisFeatureRequiresInternet')}
confirmText={translate('common.buttonConfirm')}
shouldShowCancelButton={false}
onCancel={() => setIsOfflineModalVisible(false)}
shouldHandleNavigationBack
/>

<ConfirmModal
danger
title={translate('workspace.people.removeMembersTitle', {count: selectedEmployees.length})}
isVisible={removeMembersConfirmModalVisible}
onConfirm={removeUsers}
onCancel={() => setRemoveMembersConfirmModalVisible(false)}
prompt={confirmModalPrompt}
confirmText={translate('common.remove')}
cancelText={translate('common.cancel')}
onModalHide={() => {
// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.runAfterInteractions(() => {
if (!textInputRef.current) {
return;
}
textInputRef.current.focus();
});
}}
/>
<DecisionModal
title={translate('common.downloadFailedTitle')}
prompt={translate('common.downloadFailedDescription')}
Expand All @@ -861,7 +848,6 @@
onSelectAll={filteredData.length > 0 ? () => toggleAllUsers(filteredData) : undefined}
style={{listHeaderWrapperStyle: [styles.ph9, styles.pv3, styles.pb5], listItemTitleContainerStyles: shouldUseNarrowLayout ? undefined : [styles.pr3]}}
onTurnOnSelectionMode={(item) => item && toggleUser(item.login)}
disableKeyboardShortcuts={removeMembersConfirmModalVisible}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
onCheckboxPress={(item) => toggleUser(item.login)}
shouldUseDefaultRightHandSideCheckmark={false}
Expand Down
69 changes: 34 additions & 35 deletions src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import {Str} from 'expensify-common';
import React, {useContext, useEffect, useState} from 'react';
import React, {useContext, useEffect} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import Avatar from '@components/Avatar';
import Button from '@components/Button';
import ButtonDisabledWhenOffline from '@components/Button/ButtonDisabledWhenOffline';
import ConfirmModal from '@components/ConfirmModal';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import {LockedAccountContext} from '@components/LockedAccountModalProvider';
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import {ModalActions} from '@components/Modal/Global/ModalContext';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useCardFeeds from '@hooks/useCardFeeds';
import {useCompanyCardFeedIcons} from '@hooks/useCompanyCardIcons';
import useConfirmModal from '@hooks/useConfirmModal';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
Expand Down Expand Up @@ -88,8 +89,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
const [customCardNames] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES, {canBeMissing: true});
const [fundList] = useOnyx(ONYXKEYS.FUND_LIST, {canBeMissing: true});
const expensifyCardSettings = useExpensifyCardFeeds(policyID);

const [isRemoveMemberConfirmModalVisible, setIsRemoveMemberConfirmModalVisible] = useState(false);
const {showConfirmModal} = useConfirmModal();

const accountID = Number(route.params.accountID);
const memberLogin = personalDetails?.[accountID]?.login ?? '';
Expand All @@ -109,7 +109,6 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
const isSMSLogin = Str.isSMSLogin(memberLogin);
const phoneNumber = getPhoneNumber(details);
const isReimburser = policy?.achAccount?.reimburser === memberLogin;
const [isCannotRemoveUser, setIsCannotRemoveUser] = useState(false);
const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext);

const {approvalWorkflows} = convertPolicyEmployeesToApprovalWorkflows({
Expand Down Expand Up @@ -170,14 +169,6 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
navigateAfterInteraction(() => Navigation.goBack());
}, [member?.pendingAction, prevMember]);

const askForConfirmationToRemove = () => {
if (isReimburser) {
setIsCannotRemoveUser(true);
return;
}
setIsRemoveMemberConfirmModalVisible(true);
};

// Function to remove a member and close the modal
const removeMemberAndCloseModal = () => {
removeMembers(policy, [memberLogin], {[memberLogin]: accountID});
Expand All @@ -187,7 +178,6 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
// We can't let the "Prevent Self Approvals" enabled if there's only one workspace user
setPolicyPreventSelfApproval(policyID, false);
}
setIsRemoveMemberConfirmModalVisible(false);
};

const removeUser = () => {
Expand Down Expand Up @@ -221,6 +211,36 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
removeMemberAndCloseModal();
};

const showRemoveMemberModal = async () => {
const result = await showConfirmModal({
danger: true,
title: translate('workspace.people.removeMemberTitle'),
prompt: confirmModalPrompt,
confirmText: translate('common.remove'),
cancelText: translate('common.cancel'),
});

if (result.action !== ModalActions.CONFIRM) {
return;
}
removeUser();
};

const askForConfirmationToRemove = () => {
if (isReimburser) {
showConfirmModal({
shouldShowCancelButton: false,
success: true,
title: translate('workspace.people.removeMemberTitle'),
prompt: confirmModalPrompt,
confirmText: translate('common.buttonConfirm'),
cancelText: translate('common.cancel'),
});
return;
}
showRemoveMemberModal();
};

const navigateToProfile = () => {
Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute()));
};
Expand Down Expand Up @@ -311,27 +331,6 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
style={styles.mb5}
/>
)}
<ConfirmModal
danger
title={translate('workspace.people.removeMemberTitle')}
isVisible={isRemoveMemberConfirmModalVisible}
onConfirm={removeUser}
onCancel={() => setIsRemoveMemberConfirmModalVisible(false)}
prompt={confirmModalPrompt}
confirmText={translate('common.remove')}
cancelText={translate('common.cancel')}
/>
<ConfirmModal
title={translate('workspace.people.removeMemberTitle')}
isVisible={isCannotRemoveUser}
onConfirm={() => {
setIsCannotRemoveUser(false);
}}
prompt={confirmModalPrompt}
confirmText={translate('common.buttonConfirm')}
success
shouldShowCancelButton={false}
/>
</View>
<View style={styles.w100}>
<MenuItemWithTopDescription
Expand Down
Loading
Loading