diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index c39d7a05a4f75..eb9450f6ad98d 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -12,6 +12,7 @@ import type {BaseListItemProps, ListItem} from './types'; function BaseListItem({ item, + pressableStyle, wrapperStyle, selectMultipleStyle, isDisabled = false, @@ -59,6 +60,7 @@ function BaseListItem({ dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} nativeID={keyForList} + style={pressableStyle} > {({hovered}) => ( <> diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index edaa48f2cf7a7..1c69d00b39102 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -61,6 +61,8 @@ function BaseSelectionList( rightHandSideComponent, isLoadingNewOptions = false, onLayout, + customListHeader, + listHeaderWrapperStyle, }: BaseSelectionListProps, inputRef: ForwardedRef, ) { @@ -428,7 +430,7 @@ function BaseSelectionList( <> {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( ( onPress={selectAllRow} disabled={flattenedSections.allOptions.length === flattenedSections.disabledOptionsIndexes.length} /> - - {translate('workspace.people.selectAll')} - + {customListHeader ?? ( + + {translate('workspace.people.selectAll')} + + )} )} + {!headerMessage && !canSelectMultiple && customListHeader} + {(hovered) => ( + <> + {!!item.icons && ( + + )} + + + {!!item.alternateText && ( + + )} + + {!!item.rightElement && item.rightElement} + + )} + + ); +} + +TableListItem.displayName = 'TableListItem'; + +export default TableListItem; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 868e47309921e..59f6b14cfb1fd 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -4,6 +4,7 @@ import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type RadioListItem from './RadioListItem'; +import type TableListItem from './TableListItem'; import type UserListItem from './UserListItem'; type CommonListItemProps = { @@ -28,6 +29,9 @@ type CommonListItemProps = { /** Component to display on the right side */ rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null; + /** Styles for the pressable component */ + pressableStyle?: StyleProp; + /** Styles for the wrapper view */ wrapperStyle?: StyleProp; @@ -121,6 +125,8 @@ type UserListItemProps = ListItemProps & { type RadioListItemProps = ListItemProps; +type TableListItemProps = ListItemProps; + type Section = { /** Title of the section */ title?: string; @@ -143,7 +149,7 @@ type BaseSelectionListProps = Partial & { sections: Array>>; /** Default renderer for every item in the list */ - ListItem: typeof RadioListItem | typeof UserListItem; + ListItem: typeof RadioListItem | typeof UserListItem | typeof TableListItem; /** Whether this is a multi-select list */ canSelectMultiple?: boolean; @@ -246,6 +252,12 @@ type BaseSelectionListProps = Partial & { /** Fired when the list is displayed with the items */ onLayout?: (event: LayoutChangeEvent) => void; + + /** Custom header to show right above list */ + customListHeader?: ReactNode; + + /** Styles for the list header wrapper */ + listHeaderWrapperStyle?: StyleProp; }; type ItemLayout = { @@ -272,6 +284,7 @@ export type { BaseListItemProps, UserListItemProps, RadioListItemProps, + TableListItemProps, ListItem, ListItemProps, FlattenedSectionsReturn, diff --git a/src/languages/en.ts b/src/languages/en.ts index 0553d6470ddc8..b6a24f33035c3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -205,6 +205,7 @@ export default { iAcceptThe: 'I accept the ', remove: 'Remove', admin: 'Admin', + owner: 'Owner', dateFormat: 'YYYY-MM-DD', send: 'Send', notifications: 'Notifications', @@ -308,6 +309,8 @@ export default { of: 'of', default: 'Default', update: 'Update', + member: 'Member', + role: 'Role', }, location: { useCurrent: 'Use current location', @@ -1745,6 +1748,7 @@ export default { }, addedWithPrimary: 'Some users were added with their primary logins.', invitedBySecondaryLogin: ({secondaryLogin}) => `Added by secondary login ${secondaryLogin}.`, + membersListTitle: 'Directory of all workspace members.', }, card: { header: 'Unlock free Expensify Cards', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2a2eb96bd4887..fc6755519d6f5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -195,6 +195,7 @@ export default { iAcceptThe: 'Acepto los ', remove: 'Eliminar', admin: 'Administrador', + owner: 'Poseedor', dateFormat: 'AAAA-MM-DD', send: 'Enviar', notifications: 'Notificaciones', @@ -298,6 +299,8 @@ export default { of: 'de', default: 'Predeterminado', update: 'Actualizar', + member: 'Miembro', + role: 'Role', }, location: { useCurrent: 'Usar ubicación actual', @@ -1769,6 +1772,7 @@ export default { }, addedWithPrimary: 'Se agregaron algunos usuarios con sus nombres de usuario principales.', invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`, + membersListTitle: 'Directorio de todos los miembros del espacio de trabajo.', }, card: { header: 'Desbloquea Tarjetas Expensify gratis', diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 5554f6ad282b3..f28a435d26d96 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -1,4 +1,3 @@ -import {useIsFocused} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import lodashIsEqual from 'lodash/isEqual'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; @@ -6,6 +5,7 @@ import type {TextInput} from 'react-native'; import {InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import Badge from '@components/Badge'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; @@ -15,14 +15,15 @@ import * as Illustrations from '@components/Icon/Illustrations'; import MessagesRow from '@components/MessagesRow'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; +import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; -import UserListItem from '@components/SelectionList/UserListItem'; import Text from '@components/Text'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; +import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -41,7 +42,6 @@ import type SCREENS from '@src/SCREENS'; import type {PersonalDetailsList, PolicyMember, PolicyMembers, Session} from '@src/types/onyx'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import SearchInputManager from './SearchInputManager'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -70,10 +70,10 @@ type MemberOption = Omit & {accountID: number}; function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, session, currentUserPersonalDetails, isLoadingReportData = true}: WorkspaceMembersPageProps) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [selectedEmployees, setSelectedEmployees] = useState([]); const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false); const [errors, setErrors] = useState({}); - const [searchValue, setSearchValue] = useState(''); const {isOffline} = useNetwork(); const prevIsOffline = usePrevious(isOffline); const accountIDs = useMemo(() => Object.keys(policyMembers ?? {}).map((accountID) => Number(accountID)), [policyMembers]); @@ -84,19 +84,6 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se const {translate, formatPhoneNumber, preferredLocale} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); - const isFocusedScreen = useIsFocused(); - - useEffect(() => { - setSearchValue(SearchInputManager.searchInput); - }, [isFocusedScreen]); - - useEffect( - () => () => { - SearchInputManager.searchInput = ''; - }, - [], - ); - /** * Get filtered personalDetails list with current policyMembers */ @@ -176,7 +163,6 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se * Open the modal to invite a user */ const inviteUser = () => { - setSearchValue(''); Navigation.navigate(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID)); }; @@ -306,30 +292,6 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se return; } - // If search value is provided, filter out members that don't match the search value - if (searchValue.trim()) { - let memberDetails = ''; - if (details.login) { - memberDetails += ` ${details.login.toLowerCase()}`; - } - if (details.firstName) { - memberDetails += ` ${details.firstName.toLowerCase()}`; - } - if (details.lastName) { - memberDetails += ` ${details.lastName.toLowerCase()}`; - } - if (details.displayName) { - memberDetails += ` ${details.displayName.toLowerCase()}`; - } - if (details.phoneNumber) { - memberDetails += ` ${details.phoneNumber.toLowerCase()}`; - } - - if (!OptionsListUtils.isSearchStringMatch(searchValue.trim(), memberDetails)) { - return; - } - } - // If this policy is owned by Expensify then show all support (expensify.com or team.expensify.com) emails // We don't want to show guides as policy members unless the user is a guide. Some customers get confused when they // see random people added to their policy, but guides having access to the policies help set them up. @@ -339,8 +301,20 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se } } + const isOwner = policy?.owner === details.login; const isAdmin = session?.email === details.login || policyMember.role === CONST.POLICY.ROLE.ADMIN; + let roleBadge = null; + if (isOwner || isAdmin) { + roleBadge = ( + + ); + } + result.push({ keyForList: accountIDKey, accountID, @@ -352,11 +326,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se !isEmptyObject(policyMember.errors), text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), alternateText: formatPhoneNumber(details?.login ?? ''), - rightElement: isAdmin ? ( - - {translate('common.admin')} - - ) : undefined, + rightElement: roleBadge, icons: [ { source: UserUtils.getAvatar(details.avatar, accountID), @@ -383,23 +353,34 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se if (isOfflineAndNoMemberDataAvailable) { return translate('workspace.common.mustBeOnlineToViewMembers'); } - return searchValue.trim() && !data.length ? translate('workspace.common.memberNotFound') : ''; + return !data.length ? translate('workspace.common.memberNotFound') : ''; }; - const getHeaderContent = () => { - if (isEmptyObject(invitedPrimaryToSecondaryLogins)) { - return null; - } - return ( - Policy.dismissAddedWithPrimaryLoginMessages(policyID)} - /> - ); - }; + const getHeaderContent = () => ( + <> + {translate('workspace.people.membersListTitle')} + {!isEmptyObject(invitedPrimaryToSecondaryLogins) && ( + Policy.dismissAddedWithPrimaryLoginMessages(policyID)} + /> + )} + + ); + + const getCustomListHeader = () => ( + + + {translate('common.member')} + + + {translate('common.role')} + + + ); const getHeaderButtons = () => ( @@ -440,7 +421,6 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se title={translate('workspace.common.members')} icon={Illustrations.ReceiptWrangler} onBackButtonPress={() => { - setSearchValue(''); Navigation.goBack(); }} shouldShowBackButton={isSmallScreenWidth} @@ -471,13 +451,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se { - SearchInputManager.searchInput = value; - setSearchValue(value); - }} + ListItem={TableListItem} disableKeyboardShortcuts={removeMembersConfirmModalVisible} headerMessage={getHeaderMessage()} headerContent={getHeaderContent()} @@ -488,6 +462,8 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se showScrollIndicator shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} ref={textInputRef} + customListHeader={getCustomListHeader()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} /> diff --git a/src/stories/SelectionList.stories.js b/src/stories/SelectionList.stories.js index dcd6391198869..6c289097552ba 100644 --- a/src/stories/SelectionList.stories.js +++ b/src/stories/SelectionList.stories.js @@ -1,9 +1,8 @@ import React, {useMemo, useState} from 'react'; -import {View} from 'react-native'; import _ from 'underscore'; +import Badge from '@components/Badge'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; -import Text from '@components/Text'; // eslint-disable-next-line no-restricted-imports import {defaultStyles} from '@styles/index'; import CONST from '@src/CONST'; @@ -232,9 +231,11 @@ function MultipleSelection(args) { accountID: item.keyForList, login: item.text, rightElement: isAdmin && ( - - Admin - + ), }; }); @@ -295,9 +296,11 @@ function WithSectionHeader(args) { accountID: item.keyForList, login: item.text, rightElement: isAdmin && ( - - Admin - + ), }; }); @@ -356,9 +359,11 @@ function WithConfirmButton(args) { accountID: item.keyForList, login: item.text, rightElement: isAdmin && ( - - Admin - + ), }; }); diff --git a/src/styles/index.ts b/src/styles/index.ts index 13b2015d2c9c8..238ba1afc781c 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3115,19 +3115,6 @@ const styles = (theme: ThemeColors) => ...spacing.pb2, }, - peopleBadge: { - backgroundColor: theme.icon, - ...spacing.ph3, - ...spacing.ml3, - }, - - peopleBadgeText: { - color: theme.textReversed, - fontSize: variables.fontSizeSmall, - lineHeight: variables.lineHeightNormal, - ...whiteSpace.noWrap, - }, - offlineFeedback: { deleted: { textDecorationLine: 'line-through', @@ -4231,6 +4218,17 @@ const styles = (theme: ThemeColors) => marginHorizontal: 20, }, + selectionListPressableItemWrapper: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: 16, + paddingVertical: 16, + marginHorizontal: 20, + marginBottom: 12, + backgroundColor: theme.highlightBG, + borderRadius: 8, + }, + draggableTopBar: { height: 30, width: '100%', diff --git a/src/styles/utils/borders.ts b/src/styles/utils/borders.ts index 26fdf6415fc79..2e20091e3fae8 100644 --- a/src/styles/utils/borders.ts +++ b/src/styles/utils/borders.ts @@ -8,6 +8,10 @@ export default { borderRadius: 0, }, + br1: { + borderRadius: 4, + }, + br2: { borderRadius: 8, },