From d7285fbf4002c67934f1506ca78892124ea2d48a Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Tue, 20 Jan 2026 14:19:53 +0100 Subject: [PATCH 1/3] add selection --- src/pages/domain/BaseDomainMembersPage.tsx | 139 ++++++++++++++---- .../domain/Members/DomainMembersPage.tsx | 1 + 2 files changed, 111 insertions(+), 29 deletions(-) diff --git a/src/pages/domain/BaseDomainMembersPage.tsx b/src/pages/domain/BaseDomainMembersPage.tsx index e49510c14bcb5..f11c8d3942632 100644 --- a/src/pages/domain/BaseDomainMembersPage.tsx +++ b/src/pages/domain/BaseDomainMembersPage.tsx @@ -1,30 +1,31 @@ -import React from 'react'; -import {View} from 'react-native'; +import React, { useCallback, useState } from 'react'; +import { View } from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SearchBar from '@components/SearchBar'; // eslint-disable-next-line no-restricted-imports import SelectionList from '@components/SelectionList'; import TableListItem from '@components/SelectionList/ListItem/TableListItem'; -import type {ListItem} from '@components/SelectionList/types'; +import type { ListItem } from '@components/SelectionList/types'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import { useMemoizedLazyExpensifyIcons } from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchResults from '@hooks/useSearchResults'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getLatestError} from '@libs/ErrorUtils'; -import {sortAlphabetically} from '@libs/OptionsListUtils'; -import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; +import { getLatestError } from '@libs/ErrorUtils'; +import { sortAlphabetically } from '@libs/OptionsListUtils'; +import { getDisplayNameOrDefault } from '@libs/PersonalDetailsUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; +import type { Errors, PendingAction } from '@src/types/onyx/OnyxCommon'; import type IconAsset from '@src/types/utils/IconAsset'; import DomainNotFoundPageWrapper from './DomainNotFoundPageWrapper'; + type MemberOption = Omit & { /** Member accountID */ accountID: number; @@ -62,6 +63,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 +97,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 +149,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 +216,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..c95b0714d0700 100644 --- a/src/pages/domain/Members/DomainMembersPage.tsx +++ b/src/pages/domain/Members/DomainMembersPage.tsx @@ -31,6 +31,7 @@ function DomainMembersPage({route}: DomainMembersPageProps) { searchPlaceholder={translate('domain.members.findMember')} onSelectRow={(item) => Navigation.navigate(ROUTES.DOMAIN_MEMBER_DETAILS.getRoute(domainAccountID, item.accountID))} headerIcon={illustrations.Profile} + canSelectMultiple /> ); } From 9203435731ed849c27f77ca5b6f40655d20e3716 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Tue, 20 Jan 2026 16:26:10 +0100 Subject: [PATCH 2/3] UI to close domian member account --- src/CONST/index.ts | 22 ++++-- .../ButtonWithDropdownMenu/types.ts | 3 + src/languages/en.ts | 4 ++ .../domain/Members/DomainMembersPage.tsx | 72 +++++++++++++++++-- 4 files changed, 88 insertions(+), 13 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 5c7d7bb1b813b..601191c321e58 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1,19 +1,20 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import {add as dateAdd} from 'date-fns'; -import {sub as dateSubtract} from 'date-fns/sub'; +import { add as dateAdd } from 'date-fns'; +import { sub as dateSubtract } from 'date-fns/sub'; import Config from 'react-native-config'; import * as KeyCommand from 'react-native-key-command'; -import type {ValueOf} from 'type-fest'; -import type {SearchFilterKey} from '@components/Search/types'; +import type { ValueOf } from 'type-fest'; +import type { SearchFilterKey } from '@components/Search/types'; import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types'; -import type {MileageRate} from '@libs/DistanceRequestUtils'; +import type { MileageRate } from '@libs/DistanceRequestUtils'; import addTrailingForwardSlash from '@libs/UrlUtils'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; -import type {PolicyTagLists} from '@src/types/onyx'; +import type { PolicyTagLists } from '@src/types/onyx'; import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount'; -import {LOCALES} from './LOCALES'; +import { LOCALES } from './LOCALES'; + // Creating a default array and object this way because objects ({}) and arrays ([]) are not stable types. // Freezing the array ensures that it cannot be unintentionally modified. @@ -8029,6 +8030,13 @@ 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 54a03f8bd7756..031bc87cd7485 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7936,6 +7936,10 @@ const translations = { members: { title: 'Members', findMember: 'Find member', + closeAccount: () => ({ + one: 'Close account', + other: 'Close accounts', + }), }, }, }; diff --git a/src/pages/domain/Members/DomainMembersPage.tsx b/src/pages/domain/Members/DomainMembersPage.tsx index c95b0714d0700..2bf219487e515 100644 --- a/src/pages/domain/Members/DomainMembersPage.tsx +++ b/src/pages/domain/Members/DomainMembersPage.tsx @@ -1,28 +1,85 @@ -import {memberAccountIDsSelector} from '@selectors/Domain'; -import React from 'react'; -import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import { memberAccountIDsSelector } from '@selectors/Domain'; +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 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'; + type DomainMembersPageProps = PlatformStackScreenProps; function DomainMembersPage({route}: DomainMembersPageProps) { const {domainAccountID} = route.params; const {translate} = useLocalize(); + const styles = useThemeStyles(); const illustrations = useMemoizedLazyIllustrations(['Profile']); + const [controlledSelectedMembers, controlledSetSelectedMembers] = useState([]); + 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} + /> ); } From 60f324e8d2eaa4502e31ad6dc814162cc48f04c5 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Wed, 21 Jan 2026 10:39:12 +0100 Subject: [PATCH 3/3] run prettier --- src/CONST/index.ts | 18 ++++++------- src/pages/domain/BaseDomainMembersPage.tsx | 17 ++++++------ .../domain/Members/DomainMembersPage.tsx | 26 ++++++++----------- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 679503b869a12..9b408cf6e1f69 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1,20 +1,19 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { add as dateAdd } from 'date-fns'; -import { sub as dateSubtract } from 'date-fns/sub'; +import {add as dateAdd} from 'date-fns'; +import {sub as dateSubtract} from 'date-fns/sub'; import Config from 'react-native-config'; import * as KeyCommand from 'react-native-key-command'; -import type { ValueOf } from 'type-fest'; -import type { SearchFilterKey } from '@components/Search/types'; +import type {ValueOf} from 'type-fest'; +import type {SearchFilterKey} from '@components/Search/types'; import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types'; -import type { MileageRate } from '@libs/DistanceRequestUtils'; +import type {MileageRate} from '@libs/DistanceRequestUtils'; import addTrailingForwardSlash from '@libs/UrlUtils'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; -import type { PolicyTagLists } from '@src/types/onyx'; +import type {PolicyTagLists} from '@src/types/onyx'; import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount'; -import { LOCALES } from './LOCALES'; - +import {LOCALES} from './LOCALES'; // Creating a default array and object this way because objects ({}) and arrays ([]) are not stable types. // Freezing the array ensures that it cannot be unintentionally modified. @@ -8032,8 +8031,7 @@ const CONST = { /** Onyx prefix for domain security groups */ DOMAIN_SECURITY_GROUP_PREFIX: 'domain_securityGroup_', - PRIMARY_ACTIONS: { - }, + PRIMARY_ACTIONS: {}, MEMBERS_BULK_ACTION_TYPES: { CLOSE_ACCOUNT: 'closeAccount', diff --git a/src/pages/domain/BaseDomainMembersPage.tsx b/src/pages/domain/BaseDomainMembersPage.tsx index f11c8d3942632..cc1ca964ddfcc 100644 --- a/src/pages/domain/BaseDomainMembersPage.tsx +++ b/src/pages/domain/BaseDomainMembersPage.tsx @@ -1,31 +1,30 @@ -import React, { useCallback, useState } from 'react'; -import { View } from 'react-native'; +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SearchBar from '@components/SearchBar'; // eslint-disable-next-line no-restricted-imports import SelectionList from '@components/SelectionList'; import TableListItem from '@components/SelectionList/ListItem/TableListItem'; -import type { ListItem } from '@components/SelectionList/types'; +import type {ListItem} from '@components/SelectionList/types'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; -import { useMemoizedLazyExpensifyIcons } from '@hooks/useLazyAsset'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchResults from '@hooks/useSearchResults'; import useThemeStyles from '@hooks/useThemeStyles'; -import { getLatestError } from '@libs/ErrorUtils'; -import { sortAlphabetically } from '@libs/OptionsListUtils'; -import { getDisplayNameOrDefault } from '@libs/PersonalDetailsUtils'; +import {getLatestError} from '@libs/ErrorUtils'; +import {sortAlphabetically} from '@libs/OptionsListUtils'; +import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type { Errors, PendingAction } from '@src/types/onyx/OnyxCommon'; +import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import type IconAsset from '@src/types/utils/IconAsset'; import DomainNotFoundPageWrapper from './DomainNotFoundPageWrapper'; - type MemberOption = Omit & { /** Member accountID */ accountID: number; diff --git a/src/pages/domain/Members/DomainMembersPage.tsx b/src/pages/domain/Members/DomainMembersPage.tsx index 2bf219487e515..fee8c24136362 100644 --- a/src/pages/domain/Members/DomainMembersPage.tsx +++ b/src/pages/domain/Members/DomainMembersPage.tsx @@ -1,25 +1,24 @@ -import { memberAccountIDsSelector } from '@selectors/Domain'; -import React, { useState } from 'react'; -import { View } from 'react-native'; +import {memberAccountIDsSelector} from '@selectors/Domain'; +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 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 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'; - type DomainMembersPageProps = PlatformStackScreenProps; function DomainMembersPage({route}: DomainMembersPageProps) { @@ -31,7 +30,6 @@ function DomainMembersPage({route}: DomainMembersPageProps) { const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(['RemoveMembers']); - const [memberIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { canBeMissing: true, selector: memberAccountIDsSelector, @@ -43,17 +41,15 @@ function DomainMembersPage({route}: DomainMembersPageProps) { text: translate('domain.members.closeAccount', {count: controlledSetSelectedMembers.length}), value: CONST.DOMAIN.MEMBERS_BULK_ACTION_TYPES.CLOSE_ACCOUNT, icon: icons.RemoveMembers, - onSelected: ()=>{}, + onSelected: () => {}, }, ]; - return options; }; const getHeaderButtons = () => { - - return (controlledSelectedMembers.length > 0) ? ( + return controlledSelectedMembers.length > 0 ? ( shouldAlwaysShowDropdownMenu customText={translate('workspace.common.selected', {count: controlledSelectedMembers.length})} @@ -92,7 +88,7 @@ function DomainMembersPage({route}: DomainMembersPageProps) { canSelectMultiple controlledSelectedMembers={controlledSelectedMembers} controlledSetSelectedMembers={controlledSetSelectedMembers} - /> + /> ); }