Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Invite member checkbox selection behavior #14347

Merged
merged 20 commits into from
Jan 24, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 67 additions & 82 deletions src/pages/workspace/WorkspaceMembersPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,14 @@ import ConfirmModal from '../../components/ConfirmModal';
import personalDetailsPropType from '../personalDetailsPropType';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions';
import OptionRow from '../../components/OptionRow';
import CheckboxWithTooltip from '../../components/CheckboxWithTooltip';
import Hoverable from '../../components/Hoverable';
import withPolicy, {policyPropTypes, policyDefaultProps} from './withPolicy';
import CONST from '../../CONST';
import OfflineWithFeedback from '../../components/OfflineWithFeedback';
import {withNetwork} from '../../components/OnyxProvider';
import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView';
import networkPropTypes from '../../components/networkPropTypes';
import * as Expensicons from '../../components/Icon/Expensicons';
import FormHelpMessage from '../../components/FormHelpMessage';

const propTypes = {
/** The personal details of the person who is logged in */
Expand Down Expand Up @@ -61,7 +60,7 @@ class WorkspaceMembersPage extends React.Component {
this.state = {
selectedEmployees: [],
isRemoveMembersConfirmModalVisible: false,
showTooltipForLogin: '',
errors: {},
};

this.renderItem = this.renderItem.bind(this);
Expand All @@ -77,6 +76,10 @@ class WorkspaceMembersPage extends React.Component {
}

componentDidUpdate(prevProps) {
if (prevProps.preferredLocale !== this.props.preferredLocale) {
this.validate();
}

const isReconnecting = prevProps.network.isOffline && !this.props.network.isOffline;
if (!isReconnecting) {
return;
Expand Down Expand Up @@ -110,6 +113,10 @@ class WorkspaceMembersPage extends React.Component {
* Remove selected users from the workspace
*/
removeUsers() {
if (!_.isEmpty(this.state.errors)) {
return;
}

// Remove the admin from the list
const membersToRemove = _.without(this.state.selectedEmployees, this.props.session.email);
Policy.removeMembers(membersToRemove, this.props.route.params.policyID);
Expand All @@ -123,6 +130,10 @@ class WorkspaceMembersPage extends React.Component {
* Show the modal to confirm removal of the selected members
*/
askForConfirmationToRemove() {
if (!_.isEmpty(this.state.errors)) {
return;
}

this.setState({isRemoveMembersConfirmModalVisible: true});
}

Expand All @@ -137,15 +148,14 @@ class WorkspaceMembersPage extends React.Component {
* Add or remove all users from the selectedEmployees list
*/
toggleAllUsers() {
this.setState({showTooltipForLogin: ''});
let policyMemberList = lodashGet(this.props, 'policyMemberList', {});
policyMemberList = _.filter(_.keys(policyMemberList), policyMember => policyMemberList[policyMember].pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
const removableMembers = _.without(policyMemberList, this.props.session.email, this.props.policy.owner);
this.setState(prevState => ({
selectedEmployees: removableMembers.length !== prevState.selectedEmployees.length
selectedEmployees: !_.every(removableMembers, member => _.contains(prevState.selectedEmployees, member))
? removableMembers
: [],
}));
}), () => this.validate());
}

/**
Expand All @@ -156,49 +166,16 @@ class WorkspaceMembersPage extends React.Component {
*
*/
toggleUser(login, pendingAction) {
if (this.willTooltipShowForLogin(login) || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
return;
}

// Add or remove the user if the checkbox is enabled and is clickable.
// Add or remove the user if the checkbox is enabled
if (_.contains(this.state.selectedEmployees, login)) {
this.removeUser(login);
} else {
this.addUser(login);
}

this.setState({showTooltipForLogin: ''});
}

/**
* Shows the tooltip for non removable members
*
* @param {String} login
* @param {Boolean} wasHovered
* @returns {Boolean} Return true if the tooltip was displayed so we can use the state of it in other functions.
*/
willTooltipShowForLogin(login, wasHovered = false) {
const isSmallOrMediumScreen = this.props.isSmallScreenWidth || this.props.isMediumScreenWidth;

// Small screens only show the tooltip on press, so ignore hovered event on those cases.
if (wasHovered && isSmallOrMediumScreen) {
return false;
}

const canBeRemoved = this.props.policy.owner !== login && this.props.session.email !== login;
if (!canBeRemoved) {
this.setState({
showTooltipForLogin: login,
}, () => {
// Immediately reset the login to deactivate the tooltip trigger, otherwise, the tooltip will not open again on further interactions on small screens.
if (!isSmallOrMediumScreen) {
return;
}
this.setState({showTooltipForLogin: ''});
});
}

return !canBeRemoved;
}

/**
Expand All @@ -209,7 +186,7 @@ class WorkspaceMembersPage extends React.Component {
addUser(login) {
this.setState(prevState => ({
selectedEmployees: [...prevState.selectedEmployees, login],
}));
}), () => this.validate());
}

/**
Expand All @@ -220,7 +197,7 @@ class WorkspaceMembersPage extends React.Component {
removeUser(login) {
this.setState(prevState => ({
selectedEmployees: _.without(prevState.selectedEmployees, login),
}));
}), () => this.validate());
}

/**
Expand All @@ -229,14 +206,26 @@ class WorkspaceMembersPage extends React.Component {
* @param {Object} item
*/
dismissError(item) {
// TODO: login here also probably will need to change when connecting this to the real api
if (item.pendingAction === 'delete') {
Policy.clearDeleteMemberError(this.props.route.params.policyID, item.login);
} else {
Policy.clearAddMemberError(this.props.route.params.policyID, item.login);
}
}

validate() {
const errors = {};
_.each(this.state.selectedEmployees, (member) => {
if (member !== this.props.policy.owner && member !== this.props.session.email) {
return;
}

errors[member] = this.props.translate('workspace.people.error.cannotRemove');
});

this.setState({errors});
}

/**
* Do not move this or make it an anonymous function it is a method
* so it will not be recreated each time we render an item
Expand All @@ -252,48 +241,44 @@ class WorkspaceMembersPage extends React.Component {
renderItem({
item,
}) {
const canBeRemoved = this.props.policy.owner !== item.login && this.props.session.email !== item.login && item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
return (
<OfflineWithFeedback errorRowStyles={[styles.peopleRowBorderBottom]} onClose={() => this.dismissError(item)} pendingAction={item.pendingAction} errors={item.errors}>
<Hoverable onHoverIn={() => this.willTooltipShowForLogin(item.login, true)} onHoverOut={() => this.setState({showTooltipForLogin: ''})}>
<TouchableOpacity
style={[styles.peopleRow, _.isEmpty(item.errors) && styles.peopleRowBorderBottom, !canBeRemoved && styles.cursorDisabled]}
<TouchableOpacity
style={[styles.peopleRow, (_.isEmpty(item.errors) || this.state.errors[item.login]) && styles.peopleRowBorderBottom]}
onPress={() => this.toggleUser(item.login, item.pendingAction)}
activeOpacity={0.7}
>
<Checkbox
style={[styles.peopleRowCell]}
isChecked={_.contains(this.state.selectedEmployees, item.login)}
onPress={() => this.toggleUser(item.login, item.pendingAction)}
activeOpacity={0.7}
>
<CheckboxWithTooltip
style={[styles.peopleRowCell]}
isChecked={_.contains(this.state.selectedEmployees, item.login)}
disabled={!canBeRemoved}
onPress={() => this.toggleUser(item.login, item.pendingAction)}
toggleTooltip={this.state.showTooltipForLogin === item.login}
text={this.props.translate('workspace.people.error.cannotRemove')}
/>
<View style={styles.flex1}>
<OptionRow
onSelectRow={() => this.toggleUser(item.login, item.pendingAction)}
boldStyle
option={{
text: Str.removeSMSDomain(item.displayName),
alternateText: Str.removeSMSDomain(item.login),
participantsList: [item],
icons: [item.avatar],
keyForList: item.login,
}}
/>
<View style={styles.flex1}>
<OptionRow
onSelectRow={() => this.toggleUser(item.login, item.pendingAction)}
boldStyle
isDisabled={!canBeRemoved}
option={{
text: Str.removeSMSDomain(item.displayName),
alternateText: Str.removeSMSDomain(item.login),
participantsList: [item],
icons: [item.avatar],
keyForList: item.login,
}}
/>
</View>
{(this.props.session.email === item.login || item.role === 'admin') && (
<View style={styles.peopleRowCell}>
<View style={[styles.badge, styles.peopleBadge]}>
<Text style={[styles.peopleBadgeText]}>
{this.props.translate('common.admin')}
</Text>
</View>
</View>
{(this.props.session.email === item.login || item.role === 'admin') && (
<View style={styles.peopleRowCell}>
<View style={[styles.badge, styles.peopleBadge]}>
<Text style={[styles.peopleBadgeText]}>
{this.props.translate('common.admin')}
</Text>
</View>
)}
</TouchableOpacity>
</Hoverable>
</View>
)}
</TouchableOpacity>
{!_.isEmpty(this.state.errors[item.login]) && (
<FormHelpMessage isError message={this.state.errors[item.login]} />
)}
</OfflineWithFeedback>
);
}
Expand Down Expand Up @@ -365,7 +350,7 @@ class WorkspaceMembersPage extends React.Component {
<View style={[styles.peopleRow, styles.ph5, styles.pb3]}>
<View style={[styles.peopleRowCell]}>
<Checkbox
isChecked={this.state.selectedEmployees.length === removableMembers.length && removableMembers.length !== 0}
isChecked={removableMembers.length !== 0 && _.every(removableMembers, member => _.contains(this.state.selectedEmployees, member))}
onPress={() => this.toggleAllUsers()}
/>
</View>
Expand Down