From d24b4f59c65223325e113e3b1fac8f65ebda061b Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 22 Feb 2024 10:10:05 +0100 Subject: [PATCH 01/17] table list item component introduced --- .../SelectionList/TableListItem.tsx | 100 ++++++++++++++++++ src/components/SelectionList/types.ts | 6 +- 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/components/SelectionList/TableListItem.tsx diff --git a/src/components/SelectionList/TableListItem.tsx b/src/components/SelectionList/TableListItem.tsx new file mode 100644 index 0000000000000..978a49fb98fa4 --- /dev/null +++ b/src/components/SelectionList/TableListItem.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import {View} from 'react-native'; +import MultipleAvatars from '@components/MultipleAvatars'; +import TextWithTooltip from '@components/TextWithTooltip'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import BaseListItem from './BaseListItem'; +import {TableListItemProps} from "@components/SelectionList/types"; + + +function UserListItem({ + item, + isFocused, + showTooltip, + isDisabled, + canSelectMultiple, + onSelectRow, + onDismissError, + shouldPreventDefaultFocusOnSelectRow, + rightHandSideComponent, +}: TableListItemProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const StyleUtils = useStyleUtils(); + + const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; + const hoveredBackgroundColor = !!styles.sidebarLinkHover && 'backgroundColor' in styles.sidebarLinkHover ? styles.sidebarLinkHover.backgroundColor : theme.sidebar; + + return ( + + {(hovered) => ( + <> + {!!item.icons && ( + + )} + + + {!!item.alternateText && ( + + )} + + {!!item.rightElement && item.rightElement} + + )} + + ); +} + +UserListItem.displayName = 'UserListItem'; + +export default UserListItem; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 403ccd91a26bb..2f9641008fae9 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -5,6 +5,7 @@ import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type RadioListItem from './RadioListItem'; import type UserListItem from './UserListItem'; +import TableListItem from "@components/SelectionList/TableListItem"; type CommonListItemProps = { /** Whether this item is focused (for arrow key controls) */ @@ -121,6 +122,8 @@ type UserListItemProps = ListItemProps & { type RadioListItemProps = ListItemProps; +type TableListItemProps = ListItemProps; + type Section = { /** Title of the section */ title?: string; @@ -143,7 +146,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; @@ -272,6 +275,7 @@ export type { BaseListItemProps, UserListItemProps, RadioListItemProps, + TableListItemProps, ListItem, ListItemProps, FlattenedSectionsReturn, From 1f638ed321b36d23991f17a016ef1d70b2f7d9a3 Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 22 Feb 2024 10:10:34 +0100 Subject: [PATCH 02/17] workspace members page: search removed and new list item used --- src/languages/en.ts | 4 + src/languages/es.ts | 4 + src/pages/workspace/WorkspaceMembersPage.js | 88 +++++++-------------- 3 files changed, 35 insertions(+), 61 deletions(-) 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.js b/src/pages/workspace/WorkspaceMembersPage.js index 62b96943453cc..df60497a632b2 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -1,4 +1,3 @@ -import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; @@ -16,7 +15,7 @@ import networkPropTypes from '@components/networkPropTypes'; import {withNetwork} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; -import UserListItem from '@components/SelectionList/UserListItem'; +import TableListItem from "@components/SelectionList/TableListItem"; import Text from '@components/Text'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; @@ -83,7 +82,6 @@ function WorkspaceMembersPage(props) { const [selectedEmployees, setSelectedEmployees] = useState([]); const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false); const [errors, setErrors] = useState({}); - const [searchValue, setSearchValue] = useState(''); const prevIsOffline = usePrevious(props.network.isOffline); const accountIDs = useMemo(() => _.map(_.keys(props.policyMembers), (accountID) => Number(accountID)), [props.policyMembers]); const prevAccountIDs = usePrevious(accountIDs); @@ -92,12 +90,6 @@ function WorkspaceMembersPage(props) { const prevPersonalDetails = usePrevious(props.personalDetails); const {isSmallScreenWidth} = useWindowDimensions(); - const isFocusedScreen = useIsFocused(); - - useEffect(() => { - setSearchValue(SearchInputManager.searchInput); - }, [isFocusedScreen]); - useEffect(() => () => (SearchInputManager.searchInput = ''), []); /** @@ -183,7 +175,6 @@ function WorkspaceMembersPage(props) { * Open the modal to invite a user */ const inviteUser = () => { - setSearchValue(''); Navigation.navigate(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID)); }; @@ -325,30 +316,6 @@ function WorkspaceMembersPage(props) { 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. @@ -358,8 +325,18 @@ function WorkspaceMembersPage(props) { } } + const isOwner = props.policy.owner === details.login; const isAdmin = props.session.email === details.login || policyMember.role === CONST.POLICY.ROLE.ADMIN; + let roleBadge = null; + if (isOwner || isAdmin) { + roleBadge = ( + + {isOwner ? props.translate('common.owner') : props.translate('common.admin')} + + ); + } + result.push({ keyForList: accountIDKey, accountID, @@ -371,11 +348,7 @@ function WorkspaceMembersPage(props) { !_.isEmpty(policyMember.errors), text: props.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), alternateText: props.formatPhoneNumber(details.login), - rightElement: isAdmin ? ( - - {props.translate('common.admin')} - - ) : null, + rightElement: roleBadge, icons: [ { source: UserUtils.getAvatar(details.avatar, accountID), @@ -402,22 +375,22 @@ function WorkspaceMembersPage(props) { if (isOfflineAndNoMemberDataAvailable) { return props.translate('workspace.common.mustBeOnlineToViewMembers'); } - return searchValue.trim() && !data.length ? props.translate('workspace.common.memberNotFound') : ''; + return !data.length ? props.translate('workspace.common.memberNotFound') : ''; }; - const getHeaderContent = () => { - if (_.isEmpty(invitedPrimaryToSecondaryLogins)) { - return null; - } - return ( - Policy.dismissAddedWithPrimaryLoginMessages(policyID)} - /> - ); - }; + const getHeaderContent = () => ( + <> + {props.translate('workspace.people.membersListTitle')} + {!_.isEmpty(invitedPrimaryToSecondaryLogins) && ( + Policy.dismissAddedWithPrimaryLoginMessages(policyID)} + /> + )} + + ); const getHeaderButtons = () => ( @@ -458,7 +431,6 @@ function WorkspaceMembersPage(props) { title={props.translate('workspace.common.members')} icon={Illustrations.ReceiptWrangler} onBackButtonPress={() => { - setSearchValue(''); Navigation.goBack(); }} shouldShowBackButton={isSmallScreenWidth} @@ -489,13 +461,7 @@ function WorkspaceMembersPage(props) { { - SearchInputManager.searchInput = value; - setSearchValue(value); - }} + ListItem={TableListItem} disableKeyboardShortcuts={removeMembersConfirmModalVisible} headerMessage={getHeaderMessage()} headerContent={getHeaderContent()} From 316863b45f0356a3826ec59c874f01099accef13 Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 22 Feb 2024 10:33:16 +0100 Subject: [PATCH 03/17] members list items styling added --- src/components/SelectionList/BaseListItem.tsx | 2 ++ .../SelectionList/TableListItem.tsx | 20 +++++-------------- src/components/SelectionList/types.ts | 5 ++++- src/pages/workspace/WorkspaceMembersPage.js | 2 +- src/styles/index.ts | 11 ++++++++++ 5 files changed, 23 insertions(+), 17 deletions(-) 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/TableListItem.tsx b/src/components/SelectionList/TableListItem.tsx index 978a49fb98fa4..d05bfd7e628db 100644 --- a/src/components/SelectionList/TableListItem.tsx +++ b/src/components/SelectionList/TableListItem.tsx @@ -2,13 +2,11 @@ import React from 'react'; import {View} from 'react-native'; import MultipleAvatars from '@components/MultipleAvatars'; import TextWithTooltip from '@components/TextWithTooltip'; -import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import BaseListItem from './BaseListItem'; -import {TableListItemProps} from "@components/SelectionList/types"; - +import type {TableListItemProps} from './types'; function UserListItem({ item, @@ -31,16 +29,8 @@ function UserListItem({ return ( )} - + {!!item.alternateText && ( diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 2f9641008fae9..b139b24427f0e 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,11 +1,11 @@ import type {ReactElement, ReactNode} from 'react'; import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import TableListItem from '@components/SelectionList/TableListItem'; 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 UserListItem from './UserListItem'; -import TableListItem from "@components/SelectionList/TableListItem"; type CommonListItemProps = { /** Whether this item is focused (for arrow key controls) */ @@ -29,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; diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index df60497a632b2..8acb05fc49d25 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -15,7 +15,7 @@ import networkPropTypes from '@components/networkPropTypes'; import {withNetwork} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; -import TableListItem from "@components/SelectionList/TableListItem"; +import TableListItem from '@components/SelectionList/TableListItem'; import Text from '@components/Text'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; diff --git a/src/styles/index.ts b/src/styles/index.ts index 13b2015d2c9c8..04f8f381f9d35 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4231,6 +4231,17 @@ const styles = (theme: ThemeColors) => marginHorizontal: 20, }, + selectionListPressableItemWrapper: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: 16, + paddingVertical: 12, + marginHorizontal: 20, + marginBottom: 12, + backgroundColor: theme.highlightBG, + borderRadius: 8, + }, + draggableTopBar: { height: 30, width: '100%', From 9a28821fa36b359b0939d8c6a7bd7257c609b933 Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 22 Feb 2024 11:04:46 +0100 Subject: [PATCH 04/17] custom list header handled --- .../SelectionList/BaseSelectionList.tsx | 13 +++++++++---- src/components/SelectionList/types.ts | 6 ++++++ src/pages/workspace/WorkspaceMembersPage.js | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index b0996a08895a8..5eb7db10b057b 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} = 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 = { diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 8acb05fc49d25..b8e0254e78bcc 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -21,6 +21,7 @@ import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultPro import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import usePrevious from '@hooks/usePrevious'; +import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; @@ -79,6 +80,7 @@ const defaultProps = { function WorkspaceMembersPage(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [selectedEmployees, setSelectedEmployees] = useState([]); const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false); const [errors, setErrors] = useState({}); @@ -392,6 +394,17 @@ function WorkspaceMembersPage(props) { ); + const getCustomListHeader = () => ( + + + {props.translate('common.member')} + + + {props.translate('common.role')} + + + ); + const getHeaderButtons = () => (