box-ui-elements-mlh
Version:
614 lines (563 loc) • 21.2 kB
JavaScript
// @flow
import { getTypedFileId, getTypedFolderId, isGSuiteExtension } from '../../../utils/file';
import { checkIsExternalUser } from '../../../utils/parseEmails';
import {
ACCESS_COLLAB,
ACCESS_COMPANY,
ACCESS_NONE,
ACCESS_OPEN,
INVITEE_ROLE_EDITOR,
PERMISSION_CAN_DOWNLOAD,
PERMISSION_CAN_PREVIEW,
STATUS_ACCEPTED,
STATUS_INACTIVE,
TYPE_FOLDER,
} from '../../../constants';
import {
ALLOWED_ACCESS_LEVELS,
ANYONE_IN_COMPANY,
ANYONE_WITH_LINK,
CAN_VIEW_DOWNLOAD,
CAN_VIEW_ONLY,
COLLAB_GROUP_TYPE,
COLLAB_USER_TYPE,
DISABLED_REASON_ACCESS_POLICY,
DISABLED_REASON_MALICIOUS_CONTENT,
PEOPLE_IN_ITEM,
} from '../constants';
import {
bdlDarkBlue50,
bdlGray20,
bdlGreenLight50,
bdlLightBlue50,
bdlOrange50,
bdlPurpleRain50,
bdlWatermelonRed50,
bdlYellow50,
} from '../../../styles/variables';
import {
CLASSIFICATION_COLOR_ID_0,
CLASSIFICATION_COLOR_ID_1,
CLASSIFICATION_COLOR_ID_2,
CLASSIFICATION_COLOR_ID_3,
CLASSIFICATION_COLOR_ID_4,
CLASSIFICATION_COLOR_ID_5,
CLASSIFICATION_COLOR_ID_6,
CLASSIFICATION_COLOR_ID_7,
} from '../../classification/constants';
import type {
AvatarURLMap,
ContentSharingCollaborationsRequest,
ContentSharingItemAPIResponse,
ContentSharingItemDataType,
ContentSharingUserDataType,
ConvertCollabOptions,
SharedLinkSettingsOptions,
} from '../../../elements/content-sharing/types';
import type {
BoxItemPermission,
Collaborations,
GroupCollection,
SharedLink,
User,
UserCollection,
} from '../../../common/types/core';
import type {
accessLevelsDisabledReasonType,
allowedAccessLevelsType,
collaboratorsListType,
collaboratorType,
contactType,
InviteCollaboratorsRequest,
permissionLevelType,
} from '../flowTypes';
/**
* The following constants are used for converting API requests
* and responses into objects expected by the USM, and vice versa
*/
export const API_TO_USM_ACCESS_LEVEL_MAP = {
[ACCESS_COLLAB]: PEOPLE_IN_ITEM,
[ACCESS_COMPANY]: ANYONE_IN_COMPANY,
[ACCESS_OPEN]: ANYONE_WITH_LINK,
[ACCESS_NONE]: '',
};
export const API_TO_USM_PERMISSION_LEVEL_MAP = {
[PERMISSION_CAN_DOWNLOAD]: CAN_VIEW_DOWNLOAD,
[PERMISSION_CAN_PREVIEW]: CAN_VIEW_ONLY,
};
export const USM_TO_API_ACCESS_LEVEL_MAP = {
[ANYONE_IN_COMPANY]: ACCESS_COMPANY,
[ANYONE_WITH_LINK]: ACCESS_OPEN,
[PEOPLE_IN_ITEM]: ACCESS_COLLAB,
};
export const USM_TO_API_PERMISSION_LEVEL_MAP = {
[CAN_VIEW_DOWNLOAD]: PERMISSION_CAN_DOWNLOAD,
[CAN_VIEW_ONLY]: PERMISSION_CAN_PREVIEW,
};
const API_TO_USM_CLASSIFICATION_COLORS_MAP = {
[bdlYellow50]: CLASSIFICATION_COLOR_ID_0,
[bdlOrange50]: CLASSIFICATION_COLOR_ID_1,
[bdlWatermelonRed50]: CLASSIFICATION_COLOR_ID_2,
[bdlPurpleRain50]: CLASSIFICATION_COLOR_ID_3,
[bdlLightBlue50]: CLASSIFICATION_COLOR_ID_4,
[bdlDarkBlue50]: CLASSIFICATION_COLOR_ID_5,
[bdlGreenLight50]: CLASSIFICATION_COLOR_ID_6,
[bdlGray20]: CLASSIFICATION_COLOR_ID_7,
};
const APP_USERS_DOMAIN_REGEXP = new RegExp('boxdevedition.com');
/**
* Convert access levels disabled reasons into USM format.
*
* @param {{ [string]: string }} disabledReasons
* @returns {accessLevelsDisabledReasonType | null}
*/
export const convertAccessLevelsDisabledReasons = (disabledReasons?: {
[string]: typeof DISABLED_REASON_ACCESS_POLICY | typeof DISABLED_REASON_MALICIOUS_CONTENT | null,
}): accessLevelsDisabledReasonType | null => {
if (!disabledReasons) return null;
const convertedReasons = {};
Object.entries(disabledReasons).forEach(([level, reason]) => {
convertedReasons[API_TO_USM_ACCESS_LEVEL_MAP[level]] = reason;
});
return convertedReasons;
};
/**
* Convert allowed access levels into USM format.
*
* @param {Array<string>} [levelsFromAPI]
* @returns {allowedAccessLevelsType | null}
*/
export const convertAllowedAccessLevels = (levelsFromAPI?: Array<string>): allowedAccessLevelsType | null => {
if (!levelsFromAPI) return null;
const convertedLevels = {
peopleInThisItem: false,
peopleInYourCompany: false,
peopleWithTheLink: false,
};
levelsFromAPI.forEach(level => {
convertedLevels[API_TO_USM_ACCESS_LEVEL_MAP[level]] = true;
});
return convertedLevels;
};
/**
* Convert shared link permission into USM format, taking file type limitations into account.
*
* @param {string} effectivePermissionFromAPI
* @param {string} extension
* @returns {permissionLevelType}
*/
export const convertEffectiveSharedLinkPermission = (
effectivePermissionFromAPI: string,
extension: string,
): permissionLevelType => {
return isGSuiteExtension(extension) ? CAN_VIEW_ONLY : API_TO_USM_PERMISSION_LEVEL_MAP[effectivePermissionFromAPI];
};
/**
* Convert isDownloadSettingAvailable into a value used by the USM, taking into account file type limitations.
*
* @param {boolean} isDownloadSettingAvailableFromAPI
* @param {string} extension
* @returns {boolean}
*/
export const convertIsDownloadSettingAvailable = (
isDownloadSettingAvailableFromAPI?: boolean,
extension: string,
): boolean | void => {
if (isDownloadSettingAvailableFromAPI === undefined) {
return undefined;
}
return !isGSuiteExtension(extension) && isDownloadSettingAvailableFromAPI;
};
/**
* Convert a response from the Item API to the object that the USM expects.
*
* @param {BoxItem} itemAPIData
* @returns {ContentSharingItemDataType} Object containing item and shared link information
*/
export const convertItemResponse = (itemAPIData: ContentSharingItemAPIResponse): ContentSharingItemDataType => {
const {
allowed_invitee_roles,
allowed_shared_link_access_levels,
allowed_shared_link_access_levels_disabled_reasons,
classification,
id,
description,
extension,
name,
owned_by: { id: ownerID, login: ownerEmail },
permissions,
shared_link,
shared_link_features: { download_url: isDirectLinkAvailable, password: isPasswordAvailable },
type,
} = itemAPIData;
const {
can_download: isDownloadSettingAvailableFromApi,
can_invite_collaborator: canInvite,
can_preview: isPreviewAllowed,
can_set_share_access: canChangeAccessLevel,
can_share: itemShare,
} = permissions;
// Convert classification data for the item if available
let classificationData = {};
if (classification) {
const { color, definition, name: classificationName } = classification;
classificationData = {
bannerPolicy: {
body: definition,
colorID: API_TO_USM_CLASSIFICATION_COLORS_MAP[color],
},
classification: classificationName,
};
}
const isEditAllowed = allowed_invitee_roles.indexOf(INVITEE_ROLE_EDITOR) !== -1;
// The "canInvite" property is necessary even if the item does not have a shared link,
// because it allows users to invite individual collaborators.
let sharedLink = { canInvite: !!canInvite };
if (shared_link) {
const {
download_url: directLink,
effective_access,
effective_permission,
is_password_enabled: isPasswordEnabled,
password,
unshared_at: expirationTimestamp,
url,
vanity_name: vanityName,
} = shared_link;
const isDownloadSettingAvailable = convertIsDownloadSettingAvailable(
isDownloadSettingAvailableFromApi,
extension,
);
const accessLevel = effective_access ? API_TO_USM_ACCESS_LEVEL_MAP[effective_access] : '';
const permissionLevel = effective_permission
? convertEffectiveSharedLinkPermission(effective_permission, extension)
: null;
const isDownloadAllowed = permissionLevel === API_TO_USM_PERMISSION_LEVEL_MAP.can_download;
const canChangeDownload =
canChangeAccessLevel && isDownloadSettingAvailable && effective_access !== ACCESS_COLLAB; // access must be "company" or "open"
const canChangePassword = canChangeAccessLevel && isPasswordAvailable;
const canChangeExpiration = canChangeAccessLevel && isEditAllowed;
sharedLink = {
accessLevel,
accessLevelsDisabledReason:
convertAccessLevelsDisabledReasons(allowed_shared_link_access_levels_disabled_reasons) || {},
allowedAccessLevels: convertAllowedAccessLevels(allowed_shared_link_access_levels) || ALLOWED_ACCESS_LEVELS, // show all access levels by default
canChangeAccessLevel,
canChangeDownload,
canChangeExpiration,
canChangePassword,
canChangeVanityName: false, // vanity URLs cannot be set via the API
canInvite: !!canInvite,
directLink,
expirationTimestamp: expirationTimestamp ? new Date(expirationTimestamp).getTime() : null, // convert to milliseconds
isDirectLinkAvailable,
isDownloadAllowed,
isDownloadAvailable: isDownloadSettingAvailable,
isDownloadEnabled: isDownloadAllowed,
isDownloadSettingAvailable,
isEditAllowed,
isNewSharedLink: false,
isPasswordAvailable,
isPasswordEnabled,
isPreviewAllowed,
password,
permissionLevel,
url,
vanityName: vanityName || '',
};
}
return {
item: {
canUserSeeClassification: !!classification,
description,
extension,
grantedPermissions: {
itemShare: !!itemShare,
},
hideCollaborators: false, // to do: connect to Collaborations API
id,
name,
ownerEmail, // the owner email is used to determine whether collaborators are external
ownerID, // the owner ID is used to determine whether external collaborator badges should be shown
permissions, // the original permissions are necessary for PUT requests to the Item API
type,
typedID: type === TYPE_FOLDER ? getTypedFolderId(id) : getTypedFileId(id),
...classificationData,
},
sharedLink,
};
};
/**
* Convert a response from the User API into the object that the USM expects.
*
* @param {User} userAPIData
* @returns {ContentSharingUserDataType} Object containing user and enterprise information
*/
export const convertUserResponse = (userAPIData: User): ContentSharingUserDataType => {
const { enterprise, hostname, id } = userAPIData;
return {
id,
userEnterpriseData: {
enterpriseName: enterprise ? enterprise.name : '',
serverURL: hostname ? `${hostname}v/` : '',
},
};
};
/**
* Create a shared link permissions object for the API based on a USM permission level.
*
* @param {string} newSharedLinkPermissionLevel
* @returns {$Shape<BoxItemPermission>} Object containing shared link permissions
*/
export const convertSharedLinkPermissions = (newSharedLinkPermissionLevel: string): $Shape<BoxItemPermission> => {
const sharedLinkPermissions = {};
Object.keys(USM_TO_API_PERMISSION_LEVEL_MAP).forEach(level => {
if (level === newSharedLinkPermissionLevel) {
sharedLinkPermissions[USM_TO_API_PERMISSION_LEVEL_MAP[level]] = true;
} else {
sharedLinkPermissions[USM_TO_API_PERMISSION_LEVEL_MAP[level]] = false;
}
});
return sharedLinkPermissions;
};
/**
* Convert a shared link settings object from the USM into the format that the API expects.
* This function compares the provided access level to both API and internal USM access level constants, to accommodate two potential flows:
* - Changing the settings for a shared link right after the shared link has been created. The access level is saved directly from the data
* returned by the API, so it is in API format.
* - Changing the settings for a shared link in any other scenario. The access level is saved from the initial calls to the Item API and
* convertItemResponse, so it is in internal USM format.
*
* @param {SharedLinkSettingsOptions} newSettings
* @param {accessLevel} string
* @param {serverURL} string
* @returns {$Shape<SharedLink>}
*/
export const convertSharedLinkSettings = (
newSettings: SharedLinkSettingsOptions,
accessLevel: string,
isDownloadAvailable: boolean,
serverURL: string,
): $Shape<SharedLink> => {
const {
expirationTimestamp,
isDownloadEnabled: can_download,
isExpirationEnabled,
isPasswordEnabled,
password,
vanityName,
} = newSettings;
const convertedSettings: $Shape<SharedLink> = {
unshared_at: expirationTimestamp && isExpirationEnabled ? new Date(expirationTimestamp).toISOString() : null,
vanity_url: serverURL && vanityName ? `${serverURL}${vanityName}` : '',
};
// Download permissions can only be set on "company" or "open" shared links.
if (![ACCESS_COLLAB, PEOPLE_IN_ITEM].includes(accessLevel)) {
const permissions: BoxItemPermission = { can_preview: !can_download };
if (isDownloadAvailable) {
permissions.can_download = can_download;
}
convertedSettings.permissions = permissions;
}
/**
* This block covers the following cases:
* - Setting a new password: "isPasswordEnabled" is true, and "password" is a non-empty string.
* - Removing a password: "isPasswordEnabled" is false, and "password" is an empty string.
* The API only accepts non-empty strings and null values, so the empty string must be converted to null.
*
* Other notes:
* - Passwords can only be set on "open" shared links.
* - Attempting to set the password field on any other type of shared link will throw a 400 error.
* - When other settings are updated, and a password has already been set, the SharedLinkSettingsModal
* returns password = '' and isPasswordEnabled = true. In these cases, the password should *not*
* be converted to null, because that would remove the existing password.
*/
if ([ANYONE_WITH_LINK, ACCESS_OPEN].includes(accessLevel)) {
if (isPasswordEnabled && !!password) {
convertedSettings.password = password;
} else if (!isPasswordEnabled) {
convertedSettings.password = null;
}
}
return convertedSettings;
};
/**
* Convert a collaborator.
* Note: We do not retrieve the avatar URL of collaborators right after inviting them,
* so the avatar fields (hasCustomAvatar and imageURL) are not set in that case.
*
* @param {ConvertCollabOptions} options
* @returns {collaboratorType | null} Object containing a collaborator
*/
export const convertCollab = ({
collab,
avatarURLMap,
ownerEmail,
isCurrentUserOwner = false,
}: ConvertCollabOptions): collaboratorType | null => {
if (!collab || collab.status !== STATUS_ACCEPTED) return null;
const ownerEmailDomain = ownerEmail && /@/.test(ownerEmail) ? ownerEmail.split('@')[1] : null;
const {
accessible_by: { id: userID, login: email, name, type },
id: collabID,
expires_at: executeAt,
role,
} = collab;
const avatarURL = avatarURLMap ? avatarURLMap[userID] : undefined;
const convertedCollab: collaboratorType = {
collabID: parseInt(collabID, 10),
email,
hasCustomAvatar: !!avatarURL,
imageURL: avatarURL,
isExternalCollab: checkIsExternalUser(isCurrentUserOwner, ownerEmailDomain, email),
name,
translatedRole: `${role[0].toUpperCase()}${role.slice(1)}`, // capitalize the user's role
type,
userID: parseInt(userID, 10),
};
if (executeAt) {
convertedCollab.expiration = { executeAt };
}
return convertedCollab;
};
/**
* Convert a response from the Item Collaborations API into the object that the USM expects.
*
* @param {Collaborations} collabsAPIData
* @param {AvatarURLMap | null} avatarURLMap
* @param {string | null | undefined} ownerEmail
* @param {boolean} isCurrentUserOwner
* @returns {collaboratorsListType} Object containing an array of collaborators
*/
export const convertCollabsResponse = (
collabsAPIData: Collaborations,
avatarURLMap: ?AvatarURLMap,
ownerEmail: ?string,
isCurrentUserOwner: boolean,
): collaboratorsListType => {
const { entries = [] } = collabsAPIData;
if (!entries.length) return { collaborators: [] };
const collaborators = [];
entries
// Only show accepted collaborations
.filter(collab => collab.status === STATUS_ACCEPTED)
.forEach(collab => {
const convertedCollab = convertCollab({ collab, avatarURLMap, ownerEmail, isCurrentUserOwner });
if (convertedCollab) {
// Necessary for Flow checking
collaborators.push(convertedCollab);
}
});
return { collaborators };
};
/**
* Convert a request from the USM (specifically the Invite Collaborators Modal) into the format expected by the Collaborations API.
* ContentSharing/USM will only call this function when at least one properly-formatted email is entered into the "Invite People" field.
* Within the context of this feature, groups are identified by IDs, whereas users are identified by their emails.
*
* @param {InviteCollaboratorsRequest} collabRequest
* @returns {ContentSharingCollaborationsRequest}
*/
export const convertCollabsRequest = (
collabRequest: InviteCollaboratorsRequest,
): ContentSharingCollaborationsRequest => {
const { emails, groupIDs, permission } = collabRequest;
const emailArray = emails ? emails.split(',') : [];
const groupIDArray = groupIDs ? groupIDs.split(',') : [];
const roleSettings = {
role: permission.toLowerCase(), // USM permissions are identical to API roles, except for the casing
};
const groups = groupIDArray.map(groupID => ({
accessible_by: {
id: groupID,
type: COLLAB_GROUP_TYPE,
},
...roleSettings,
}));
const users = emailArray.map(email => ({
accessible_by: {
login: email,
type: COLLAB_USER_TYPE,
},
...roleSettings,
}));
return { groups, users };
};
const sortByName = ({ name: nameA = '' }, { name: nameB = '' }) => nameA.localeCompare(nameB);
/**
* Convert an enterprise users API response into an array of internal USM contacts.
*
* @param {UserCollection} contactsAPIData
* @param {string|null} currentUserID
* @returns {Array<contactType>} Array of USM contacts
*/
export const convertUserContactsResponse = (
contactsAPIData: UserCollection,
currentUserID: string | null,
): Array<contactType> => {
const { entries = [] } = contactsAPIData;
// Return all active users except for the current user and app users
return entries
.filter(
({ id, login: email, status }) =>
id !== currentUserID &&
email &&
!APP_USERS_DOMAIN_REGEXP.test(email) &&
status &&
status !== STATUS_INACTIVE,
)
.map(contact => {
const { id, login: email, name, type } = contact;
return {
id,
email,
name,
type,
};
})
.sort(sortByName);
};
/**
* Convert an enterprise users API response into an object of internal USM contacts, keyed by email, which is
* then passed to the mergeContacts function.
*
* @param {UserCollection} contactsAPIData
* @returns { [string]: contactType } Object of USM contacts
*/
export const convertUserContactsByEmailResponse = (contactsAPIData: UserCollection): { [string]: contactType } => {
const { entries = [] } = contactsAPIData;
const contactsMap = {};
entries.forEach(contact => {
const { id, login: email = '', name, type } = contact;
contactsMap[email] = {
id,
email,
name,
type,
};
});
return contactsMap;
};
/**
* Convert an enterprise groups API response into an array of internal USM contacts.
*
* @param {GroupCollection} contactsAPIData
* @returns {Array<contactType>} Array of USM contacts
*/
export const convertGroupContactsResponse = (contactsAPIData: GroupCollection): Array<contactType> => {
const { entries = [] } = contactsAPIData;
// Only return groups with the correct permissions
return entries
.filter(({ permissions }) => {
return permissions && permissions.can_invite_as_collaborator;
})
.map(contact => {
const { id, name, type } = contact;
return {
id,
name,
type,
};
})
.sort(sortByName);
};