diff --git a/src/CONST/index.ts b/src/CONST/index.ts index c73cbde5f39ac..9b408cf6e1f69 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -8030,6 +8030,12 @@ const CONST = { EXPENSIFY_ADMIN_ACCESS_PREFIX: 'expensify_adminPermissions_', /** Onyx prefix for domain security groups */ DOMAIN_SECURITY_GROUP_PREFIX: 'domain_securityGroup_', + + PRIMARY_ACTIONS: {}, + + MEMBERS_BULK_ACTION_TYPES: { + CLOSE_ACCOUNT: 'closeAccount', + }, }, } as const; diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index ae479d2beff0d..56c2142c4ce4e 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -14,6 +14,8 @@ type WorkspaceMemberBulkActionType = DeepValueOf; +type DomainMemberBulkActionType = DeepValueOf; + type WorkspaceDistanceRatesBulkActionType = DeepValueOf; type WorkspaceTaxRatesBulkActionType = DeepValueOf; @@ -167,6 +169,7 @@ type ButtonWithDropdownMenuRef = { export type { PaymentType, WorkspaceMemberBulkActionType, + DomainMemberBulkActionType, RoomMemberBulkActionType, WorkspaceDistanceRatesBulkActionType, DropdownOption, diff --git a/src/languages/en.ts b/src/languages/en.ts index 2cc85b3405ee6..7bda5a24e3036 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7925,6 +7925,10 @@ const translations = { members: { title: 'Members', findMember: 'Find member', + closeAccount: () => ({ + one: 'Close account', + other: 'Close accounts', + }), }, }, }; diff --git a/src/pages/domain/BaseDomainMembersPage.tsx b/src/pages/domain/BaseDomainMembersPage.tsx index e49510c14bcb5..cc1ca964ddfcc 100644 --- a/src/pages/domain/BaseDomainMembersPage.tsx +++ b/src/pages/domain/BaseDomainMembersPage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -62,6 +62,27 @@ type BaseDomainMembersPageProps = { /** Callback fired when the user dismisses an error message for a specific row */ onDismissError?: (item: MemberOption) => void; + + /** + * Allow multiple members to be selected at the same time. + * Defaults to false. + */ + canSelectMultiple?: boolean; + + /** + * **Controlled selected members**. + * Should be provided from the parent component. + * If this is set, `controlledSetSelectedMembers` **must** also be provided. + */ + controlledSelectedMembers?: string[]; + + /** + * **Setter for controlled selected members**. + * Should be provided from the parent component. + * Works like the setter returned by `useState`. + * If this is set, `controlledSelectedMembers` **must** also be provided. + */ + controlledSetSelectedMembers?: React.Dispatch>; }; function BaseDomainMembersPage({ @@ -75,12 +96,20 @@ function BaseDomainMembersPage({ getCustomRightElement, getCustomRowProps, onDismissError, + controlledSelectedMembers, + controlledSetSelectedMembers, + canSelectMultiple = false, }: BaseDomainMembersPageProps) { const {formatPhoneNumber, localeCompare} = useLocalize(); const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar']); + const [internalSelectedMembers, setInternalSelectedMembers] = useState([]); + + const selectedMembers = controlledSelectedMembers ?? internalSelectedMembers; + + const setSelectedMembers = controlledSetSelectedMembers ?? setInternalSelectedMembers; const data: MemberOption[] = accountIDs.map((accountID) => { const details = personalDetails?.[accountID]; @@ -119,13 +148,39 @@ function BaseDomainMembersPage({ const [inputValue, setInputValue, filteredData] = useSearchResults(data, filterMember, sortMembers); + const toggleAllUsers = () => { + const enabledAccounts = filteredData.filter((member) => !member.isDisabled && !member.isDisabledCheckbox); + const enabledAccountIDs = enabledAccounts.map((member) => member.keyForList); + const everySelected = enabledAccountIDs.every((accountID) => selectedMembers.includes(accountID)); + + if (everySelected) { + setSelectedMembers((prevSelected) => prevSelected.filter((accountID) => !enabledAccountIDs.includes(accountID))); + } else { + setSelectedMembers((prevSelected) => { + const newSelected = new Set([...prevSelected, ...enabledAccountIDs]); + return Array.from(newSelected); + }); + } + }; + + const toggleUser = useCallback( + (member: MemberOption) => { + if (selectedMembers.includes(member.keyForList)) { + setSelectedMembers((prevSelected) => prevSelected.filter((accountID) => accountID !== member.keyForList)); + } else { + setSelectedMembers((prevSelected) => [...prevSelected, member.keyForList]); + } + }, + [selectedMembers], + ); + const getCustomListHeader = () => { if (filteredData.length === 0) { return null; } return ( ); @@ -160,26 +215,51 @@ function BaseDomainMembersPage({ {shouldUseNarrowLayout && !!headerContent && {headerContent}} - + {canSelectMultiple ? ( + + ) : ( + + )} ); diff --git a/src/pages/domain/Members/DomainMembersPage.tsx b/src/pages/domain/Members/DomainMembersPage.tsx index 1d75e1cbff477..fee8c24136362 100644 --- a/src/pages/domain/Members/DomainMembersPage.tsx +++ b/src/pages/domain/Members/DomainMembersPage.tsx @@ -1,12 +1,20 @@ import {memberAccountIDsSelector} from '@selectors/Domain'; -import React from 'react'; -import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import React, {useState} from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; +import type {DomainMemberBulkActionType, DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types'; +import {Plus} from '@components/Icon/Expensicons'; +import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@navigation/Navigation'; import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; import type {DomainSplitNavigatorParamList} from '@navigation/types'; import BaseDomainMembersPage from '@pages/domain/BaseDomainMembersPage'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -16,13 +24,58 @@ type DomainMembersPageProps = PlatformStackScreenProps([]); + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); + const icons = useMemoizedLazyExpensifyIcons(['RemoveMembers']); const [memberIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { canBeMissing: true, selector: memberAccountIDsSelector, }); + const getBulkActionsButtonOptions = () => { + const options: Array> = [ + { + text: translate('domain.members.closeAccount', {count: controlledSetSelectedMembers.length}), + value: CONST.DOMAIN.MEMBERS_BULK_ACTION_TYPES.CLOSE_ACCOUNT, + icon: icons.RemoveMembers, + onSelected: () => {}, + }, + ]; + + return options; + }; + + const getHeaderButtons = () => { + return controlledSelectedMembers.length > 0 ? ( + + shouldAlwaysShowDropdownMenu + customText={translate('workspace.common.selected', {count: controlledSelectedMembers.length})} + buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} + onPress={() => null} + options={getBulkActionsButtonOptions()} + isSplitButton={false} + style={[shouldUseNarrowLayout && styles.flexGrow1, shouldUseNarrowLayout && styles.mb3]} + isDisabled={!controlledSelectedMembers.length} + testID="DomainMembersPage-header-dropdown-menu-button" + /> + ) : ( + + {}} + shouldAlwaysShowDropdownMenu + customText={translate('common.more')} + options={[]} + isSplitButton={false} + wrapperStyle={styles.flexGrow0} + /> + + ); + }; + return ( Navigation.navigate(ROUTES.DOMAIN_MEMBER_DETAILS.getRoute(domainAccountID, item.accountID))} headerIcon={illustrations.Profile} + headerContent={getHeaderButtons()} + canSelectMultiple + controlledSelectedMembers={controlledSelectedMembers} + controlledSetSelectedMembers={controlledSetSelectedMembers} /> ); }