box-ui-elements-mlh
Version:
706 lines (626 loc) • 28.6 kB
Flow
// @flow
import * as React from 'react';
import isEmpty from 'lodash/isEmpty';
import isEqual from 'lodash/isEqual';
import { FormattedMessage, injectIntl } from 'react-intl';
import LoadingIndicatorWrapper from '../../components/loading-indicator/LoadingIndicatorWrapper';
import { Link } from '../../components/link';
import Button from '../../components/button';
import { UpgradeBadge } from '../../components/badge';
import { ITEM_TYPE_WEBLINK } from '../../common/constants';
import Tooltip from '../../components/tooltip';
import { CollaboratorAvatars, CollaboratorList } from '../collaborator-avatars';
import InviteePermissionsMenu from './InviteePermissionsMenu';
import messages from './messages';
import SharedLinkSection from './SharedLinkSection';
import EmailForm from './EmailForm';
import getDefaultPermissionLevel from './utils/defaultPermissionLevel';
import hasRestrictedExternalContacts from './utils/hasRestrictedExternalContacts';
import mergeContacts from './utils/mergeContacts';
import { JUSTIFICATION_CHECKPOINT_EXTERNAL_COLLAB } from './constants';
import type {
contactType as Contact,
getJustificationReasonsResponseType as GetJustificationReasonsResponse,
item as Item,
justificationCheckpointType as JustificationCheckpoint,
USFProps,
} from './flowTypes';
import type { SelectOptionProp } from '../../components/select-field/props';
const SHARED_LINKS_COMMUNITY_URL = 'https://community.box.com/t5/Using-Shared-Links/Creating-Shared-Links/ta-p/19523';
const INVITE_COLLABS_CONTACTS_TYPE = 'inviteCollabsContacts';
const EMAIL_SHARED_LINK_CONTACTS_TYPE = 'emailSharedLinkContacts';
type State = {
classificationLabelId: string,
emailSharedLinkContacts: Array<Contact>,
inviteCollabsContacts: Array<Contact>,
inviteePermissionLevel: string,
isEmailLinkSectionExpanded: boolean,
isFetchingJustificationReasons: boolean,
isInviteSectionExpanded: boolean,
justificationReasons: Array<SelectOptionProp>,
showCollaboratorList: boolean,
};
class UnifiedShareForm extends React.Component<USFProps, State> {
static defaultProps = {
displayInModal: true,
initiallySelectedContacts: [],
createSharedLinkOnLoad: false,
focusSharedLinkOnLoad: false,
restrictedExternalCollabEmails: [],
trackingProps: {
collaboratorListTracking: {},
inviteCollabsEmailTracking: {},
inviteCollabTracking: {},
modalTracking: {},
removeLinkConfirmModalTracking: {},
sharedLinkEmailTracking: {},
sharedLinkTracking: {},
},
};
constructor(props: USFProps) {
super(props);
this.state = {
classificationLabelId: '',
emailSharedLinkContacts: [],
inviteCollabsContacts: props.initiallySelectedContacts,
inviteePermissionLevel: '',
isEmailLinkSectionExpanded: false,
isFetchingJustificationReasons: false,
isInviteSectionExpanded: !!props.initiallySelectedContacts.length,
justificationReasons: [],
showCollaboratorList: false,
};
}
componentDidUpdate(prevProps: USFProps) {
const { isCollabRestrictionJustificationAllowed, item, restrictedExternalCollabEmails } = this.props;
const {
restrictedExternalCollabEmails: prevRestrictedExternalCollabEmails,
isCollabRestrictionJustificationAllowed: prevIsCollabRestrictionJustificationAllowed,
} = prevProps;
const didExternalCollabRestrictionsChange =
!isEqual(restrictedExternalCollabEmails, prevRestrictedExternalCollabEmails) ||
isCollabRestrictionJustificationAllowed !== prevIsCollabRestrictionJustificationAllowed;
if (didExternalCollabRestrictionsChange && this.shouldRequireExternalCollabJustification()) {
this.fetchJustificationReasons(item, JUSTIFICATION_CHECKPOINT_EXTERNAL_COLLAB);
}
}
fetchJustificationReasons = (item: Item, checkpoint: JustificationCheckpoint) => {
const { justificationReasons } = this.state;
const { getJustificationReasons } = this.props;
const hasJustificationReasons = !!justificationReasons.length;
if (!getJustificationReasons || hasJustificationReasons) {
return Promise.resolve();
}
this.setState({ isFetchingJustificationReasons: true });
return getJustificationReasons(item.typedID, checkpoint)
.then(({ classificationLabelId, options }: GetJustificationReasonsResponse) => {
this.setState({
classificationLabelId,
justificationReasons: options.map(({ id, title }) => ({
displayText: title,
value: id,
})),
});
})
.finally(() => {
this.setState({ isFetchingJustificationReasons: false });
});
};
shouldRequireExternalCollabJustification = () => {
const { inviteCollabsContacts } = this.state;
const { isCollabRestrictionJustificationAllowed, restrictedExternalCollabEmails } = this.props;
const hasRestrictedExternalCollabs = hasRestrictedExternalContacts(
inviteCollabsContacts,
restrictedExternalCollabEmails,
);
return hasRestrictedExternalCollabs && isCollabRestrictionJustificationAllowed;
};
handleInviteCollabPillCreate = (pills: Array<SelectOptionProp | Contact>) => {
return this.onPillCreate(INVITE_COLLABS_CONTACTS_TYPE, pills);
};
handleEmailSharedLinkPillCreate = (pills: Array<SelectOptionProp | Contact>) => {
return this.onPillCreate(EMAIL_SHARED_LINK_CONTACTS_TYPE, pills);
};
onToggleSharedLink = (event: SyntheticInputEvent<HTMLInputElement>) => {
const { target } = event;
const {
handleFtuxCloseClick,
onAddLink,
openConfirmModal,
shouldRenderFTUXTooltip,
trackingProps,
} = this.props;
const { sharedLinkTracking } = trackingProps;
const { onToggleLink } = sharedLinkTracking;
if (shouldRenderFTUXTooltip) {
handleFtuxCloseClick();
}
if (target.type === 'checkbox') {
if (target.checked === false) {
openConfirmModal();
} else {
onAddLink();
}
if (onToggleLink) {
onToggleLink(target.checked);
}
}
};
showCollaboratorList = () => {
this.setState({ showCollaboratorList: true });
};
closeCollaboratorList = () => {
this.setState({ showCollaboratorList: false });
};
handleSendInvites = (data: Object) => {
const { inviteePermissions, isCollabRestrictionJustificationAllowed, sendInvites, trackingProps } = this.props;
const { inviteCollabsEmailTracking } = trackingProps;
const { onSendClick } = inviteCollabsEmailTracking;
const { classificationLabelId, inviteePermissionLevel } = this.state;
const defaultPermissionLevel = getDefaultPermissionLevel(inviteePermissions);
const selectedPermissionLevel = inviteePermissionLevel || defaultPermissionLevel;
const { emails, groupIDs, justificationReason, message, restrictedExternalEmails } = data;
let params = {
emails: emails.join(','),
groupIDs: groupIDs.join(','),
emailMessage: message,
permission: selectedPermissionLevel,
numsOfInvitees: emails.length,
numOfInviteeGroups: groupIDs.length,
};
const hasJustificationReason = !!justificationReason;
const hasRestrictedExternalInvitees = !isEmpty(restrictedExternalEmails);
const shouldSubmitJustificationReason =
hasJustificationReason && hasRestrictedExternalInvitees && isCollabRestrictionJustificationAllowed;
if (shouldSubmitJustificationReason) {
params = {
...params,
classificationLabelId,
justificationReason: {
id: justificationReason.value,
title: justificationReason.displayText,
},
};
}
if (onSendClick) {
onSendClick(params);
}
return sendInvites(params);
};
handleSendSharedLink = (data: Object) => {
const { sendSharedLink, trackingProps } = this.props;
const { sharedLinkEmailTracking } = trackingProps;
const { onSendClick } = sharedLinkEmailTracking;
const { emails, groupIDs } = data;
if (onSendClick) {
const params = {
...data,
numsOfRecipients: emails.length,
numOfRecipientGroups: groupIDs.length,
};
onSendClick(params);
}
return sendSharedLink(data);
};
// TODO-AH: Change permission level to use the appropriate flow type
handleInviteePermissionChange = (permissionLevel: string) => {
const { trackingProps } = this.props;
const { inviteCollabTracking } = trackingProps;
const { onInviteePermissionChange } = inviteCollabTracking;
this.setState({ inviteePermissionLevel: permissionLevel });
if (onInviteePermissionChange) {
onInviteePermissionChange(permissionLevel);
}
};
onPillCreate = (type: string, pills: Array<SelectOptionProp | Contact>) => {
// If this is a dropdown select event, we ignore it
// $FlowFixMe
const selectOptionPills = pills.filter(pill => !pill.id);
if (selectOptionPills.length === 0) {
return;
}
const { getContactsByEmail } = this.props;
if (getContactsByEmail) {
const emails = pills.map(pill => pill.value);
// $FlowFixMe
getContactsByEmail({ emails }).then((contacts: Object) => {
if (type === INVITE_COLLABS_CONTACTS_TYPE) {
this.setState(prevState => ({
inviteCollabsContacts: mergeContacts(prevState.inviteCollabsContacts, contacts),
}));
} else if (type === EMAIL_SHARED_LINK_CONTACTS_TYPE) {
this.setState(prevState => ({
emailSharedLinkContacts: mergeContacts(prevState.emailSharedLinkContacts, contacts),
}));
}
});
}
};
openInviteCollaborators = (value: string) => {
const { handleFtuxCloseClick } = this.props;
if (this.state.isInviteSectionExpanded) {
return;
}
// checking the value because IE seems to trigger onInput immediately
// on focus of the contacts field
if (value !== '') {
handleFtuxCloseClick();
this.setState(
{
isInviteSectionExpanded: true,
},
() => {
const {
trackingProps: {
inviteCollabTracking: { onEnterInviteCollabs },
},
} = this.props;
if (onEnterInviteCollabs) {
onEnterInviteCollabs();
}
},
);
}
};
openInviteCollaboratorsSection = () => {
this.setState({
isInviteSectionExpanded: true,
});
};
closeInviteCollaborators = () => {
this.setState({
isInviteSectionExpanded: false,
});
};
openEmailSharedLinkForm = () => {
const { handleFtuxCloseClick } = this.props;
handleFtuxCloseClick();
this.setState({
isEmailLinkSectionExpanded: true,
});
};
closeEmailSharedLinkForm = () => {
this.setState({ isEmailLinkSectionExpanded: false });
};
hasExternalContact = (type: string): boolean => {
const { inviteCollabsContacts, emailSharedLinkContacts } = this.state;
if (type === INVITE_COLLABS_CONTACTS_TYPE) {
return inviteCollabsContacts.some(contact => contact.isExternalUser);
}
if (type === EMAIL_SHARED_LINK_CONTACTS_TYPE) {
return emailSharedLinkContacts.some(contact => contact.isExternalUser);
}
return false;
};
isRemovingAllRestrictedExternalCollabs = (
currentInviteCollabsContacts: Array<Contact>,
newInviteCollabsContacts: Array<Contact>,
) => {
const { restrictedExternalCollabEmails } = this.props;
const hasRestrictedExternalCollabs = hasRestrictedExternalContacts(
currentInviteCollabsContacts,
restrictedExternalCollabEmails,
);
const hasRestrictedExternalCollabsAfterUpdate = hasRestrictedExternalContacts(
newInviteCollabsContacts,
restrictedExternalCollabEmails,
);
return hasRestrictedExternalCollabs && !hasRestrictedExternalCollabsAfterUpdate;
};
updateInviteCollabsContacts = (inviteCollabsContacts: Array<Contact>) => {
const { inviteCollabsContacts: currentInviteCollabsContacts } = this.state;
const { onRemoveAllRestrictedExternalCollabs, setUpdatedContacts } = this.props;
const isRemovingAllRestrictedExternalCollabs = this.isRemovingAllRestrictedExternalCollabs(
currentInviteCollabsContacts,
inviteCollabsContacts,
);
this.setState({
inviteCollabsContacts,
});
if (setUpdatedContacts) {
setUpdatedContacts(inviteCollabsContacts);
}
if (onRemoveAllRestrictedExternalCollabs && isRemovingAllRestrictedExternalCollabs) {
onRemoveAllRestrictedExternalCollabs();
}
};
updateEmailSharedLinkContacts = (emailSharedLinkContacts: Array<Contact>) => {
this.setState({
emailSharedLinkContacts,
});
};
shouldAutoFocusSharedLink = () => {
const { focusSharedLinkOnLoad, sharedLink, sharedLinkLoaded } = this.props;
// if not forcing focus or not a newly added shared link, return false
if (!(focusSharedLinkOnLoad || sharedLink.isNewSharedLink)) {
return false;
}
// otherwise wait until the link data is loaded before focusing
if (!sharedLinkLoaded) {
return false;
}
return true;
};
renderInviteSection() {
const {
canInvite,
collaborationRestrictionWarning,
config,
contactLimit,
getCollaboratorContacts,
getContactAvatarUrl,
handleFtuxCloseClick,
item,
recommendedSharingTooltipCalloutName = null,
restrictedExternalCollabEmails,
sendInvitesError,
shouldRenderFTUXTooltip,
showEnterEmailsCallout = false,
showCalloutForUser = false,
showUpgradeOptions,
submitting,
suggestedCollaborators,
trackingProps,
} = this.props;
const { type } = item;
const { isFetchingJustificationReasons, isInviteSectionExpanded, justificationReasons } = this.state;
const { inviteCollabsEmailTracking, modalTracking } = trackingProps;
const contactsFieldDisabledTooltip =
type === ITEM_TYPE_WEBLINK ? (
<FormattedMessage {...messages.inviteDisabledWeblinkTooltip} />
) : (
<FormattedMessage {...messages.inviteDisabledTooltip} />
);
const inlineNotice = sendInvitesError
? {
type: 'error',
content: sendInvitesError,
}
: {
type: 'warning',
content: collaborationRestrictionWarning,
};
const avatars = this.renderCollaboratorAvatars();
const { ftuxConfirmButtonProps } = modalTracking;
const ftuxTooltipText = (
<div>
<h4 className="ftux-tooltip-title">
<FormattedMessage {...messages.ftuxNewUSMUserTitle} />
</h4>
<p className="ftux-tooltip-body">
<FormattedMessage {...messages.ftuxNewUSMUserBody} />{' '}
<Link className="ftux-tooltip-link" href={SHARED_LINKS_COMMUNITY_URL} target="_blank">
<FormattedMessage {...messages.ftuxLinkText} />
</Link>
</p>
<div className="ftux-tooltip-controls">
<Button className="ftux-tooltip-button" onClick={handleFtuxCloseClick} {...ftuxConfirmButtonProps}>
<FormattedMessage {...messages.ftuxConfirmLabel} />
</Button>
</div>
</div>
);
const ftuxTooltipProps = {
className: 'usm-ftux-tooltip',
// don't want ftux tooltip to show if the recommended sharing tooltip callout is showing
isShown: !recommendedSharingTooltipCalloutName && shouldRenderFTUXTooltip && showCalloutForUser,
position: 'middle-left',
showCloseButton: true,
text: ftuxTooltipText,
theme: 'callout',
};
return (
<>
<Tooltip {...ftuxTooltipProps}>
<div className="invite-collaborator-container" data-testid="invite-collaborator-container">
<EmailForm
config={config}
contactLimit={contactLimit}
contactsFieldAvatars={avatars}
contactsFieldDisabledTooltip={contactsFieldDisabledTooltip}
contactsFieldLabel={<FormattedMessage {...messages.inviteFieldLabel} />}
getContacts={getCollaboratorContacts}
getContactAvatarUrl={getContactAvatarUrl}
inlineNotice={inlineNotice}
isContactsFieldEnabled={canInvite}
isExpanded={isInviteSectionExpanded}
isFetchingJustificationReasons={isFetchingJustificationReasons}
isExternalUserSelected={this.hasExternalContact(INVITE_COLLABS_CONTACTS_TYPE)}
isRestrictionJustificationEnabled={this.shouldRequireExternalCollabJustification()}
justificationReasons={justificationReasons}
onContactInput={this.openInviteCollaborators}
onPillCreate={this.handleInviteCollabPillCreate}
onRequestClose={this.closeInviteCollaborators}
onSubmit={this.handleSendInvites}
openInviteCollaboratorsSection={this.openInviteCollaboratorsSection}
recommendedSharingTooltipCalloutName={recommendedSharingTooltipCalloutName}
restrictedExternalEmails={restrictedExternalCollabEmails}
showEnterEmailsCallout={showEnterEmailsCallout}
submitting={submitting}
selectedContacts={this.state.inviteCollabsContacts}
suggestedCollaborators={suggestedCollaborators}
updateSelectedContacts={this.updateInviteCollabsContacts}
{...inviteCollabsEmailTracking}
>
{this.renderInviteePermissionsDropdown()}
{isInviteSectionExpanded && showUpgradeOptions && this.renderUpgradeLinkDescription()}
</EmailForm>
</div>
</Tooltip>
</>
);
}
renderCollaboratorAvatars() {
const { collaboratorsList, canInvite, currentUserID, item, trackingProps } = this.props;
const { modalTracking } = trackingProps;
let avatarsContent = null;
if (collaboratorsList) {
const { collaborators } = collaboratorsList;
const { hideCollaborators = true } = item;
const canShowCollaboratorAvatars = hideCollaborators ? canInvite : true;
// filter out the current user by comparing to the ItemCollabRecord ID field
avatarsContent = canShowCollaboratorAvatars && (
<CollaboratorAvatars
collaborators={collaborators.filter(collaborator => String(collaborator.userID) !== currentUserID)}
onClick={this.showCollaboratorList}
containerAttributes={modalTracking.collaboratorAvatarsProps}
/>
);
}
return avatarsContent;
}
renderUpgradeLinkDescription() {
const { trackingProps = {} } = this.props;
const { inviteCollabsEmailTracking = {} } = trackingProps;
const { upgradeLinkProps = {} } = inviteCollabsEmailTracking;
return (
<div className="upgrade-description">
<UpgradeBadge type="warning" />
<FormattedMessage
values={{
upgradeGetMoreAccessControlsLink: (
<Link className="upgrade-link" href="/upgrade" {...upgradeLinkProps}>
<FormattedMessage {...messages.upgradeGetMoreAccessControlsLink} />
</Link>
),
}}
{...messages.upgradeGetMoreAccessControlsDescription}
/>
</div>
);
}
renderInviteePermissionsDropdown() {
const { inviteePermissions, item, submitting, canInvite, trackingProps } = this.props;
const { type } = item;
const { inviteCollabTracking } = trackingProps;
return (
inviteePermissions && (
<InviteePermissionsMenu
disabled={!canInvite || submitting}
inviteePermissionsButtonProps={inviteCollabTracking.inviteePermissionsButtonProps}
inviteePermissionLevel={this.state.inviteePermissionLevel}
inviteePermissions={inviteePermissions}
changeInviteePermissionLevel={this.handleInviteePermissionChange}
itemType={type}
/>
)
);
}
renderCollaboratorList() {
const { item, collaboratorsList, trackingProps } = this.props;
const { name, type } = item;
const { collaboratorListTracking } = trackingProps;
let listContent = null;
if (collaboratorsList) {
const { collaborators } = collaboratorsList;
listContent = (
<CollaboratorList
itemName={name}
itemType={type}
onDoneClick={this.closeCollaboratorList}
item={item}
collaborators={collaborators}
trackingProps={collaboratorListTracking}
/>
);
}
return listContent;
}
render() {
// Shared link section props
const {
allShareRestrictionWarning,
changeSharedLinkAccessLevel,
createSharedLinkOnLoad,
changeSharedLinkPermissionLevel,
config,
displayInModal,
focusSharedLinkOnLoad,
getSharedLinkContacts,
getContactAvatarUrl,
intl,
isFetching,
item,
onAddLink,
onCopyError,
onCopyInit,
onCopySuccess,
onDismissTooltip = () => {},
onSettingsClick,
sendSharedLinkError,
sharedLink,
showEnterEmailsCallout = false,
showSharedLinkSettingsCallout = false,
submitting,
tooltips = {},
trackingProps,
} = this.props;
const { sharedLinkTracking, sharedLinkEmailTracking } = trackingProps;
const { isEmailLinkSectionExpanded, isInviteSectionExpanded, showCollaboratorList } = this.state;
// Only show the restriction warning on the main page of the USM where the email and share link option is available
const showShareRestrictionWarning =
!isEmailLinkSectionExpanded &&
!isInviteSectionExpanded &&
!showCollaboratorList &&
allShareRestrictionWarning;
return (
<div className={displayInModal ? '' : 'be bdl-UnifiedShareForm'}>
<LoadingIndicatorWrapper isLoading={isFetching} hideContent>
{showShareRestrictionWarning && allShareRestrictionWarning}
{!isEmailLinkSectionExpanded && !showCollaboratorList && this.renderInviteSection()}
{!isEmailLinkSectionExpanded && !isInviteSectionExpanded && !showCollaboratorList && (
<SharedLinkSection
addSharedLink={onAddLink}
autofocusSharedLink={this.shouldAutoFocusSharedLink()}
autoCreateSharedLink={createSharedLinkOnLoad}
config={config}
triggerCopyOnLoad={createSharedLinkOnLoad && focusSharedLinkOnLoad}
changeSharedLinkAccessLevel={changeSharedLinkAccessLevel}
changeSharedLinkPermissionLevel={changeSharedLinkPermissionLevel}
intl={intl}
item={item}
itemType={item.type}
onDismissTooltip={onDismissTooltip}
onEmailSharedLinkClick={this.openEmailSharedLinkForm}
onSettingsClick={onSettingsClick}
onToggleSharedLink={this.onToggleSharedLink}
onCopyInit={onCopyInit}
onCopySuccess={onCopySuccess}
onCopyError={onCopyError}
sharedLink={sharedLink}
showSharedLinkSettingsCallout={showSharedLinkSettingsCallout}
submitting={submitting || isFetching}
trackingProps={sharedLinkTracking}
tooltips={tooltips}
/>
)}
{isEmailLinkSectionExpanded && !showCollaboratorList && (
<EmailForm
contactsFieldLabel={<FormattedMessage {...messages.sendSharedLinkFieldLabel} />}
getContactAvatarUrl={getContactAvatarUrl}
getContacts={getSharedLinkContacts}
inlineNotice={{
type: 'error',
content: sendSharedLinkError,
}}
isContactsFieldEnabled
isExpanded
isExternalUserSelected={this.hasExternalContact(EMAIL_SHARED_LINK_CONTACTS_TYPE)}
onPillCreate={this.handleEmailSharedLinkPillCreate}
onRequestClose={this.closeEmailSharedLinkForm}
onSubmit={this.handleSendSharedLink}
showEnterEmailsCallout={showEnterEmailsCallout}
submitting={submitting}
selectedContacts={this.state.emailSharedLinkContacts}
updateSelectedContacts={this.updateEmailSharedLinkContacts}
{...sharedLinkEmailTracking}
/>
)}
{showCollaboratorList && this.renderCollaboratorList()}
</LoadingIndicatorWrapper>
</div>
);
}
}
export { UnifiedShareForm as UnifiedShareFormBase };
export default injectIntl(UnifiedShareForm);