diff --git a/src/components/SelectionListRadio/BaseSelectionListRadio.js b/src/components/SelectionListRadio/BaseSelectionListRadio.js index 2b8f373b987a..ac425771dd96 100644 --- a/src/components/SelectionListRadio/BaseSelectionListRadio.js +++ b/src/components/SelectionListRadio/BaseSelectionListRadio.js @@ -11,6 +11,7 @@ import CONST from '../../CONST'; import variables from '../../styles/variables'; import {propTypes as selectionListRadioPropTypes, defaultProps as selectionListRadioDefaultProps} from './selectionListRadioPropTypes'; import RadioListItem from './RadioListItem'; +import CheckboxListItem from './CheckboxListItem'; import useKeyboardShortcut from '../../hooks/useKeyboardShortcut'; import SafeAreaConsumer from '../SafeAreaConsumer'; import withKeyboardState, {keyboardStatePropTypes} from '../withKeyboardState'; @@ -44,6 +45,8 @@ function BaseSelectionListRadio(props) { let offset = 0; const itemLayouts = [{length: 0, offset}]; + let selectedCount = 0; + _.each(props.sections, (section, sectionIndex) => { // We're not rendering any section header, but we need to push to the array // because React Native accounts for it in getItemLayout @@ -51,16 +54,16 @@ function BaseSelectionListRadio(props) { itemLayouts.push({length: sectionHeaderHeight, offset}); offset += sectionHeaderHeight; - _.each(section.data, (option, optionIndex) => { + _.each(section.data, (item, optionIndex) => { // Add item to the general flattened array allOptions.push({ - ...option, + ...item, sectionIndex, index: optionIndex, }); // If disabled, add to the disabled indexes array - if (section.isDisabled || option.isDisabled) { + if (section.isDisabled || item.isDisabled) { disabledOptionsIndexes.push(disabledIndex); } disabledIndex += 1; @@ -69,6 +72,10 @@ function BaseSelectionListRadio(props) { const fullItemHeight = variables.optionRowHeight; itemLayouts.push({length: fullItemHeight, offset}); offset += fullItemHeight; + + if (item.isSelected) { + selectedCount++; + } }); // We're not rendering any section footer, but we need to push to the array @@ -80,6 +87,12 @@ function BaseSelectionListRadio(props) { // because React Native accounts for it in getItemLayout itemLayouts.push({length: 0, offset}); + if (selectedCount > 1 && !props.canSelectMultiple) { + throw new Error( + 'Dev error: SelectionList - multiple items are selected but prop `canSelectMultiple` is false. Please enable `canSelectMultiple` or make your list have only 1 item with `isSelected: true`.', + ); + } + return { allOptions, disabledOptionsIndexes, @@ -159,6 +172,16 @@ function BaseSelectionListRadio(props) { const renderItem = ({item, index, section}) => { const isFocused = focusedIndex === index + lodashGet(section, 'indexOffset', 0); + if (props.canSelectMultiple) { + return ( + + ); + } + return ( {}, +}; + +function CheckboxListItem(props) { + // TODO: REVIEW ERRORS + + const errors = {}; + + return ( + <> + props.onSelectRow(props.item)} + accessibilityLabel={props.item.text} + accessibilityRole="checkbox" + accessibilityState={{checked: props.item.isSelected}} + hoverDimmingValue={1} + hoverStyle={styles.hoveredComponentBG} + focusStyle={styles.hoveredComponentBG} + > + props.onSelectRow(props.item)} + /> + + + + + {props.item.text} + + + {Boolean(props.item.alternateText) && ( + + {props.item.alternateText} + + )} + + + + {props.item.isAdmin && ( + + {props.translate('common.admin')} + + )} + + {!_.isEmpty(errors[item.accountID]) && ( + + )} + + ); +} + +CheckboxListItem.displayName = 'CheckboxListItem'; +CheckboxListItem.propTypes = propTypes; +CheckboxListItem.defaultProps = defaultProps; + +export default withLocalize(CheckboxListItem); diff --git a/src/components/SelectionListRadio/selectionListRadioPropTypes.js b/src/components/SelectionListRadio/selectionListRadioPropTypes.js index 14e41b195d7b..39081def712b 100644 --- a/src/components/SelectionListRadio/selectionListRadioPropTypes.js +++ b/src/components/SelectionListRadio/selectionListRadioPropTypes.js @@ -33,6 +33,9 @@ const propTypes = { }), ).isRequired, + /** Whether this is a multi-select list */ + canSelectMultiple: PropTypes.bool, + /** Callback to fire when a row is tapped */ onSelectRow: PropTypes.func, @@ -71,6 +74,7 @@ const propTypes = { }; const defaultProps = { + canSelectMultiple: false, onSelectRow: () => {}, textInputLabel: '', textInputPlaceholder: '', diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 2a5cecb1cf94..abf29731ca6d 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -38,6 +38,7 @@ import PressableWithFeedback from '../../components/Pressable/PressableWithFeedb import usePrevious from '../../hooks/usePrevious'; import Log from '../../libs/Log'; import * as PersonalDetailsUtils from '../../libs/PersonalDetailsUtils'; +import SelectionListRadio from '../../components/SelectionListRadio'; const propTypes = { /** The personal details of the person who is logged in */ @@ -249,9 +250,9 @@ function WorkspaceMembersPage(props) { // Add or remove the user if the checkbox is enabled if (_.contains(selectedEmployees, Number(accountID))) { - removeUser(accountID); + removeUser(Number(accountID)); } else { - addUser(accountID); + addUser(Number(accountID)); } }, [selectedEmployees, addUser, removeUser], @@ -395,6 +396,77 @@ function WorkspaceMembersPage(props) { const policyID = lodashGet(props.route, 'params.policyID'); const policyName = lodashGet(props.policy, 'name'); + const getListData = () => { + let result = []; + + _.each(props.policyMembers, (policyMember, accountID) => { + if (isDeletedPolicyMember(policyMember)) { + return; + } + + const details = props.personalDetails[accountID]; + + if (!details) { + Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`); + 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. + if (PolicyUtils.isExpensifyTeam(details.login || details.displayName)) { + if (policyOwner && currentUserLogin && !PolicyUtils.isExpensifyTeam(policyOwner) && !PolicyUtils.isExpensifyTeam(currentUserLogin)) { + return; + } + } + + result.push({ + keyForList: accountID, + isSelected: _.contains(selectedEmployees, Number(accountID)), + text: props.formatPhoneNumber(details.displayName), + alternateText: props.formatPhoneNumber(details.login), + isAdmin: props.session.email === details.login || policyMember.role === 'admin', + avatar: { + source: UserUtils.getAvatar(details.avatar, accountID), + name: details.login, + type: CONST.ICON_TYPE_AVATAR, + }, + }); + }); + + result = _.sortBy(result, (value) => value.text.toLowerCase()); + + return result; + }; + + const data2 = getListData(); + + const headerMessage = searchValue.trim() && !data2.length ? props.translate('common.noResultsFound') : ''; + return ( - - */} + {/* */} + {/* */} + {/* {data.length > 0 ? ( */} + + + _.contains(selectedEmployees, Number(accountID)))} + onPress={() => toggleAllUsers(removableMembers)} + /> + + {props.translate('workspace.people.selectAll')} + + + + toggleUser(item.keyForList)} /> + + {/* item.login} */} + {/* showsVerticalScrollIndicator */} + {/* style={[styles.ph5, styles.pb5]} */} + {/* contentContainerStyle={safeAreaPaddingBottomStyle} */} + {/* keyboardShouldPersistTaps="handled" */} + {/* /> */} - {data.length > 0 ? ( - - - toggleAllUsers(removableMembers)} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.CHECKBOX} - accessibilityState={{ - checked: !_.isEmpty(removableMembers) && _.every(_.keys(removableMembers), (accountID) => _.contains(selectedEmployees, Number(accountID))), - }} - accessibilityLabel={props.translate('workspace.people.selectAll')} - hoverDimmingValue={1} - pressDimmingValue={0.7} - > - _.contains(selectedEmployees, Number(accountID)))} - onPress={() => toggleAllUsers(removableMembers)} - accessibilityLabel={props.translate('workspace.people.selectAll')} - /> - - - {props.translate('workspace.people.selectAll')} - - - item.login} - showsVerticalScrollIndicator - style={[styles.ph5, styles.pb5]} - contentContainerStyle={safeAreaPaddingBottomStyle} - keyboardShouldPersistTaps="handled" - /> - - ) : ( - - {props.translate('workspace.common.memberNotFound')} - - )} + {/* ) : ( */} + {/* */} + {/* {props.translate('workspace.common.memberNotFound')} */} + {/* */} + {/* )} */} )}