Skip to content

Commit

Permalink
Merge pull request #23702 from Expensify/neil-secondary-members
Browse files Browse the repository at this point in the history
Show info about workspace members added by secondary logins
  • Loading branch information
neil-marcellini authored Oct 27, 2023
2 parents 7da77b2 + 7eb0ba5 commit 32603b6
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 28 deletions.
73 changes: 73 additions & 0 deletions src/components/MessagesRow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import _ from 'underscore';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import styles from '../styles/styles';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import DotIndicatorMessage from './DotIndicatorMessage';
import Tooltip from './Tooltip';
import CONST from '../CONST';
import * as StyleUtils from '../styles/StyleUtils';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import stylePropTypes from '../styles/stylePropTypes';
import useLocalize from '../hooks/useLocalize';

const propTypes = {
/* The messages to display */
messages: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))])),

/* The type of message, 'error' shows a red dot, 'success' shows a green dot */
type: PropTypes.oneOf(['error', 'success']).isRequired,

/** A function to run when the X button next to the message is clicked */
onClose: PropTypes.func,

/** Additional style object for the container */
containerStyles: stylePropTypes,

/** Whether we can dismiss the messages */
canDismiss: PropTypes.bool,
};

const defaultProps = {
messages: {},
onClose: () => {},
containerStyles: [],
canDismiss: true,
};

function MessagesRow({messages, type, onClose, containerStyles, canDismiss}) {
const {translate} = useLocalize();
if (_.isEmpty(messages)) {
return null;
}

return (
<View style={StyleUtils.combineStyles(styles.flexRow, styles.alignItemsCenter, containerStyles)}>
<DotIndicatorMessage
style={[styles.flex1]}
messages={messages}
type={type}
/>
{canDismiss && (
<Tooltip text={translate('common.close')}>
<PressableWithoutFeedback
onPress={onClose}
style={[styles.touchableButtonImage]}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
accessibilityLabel={translate('common.close')}
>
<Icon src={Expensicons.Close} />
</PressableWithoutFeedback>
</Tooltip>
)}
</View>
);
}

MessagesRow.propTypes = propTypes;
MessagesRow.defaultProps = defaultProps;
MessagesRow.displayName = 'MessagesRow';

export default MessagesRow;
34 changes: 8 additions & 26 deletions src/components/OfflineWithFeedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,9 @@ import PropTypes from 'prop-types';
import CONST from '../CONST';
import stylePropTypes from '../styles/stylePropTypes';
import styles from '../styles/styles';
import Tooltip from './Tooltip';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import * as StyleUtils from '../styles/StyleUtils';
import DotIndicatorMessage from './DotIndicatorMessage';
import shouldRenderOffscreen from '../libs/shouldRenderOffscreen';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import useLocalize from '../hooks/useLocalize';
import MessagesRow from './MessagesRow';
import useNetwork from '../hooks/useNetwork';

/**
Expand Down Expand Up @@ -97,7 +92,6 @@ function applyStrikeThrough(children) {
}

function OfflineWithFeedback(props) {
const {translate} = useLocalize();
const {isOffline} = useNetwork();

const hasErrors = !_.isEmpty(props.errors);
Expand Down Expand Up @@ -128,25 +122,13 @@ function OfflineWithFeedback(props) {
</View>
)}
{props.shouldShowErrorMessages && hasErrorMessages && (
<View style={StyleUtils.combineStyles(styles.offlineFeedback.error, props.errorRowStyles)}>
<DotIndicatorMessage
style={[styles.flex1]}
messages={errorMessages}
type="error"
/>
{props.canDismissError && (
<Tooltip text={translate('common.close')}>
<PressableWithoutFeedback
onPress={props.onClose}
style={[styles.touchableButtonImage]}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
accessibilityLabel={translate('common.close')}
>
<Icon src={Expensicons.Close} />
</PressableWithoutFeedback>
</Tooltip>
)}
</View>
<MessagesRow
messages={errorMessages}
type="error"
onClose={props.onClose}
containerStyles={props.errorRowStyles}
canDismiss={props.canDismissError}
/>
)}
</View>
);
Expand Down
10 changes: 8 additions & 2 deletions src/components/SelectionList/BaseListItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import UserListItem from './UserListItem';
import RadioListItem from './RadioListItem';
import OfflineWithFeedback from '../OfflineWithFeedback';
import CONST from '../../CONST';
import useLocalize from '../../hooks/useLocalize';
import Text from '../Text';

function BaseListItem({
item,
Expand All @@ -23,6 +25,7 @@ function BaseListItem({
onSelectRow,
onDismissError = () => {},
}) {
const {translate} = useLocalize();
const isUserItem = lodashGet(item, 'icons.length', 0) > 0;
const ListItem = isUserItem ? UserListItem : RadioListItem;

Expand Down Expand Up @@ -76,15 +79,13 @@ function BaseListItem({
</View>
</View>
)}

<ListItem
item={item}
isFocused={isFocused}
isDisabled={isDisabled}
onSelectRow={onSelectRow}
showTooltip={showTooltip}
/>

{!canSelectMultiple && item.isSelected && (
<View
style={[styles.flexRow, styles.alignItemsCenter, styles.ml3]}
Expand All @@ -99,6 +100,11 @@ function BaseListItem({
</View>
)}
</View>
{Boolean(item.invitedSecondaryLogin) && (
<Text style={[styles.ml9, styles.ph5, styles.pb3, styles.textLabelSupporting]}>
{translate('workspace.people.invitedBySecondaryLogin', {secondaryLogin: item.invitedSecondaryLogin})}
</Text>
)}
</PressableWithFeedback>
</OfflineWithFeedback>
);
Expand Down
2 changes: 2 additions & 0 deletions src/components/SelectionList/BaseSelectionList.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function BaseSelectionList({
headerMessage = '',
confirmButtonText = '',
onConfirm,
headerContent,
footerContent,
showScrollIndicator = false,
showLoadingPlaceholder = false,
Expand Down Expand Up @@ -391,6 +392,7 @@ function BaseSelectionList({
<Text style={[styles.textLabel, styles.colorMuted]}>{headerMessage}</Text>
</View>
)}
{Boolean(headerContent) && headerContent}
{flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? (
<OptionsListSkeletonView shouldAnimate />
) : (
Expand Down
3 changes: 3 additions & 0 deletions src/components/SelectionList/selectionListPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ const propTypes = {
/** A ref to forward to the TextInput */
inputRef: PropTypes.oneOfType([PropTypes.object]),

/** Custom content to display in the header */
headerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),

/** Custom content to display in the footer */
footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
};
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,8 @@ export default {
cannotRemove: 'You cannot remove yourself or the workspace owner.',
genericRemove: 'There was a problem removing that workspace member.',
},
addedWithPrimary: 'Some users were added with their primary logins.',
invitedBySecondaryLogin: ({secondaryLogin}) => `Added by secondary login ${secondaryLogin}.`,
},
card: {
header: 'Unlock free Expensify Cards',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,8 @@ export default {
cannotRemove: 'No puedes eliminarte ni a ti mismo ni al dueño del espacio de trabajo.',
genericRemove: 'Ha ocurrido un problema al eliminar al miembro del espacio de trabajo.',
},
addedWithPrimary: 'Se agregaron algunos usuarios con sus nombres de usuario principales.',
invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`,
},
card: {
header: 'Desbloquea Tarjetas Expensify gratis',
Expand Down
38 changes: 38 additions & 0 deletions src/libs/actions/Policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ Onyx.connect({
},
});

let allPolicyMembers;
Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY_MEMBERS,
waitForCollectionCallback: true,
callback: (val) => (allPolicyMembers = val),
});

let lastAccessedWorkspacePolicyID = null;
Onyx.connect({
key: ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID,
Expand Down Expand Up @@ -249,6 +256,27 @@ function removeMembers(accountIDs, policyID) {
})),
];

// If the policy has primaryLoginsInvited, then it displays informative messages on the members page about which primary logins were added by secondary logins.
// If we delete all these logins then we should clear the informative messages since they are no longer relevant.
if (!_.isEmpty(policy.primaryLoginsInvited)) {
// Take the current policy members and remove them optimistically
const policyMemberAccountIDs = _.map(allPolicyMembers[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`], (value, key) => Number(key));
const remainingMemberAccountIDs = _.difference(policyMemberAccountIDs, accountIDs);
const remainingLogins = PersonalDetailsUtils.getLoginsByAccountIDs(remainingMemberAccountIDs);
const invitedPrimaryToSecondaryLogins = _.invert(policy.primaryLoginsInvited);

// Then, if no remaining members exist that were invited by a secondary login, clear the informative messages
if (!_.some(remainingLogins, (remainingLogin) => Boolean(invitedPrimaryToSecondaryLogins[remainingLogin]))) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
primaryLoginsInvited: null,
},
});
}
}

const successData = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand Down Expand Up @@ -1306,6 +1334,15 @@ function clearErrors(policyID) {
hideWorkspaceAlertMessage(policyID);
}

/**
* Dismiss the informative messages about which policy members were added with primary logins when invited with their secondary login.
*
* @param {String} policyID
*/
function dismissAddedWithPrimaryMessages(policyID) {
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {primaryLoginsInvited: null});
}

/**
* @param {String} policyID
* @param {String} category
Expand Down Expand Up @@ -1349,6 +1386,7 @@ export {
removeWorkspace,
setWorkspaceInviteMembersDraft,
clearErrors,
dismissAddedWithPrimaryMessages,
openDraftWorkspaceRequest,
buildOptimisticPolicyRecentlyUsedCategories,
createDraftInitialWorkspace,
Expand Down
20 changes: 20 additions & 0 deletions src/pages/workspace/WorkspaceMembersPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import * as PolicyUtils from '../../libs/PolicyUtils';
import usePrevious from '../../hooks/usePrevious';
import Log from '../../libs/Log';
import * as PersonalDetailsUtils from '../../libs/PersonalDetailsUtils';
import MessagesRow from '../../components/MessagesRow';
import SelectionList from '../../components/SelectionList';
import Text from '../../components/Text';
import * as Browser from '../../libs/Browser';
Expand Down Expand Up @@ -290,6 +291,7 @@ function WorkspaceMembersPage(props) {
const currentUserLogin = lodashGet(props.currentUserPersonalDetails, 'login');
const policyID = lodashGet(props.route, 'params.policyID');
const policyName = lodashGet(props.policy, 'name');
const invitedPrimaryToSecondaryLogins = _.invert(props.policy.primaryLoginsInvited);

const getMemberOptions = () => {
let result = [];
Expand Down Expand Up @@ -367,6 +369,9 @@ function WorkspaceMembersPage(props) {
],
errors: policyMember.errors,
pendingAction: policyMember.pendingAction,

// Note which secondary login was used to invite this primary login
invitedSecondaryLogin: invitedPrimaryToSecondaryLogins[details.login] || '',
});
});

Expand All @@ -383,6 +388,20 @@ function WorkspaceMembersPage(props) {
return searchValue.trim() && !data.length ? props.translate('workspace.common.memberNotFound') : '';
};

const getHeaderContent = () => {
if (_.isEmpty(invitedPrimaryToSecondaryLogins)) {
return null;
}
return (
<MessagesRow
type="success"
messages={{0: props.translate('workspace.people.addedWithPrimary')}}
containerStyles={[styles.pb5, styles.ph5]}
onClose={() => Policy.dismissAddedWithPrimaryMessages(policyID)}
/>
);
};

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
Expand Down Expand Up @@ -447,6 +466,7 @@ function WorkspaceMembersPage(props) {
textInputValue={searchValue}
onChangeText={setSearchValue}
headerMessage={getHeaderMessage()}
headerContent={getHeaderContent()}
onSelectRow={(item) => toggleUser(item.accountID)}
onSelectAll={() => toggleAllUsers(data)}
onDismissError={dismissError}
Expand Down
4 changes: 4 additions & 0 deletions src/styles/utilities/spacing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ export default {
marginLeft: 32,
},

ml9: {
marginLeft: 36,
},

ml10: {
marginLeft: 40,
},
Expand Down

0 comments on commit 32603b6

Please sign in to comment.