|
| 1 | +/** |
| 2 | + * @file SharingModal |
| 3 | + * @description This is the second-level component for the ContentSharing Element. It receives an API instance |
| 4 | + * from its parent component, ContentSharing, and then instantiates the UnifiedShareModal with API data. |
| 5 | + * @author Box |
| 6 | + */ |
| 7 | +import * as React from 'react'; |
| 8 | +import isEmpty from 'lodash/isEmpty'; |
| 9 | +import noop from 'lodash/noop'; |
| 10 | +import { FormattedMessage, type MessageDescriptor } from 'react-intl'; |
| 11 | +import type { AxiosError } from 'axios'; |
| 12 | +import API from '../../api'; |
| 13 | +import Internationalize from '../common/Internationalize'; |
| 14 | +import NotificationsWrapper from '../../components/notification/NotificationsWrapper'; |
| 15 | +import Notification from '../../components/notification/Notification'; |
| 16 | +import { DURATION_SHORT, TYPE_ERROR } from '../../components/notification/constants'; |
| 17 | +import LoadingIndicator from '../../components/loading-indicator/LoadingIndicator'; |
| 18 | +import UnifiedShareModal from '../../features/unified-share-modal'; |
| 19 | +import SharedLinkSettingsModal from '../../features/shared-link-settings-modal'; |
| 20 | +import SharingNotification from './SharingNotification'; |
| 21 | +import { |
| 22 | + convertItemResponse, |
| 23 | + convertUserContactsByEmailResponse, |
| 24 | + convertUserResponse, |
| 25 | +} from '../../features/unified-share-modal/utils/convertData'; |
| 26 | +import useContactsByEmail from './hooks/useContactsByEmail'; |
| 27 | +import { FIELD_ENTERPRISE, FIELD_HOSTNAME, TYPE_FILE, TYPE_FOLDER } from '../../constants'; |
| 28 | +import { CONTENT_SHARING_ERRORS, CONTENT_SHARING_ITEM_FIELDS, CONTENT_SHARING_VIEWS } from './constants'; |
| 29 | +import { INVITEE_PERMISSIONS_FOLDER, INVITEE_PERMISSIONS_FILE } from '../../features/unified-share-modal/constants'; |
| 30 | +import contentSharingMessages from './messages'; |
| 31 | +import type { ErrorResponseData } from '../../common/types/api'; |
| 32 | +import type { ItemType, StringMap, User } from '../../common/types/core'; |
| 33 | +import type { Item as itemFlowType, USMConfig, CollaboratorListType } from '../../features/unified-share-modal/types'; |
| 34 | +import type { |
| 35 | + ContentSharingItemAPIResponse, |
| 36 | + ContentSharingSharedLinkType, |
| 37 | + GetContactsFnType, |
| 38 | + GetContactsByEmailFnType, |
| 39 | + SendInvitesFnType, |
| 40 | + SharedLinkUpdateLevelFnType, |
| 41 | + SharedLinkUpdateSettingsFnType, |
| 42 | +} from './types'; |
| 43 | + |
| 44 | +type SharingModalProps = { |
| 45 | + api: API; |
| 46 | + config?: USMConfig; |
| 47 | + displayInModal: boolean; |
| 48 | + isVisible: boolean; |
| 49 | + itemID: string; |
| 50 | + itemType: ItemType; |
| 51 | + language: string; |
| 52 | + messages?: StringMap; |
| 53 | + setIsVisible: (arg: boolean) => void; |
| 54 | + uuid?: string; |
| 55 | +}; |
| 56 | + |
| 57 | +function SharingModal({ |
| 58 | + api, |
| 59 | + config, |
| 60 | + displayInModal, |
| 61 | + isVisible, |
| 62 | + itemID, |
| 63 | + itemType, |
| 64 | + language, |
| 65 | + messages, |
| 66 | + setIsVisible, |
| 67 | + uuid, |
| 68 | +}: SharingModalProps) { |
| 69 | + const [item, setItem] = React.useState<itemFlowType | null>(null); |
| 70 | + const [sharedLink, setSharedLink] = React.useState<ContentSharingSharedLinkType | null>(null); |
| 71 | + const [currentUserEnterpriseName, setCurrentUserEnterpriseName] = React.useState<string | null>(null); |
| 72 | + const [currentUserID, setCurrentUserID] = React.useState<string | null>(null); |
| 73 | + const [initialDataErrorMessage, setInitialDataErrorMessage] = React.useState<MessageDescriptor | null>(null); |
| 74 | + const [isInitialDataErrorVisible, setIsInitialDataErrorVisible] = React.useState<boolean>(false); |
| 75 | + const [collaboratorsList, setCollaboratorsList] = React.useState<CollaboratorListType | null>(null); |
| 76 | + const [onAddLink, setOnAddLink] = React.useState<null | SharedLinkUpdateLevelFnType>(null); |
| 77 | + const [onRemoveLink, setOnRemoveLink] = React.useState<null | SharedLinkUpdateLevelFnType>(null); |
| 78 | + const [changeSharedLinkAccessLevel, setChangeSharedLinkAccessLevel] = |
| 79 | + React.useState<null | SharedLinkUpdateLevelFnType>(null); |
| 80 | + const [changeSharedLinkPermissionLevel, setChangeSharedLinkPermissionLevel] = |
| 81 | + React.useState<null | SharedLinkUpdateLevelFnType>(null); |
| 82 | + const [onSubmitSettings, setOnSubmitSettings] = React.useState<null | SharedLinkUpdateSettingsFnType>(null); |
| 83 | + const [currentView, setCurrentView] = React.useState<string>(CONTENT_SHARING_VIEWS.UNIFIED_SHARE_MODAL); |
| 84 | + const [getContacts, setGetContacts] = React.useState<null | GetContactsFnType>(null); |
| 85 | + const [getContactsByEmail, setGetContactsByEmail] = React.useState<null | GetContactsByEmailFnType>(null); |
| 86 | + const [sendInvites, setSendInvites] = React.useState<null | SendInvitesFnType>(null); |
| 87 | + const [isLoading, setIsLoading] = React.useState<boolean>(true); |
| 88 | + |
| 89 | + // Handle successful GET requests to /files or /folders |
| 90 | + const handleGetItemSuccess = React.useCallback((itemData: ContentSharingItemAPIResponse) => { |
| 91 | + const { item: itemFromAPI, sharedLink: sharedLinkFromAPI } = convertItemResponse(itemData); |
| 92 | + setItem(itemFromAPI); |
| 93 | + setSharedLink(sharedLinkFromAPI); |
| 94 | + setIsLoading(false); |
| 95 | + }, []); |
| 96 | + |
| 97 | + // Handle initial data retrieval errors |
| 98 | + const getError = React.useCallback( |
| 99 | + (error: AxiosError<Object> | ErrorResponseData) => { |
| 100 | + if (isInitialDataErrorVisible) return; // display only one component-level notification at a time |
| 101 | + |
| 102 | + setIsInitialDataErrorVisible(true); |
| 103 | + setIsLoading(false); |
| 104 | + |
| 105 | + let errorObject; |
| 106 | + if (error.status) { |
| 107 | + errorObject = contentSharingMessages[CONTENT_SHARING_ERRORS[error.status]]; |
| 108 | + } else if (error.response && error.response.status) { |
| 109 | + errorObject = contentSharingMessages[CONTENT_SHARING_ERRORS[error.response.status]]; |
| 110 | + } else { |
| 111 | + errorObject = contentSharingMessages.loadingError; |
| 112 | + } |
| 113 | + setInitialDataErrorMessage(errorObject); |
| 114 | + }, |
| 115 | + [isInitialDataErrorVisible], |
| 116 | + ); |
| 117 | + |
| 118 | + // Reset state if the API has changed |
| 119 | + React.useEffect(() => { |
| 120 | + setChangeSharedLinkAccessLevel(null); |
| 121 | + setChangeSharedLinkPermissionLevel(null); |
| 122 | + setCollaboratorsList(null); |
| 123 | + setInitialDataErrorMessage(null); |
| 124 | + setCurrentUserID(null); |
| 125 | + setCurrentUserEnterpriseName(null); |
| 126 | + setIsInitialDataErrorVisible(false); |
| 127 | + setIsLoading(true); |
| 128 | + setItem(null); |
| 129 | + setOnAddLink(null); |
| 130 | + setOnRemoveLink(null); |
| 131 | + setSharedLink(null); |
| 132 | + }, [api]); |
| 133 | + |
| 134 | + // Refresh error state if the uuid has changed |
| 135 | + React.useEffect(() => { |
| 136 | + setInitialDataErrorMessage(null); |
| 137 | + setIsInitialDataErrorVisible(false); |
| 138 | + }, [uuid]); |
| 139 | + |
| 140 | + // Get initial data for the item |
| 141 | + React.useEffect(() => { |
| 142 | + const getItem = () => { |
| 143 | + if (itemType === TYPE_FILE) { |
| 144 | + api.getFileAPI().getFile(itemID, handleGetItemSuccess, getError, { |
| 145 | + fields: CONTENT_SHARING_ITEM_FIELDS, |
| 146 | + }); |
| 147 | + } else if (itemType === TYPE_FOLDER) { |
| 148 | + api.getFolderAPI().getFolderFields(itemID, handleGetItemSuccess, getError, { |
| 149 | + fields: CONTENT_SHARING_ITEM_FIELDS, |
| 150 | + }); |
| 151 | + } |
| 152 | + }; |
| 153 | + |
| 154 | + if (api && !isEmpty(api) && !initialDataErrorMessage && isVisible && !item && !sharedLink) { |
| 155 | + getItem(); |
| 156 | + } |
| 157 | + }, [api, initialDataErrorMessage, getError, handleGetItemSuccess, isVisible, item, itemID, itemType, sharedLink]); |
| 158 | + |
| 159 | + // Get initial data for the user |
| 160 | + React.useEffect(() => { |
| 161 | + const getUserSuccess = (userData: User) => { |
| 162 | + const { id, userEnterpriseData } = convertUserResponse(userData); |
| 163 | + setCurrentUserID(id); |
| 164 | + setCurrentUserEnterpriseName(userEnterpriseData.enterpriseName || null); |
| 165 | + setSharedLink(prevSharedLink => ({ ...prevSharedLink, ...userEnterpriseData })); |
| 166 | + setInitialDataErrorMessage(null); |
| 167 | + setIsLoading(false); |
| 168 | + }; |
| 169 | + |
| 170 | + const getUserData = () => { |
| 171 | + api.getUsersAPI(false).getUser(itemID, getUserSuccess, getError, { |
| 172 | + params: { |
| 173 | + fields: [FIELD_ENTERPRISE, FIELD_HOSTNAME].toString(), |
| 174 | + }, |
| 175 | + }); |
| 176 | + }; |
| 177 | + |
| 178 | + if (api && !isEmpty(api) && !initialDataErrorMessage && item && sharedLink && !currentUserID) { |
| 179 | + getUserData(); |
| 180 | + } |
| 181 | + }, [getError, item, itemID, itemType, sharedLink, currentUserID, api, initialDataErrorMessage]); |
| 182 | + |
| 183 | + // Set the getContactsByEmail function. This call is not associated with a banner notification, |
| 184 | + // which is why it exists at this level and not in SharingNotification |
| 185 | + const getContactsByEmailFn: GetContactsByEmailFnType | null = useContactsByEmail(api, itemID, { |
| 186 | + transformUsers: data => convertUserContactsByEmailResponse(data), |
| 187 | + }); |
| 188 | + if (getContactsByEmailFn && !getContactsByEmail) { |
| 189 | + setGetContactsByEmail((): GetContactsByEmailFnType => getContactsByEmailFn); |
| 190 | + } |
| 191 | + |
| 192 | + // Display a notification if there is an error in retrieving initial data |
| 193 | + if (initialDataErrorMessage) { |
| 194 | + return isInitialDataErrorVisible ? ( |
| 195 | + <Internationalize language={language} messages={messages}> |
| 196 | + <NotificationsWrapper> |
| 197 | + <Notification |
| 198 | + onClose={() => setIsInitialDataErrorVisible(false)} |
| 199 | + type={TYPE_ERROR} |
| 200 | + duration={DURATION_SHORT} |
| 201 | + > |
| 202 | + <span> |
| 203 | + <FormattedMessage {...initialDataErrorMessage} /> |
| 204 | + </span> |
| 205 | + </Notification> |
| 206 | + </NotificationsWrapper> |
| 207 | + </Internationalize> |
| 208 | + ) : null; |
| 209 | + } |
| 210 | + |
| 211 | + // Ensure that all necessary data has been received before rendering child components. |
| 212 | + // If the USM is visible, show the LoadingIndicator; otherwise, show nothing. |
| 213 | + // "serverURL" is added to sharedLink after the call to the Users API, so it needs to be checked separately. |
| 214 | + if (!item || !sharedLink || !currentUserID || !sharedLink.serverURL) { |
| 215 | + return isVisible ? <LoadingIndicator /> : null; |
| 216 | + } |
| 217 | + |
| 218 | + const { ownerEmail, ownerID, permissions } = item; |
| 219 | + const { |
| 220 | + accessLevel = '', |
| 221 | + canChangeExpiration = false, |
| 222 | + expirationTimestamp, |
| 223 | + isDownloadAvailable = false, |
| 224 | + serverURL, |
| 225 | + } = sharedLink; |
| 226 | + return ( |
| 227 | + <Internationalize language={language} messages={messages}> |
| 228 | + <> |
| 229 | + <SharingNotification |
| 230 | + accessLevel={accessLevel} |
| 231 | + api={api} |
| 232 | + closeComponent={displayInModal ? () => setIsVisible(false) : noop} |
| 233 | + closeSettings={() => setCurrentView(CONTENT_SHARING_VIEWS.UNIFIED_SHARE_MODAL)} |
| 234 | + collaboratorsList={collaboratorsList} |
| 235 | + currentUserID={currentUserID} |
| 236 | + getContacts={getContacts} |
| 237 | + isDownloadAvailable={isDownloadAvailable} |
| 238 | + itemID={itemID} |
| 239 | + itemType={itemType} |
| 240 | + onSubmitSettings={onSubmitSettings} |
| 241 | + ownerEmail={ownerEmail} |
| 242 | + ownerID={ownerID} |
| 243 | + permissions={permissions} |
| 244 | + sendInvites={sendInvites} |
| 245 | + serverURL={serverURL} |
| 246 | + setChangeSharedLinkAccessLevel={setChangeSharedLinkAccessLevel} |
| 247 | + setChangeSharedLinkPermissionLevel={setChangeSharedLinkPermissionLevel} |
| 248 | + setGetContacts={setGetContacts} |
| 249 | + setCollaboratorsList={setCollaboratorsList} |
| 250 | + setIsLoading={setIsLoading} |
| 251 | + setItem={setItem} |
| 252 | + setOnAddLink={setOnAddLink} |
| 253 | + setOnRemoveLink={setOnRemoveLink} |
| 254 | + setOnSubmitSettings={setOnSubmitSettings} |
| 255 | + setSendInvites={setSendInvites} |
| 256 | + setSharedLink={setSharedLink} |
| 257 | + /> |
| 258 | + {isVisible && currentView === CONTENT_SHARING_VIEWS.SHARED_LINK_SETTINGS && ( |
| 259 | + <SharedLinkSettingsModal |
| 260 | + isDirectLinkUnavailableDueToDownloadSettings={false} |
| 261 | + isDirectLinkUnavailableDueToAccessPolicy={false} |
| 262 | + isDirectLinkUnavailableDueToMaliciousContent={false} |
| 263 | + isOpen={isVisible} |
| 264 | + item={item} |
| 265 | + onRequestClose={() => setCurrentView(CONTENT_SHARING_VIEWS.UNIFIED_SHARE_MODAL)} |
| 266 | + onSubmit={onSubmitSettings} |
| 267 | + submitting={isLoading} |
| 268 | + {...sharedLink} |
| 269 | + canChangeExpiration={canChangeExpiration && !!currentUserEnterpriseName} |
| 270 | + /> |
| 271 | + )} |
| 272 | + {isVisible && currentView === CONTENT_SHARING_VIEWS.UNIFIED_SHARE_MODAL && ( |
| 273 | + <UnifiedShareModal |
| 274 | + canInvite={sharedLink.canInvite} |
| 275 | + config={config} |
| 276 | + changeSharedLinkAccessLevel={changeSharedLinkAccessLevel} |
| 277 | + changeSharedLinkPermissionLevel={changeSharedLinkPermissionLevel} |
| 278 | + collaboratorsList={collaboratorsList} |
| 279 | + currentUserID={currentUserID} |
| 280 | + displayInModal={displayInModal} |
| 281 | + getCollaboratorContacts={getContacts} |
| 282 | + getContactsByEmail={getContactsByEmail} |
| 283 | + initialDataReceived |
| 284 | + inviteePermissions={ |
| 285 | + itemType === TYPE_FOLDER ? INVITEE_PERMISSIONS_FOLDER : INVITEE_PERMISSIONS_FILE |
| 286 | + } |
| 287 | + isOpen={isVisible} |
| 288 | + item={item} |
| 289 | + onAddLink={onAddLink} |
| 290 | + onRequestClose={displayInModal ? () => setIsVisible(false) : noop} |
| 291 | + onRemoveLink={onRemoveLink} |
| 292 | + onSettingsClick={() => setCurrentView(CONTENT_SHARING_VIEWS.SHARED_LINK_SETTINGS)} |
| 293 | + sendInvites={sendInvites} |
| 294 | + sharedLink={{ |
| 295 | + ...sharedLink, |
| 296 | + expirationTimestamp: expirationTimestamp ? expirationTimestamp / 1000 : null, |
| 297 | + }} // the USM expects this value in seconds, while the SLSM expects this value in milliseconds |
| 298 | + submitting={isLoading} |
| 299 | + /> |
| 300 | + )} |
| 301 | + </> |
| 302 | + </Internationalize> |
| 303 | + ); |
| 304 | +} |
| 305 | + |
| 306 | +export default SharingModal; |
0 commit comments