diff --git a/package.json b/package.json index 789bbdb50b..4c6dba7099 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "@box/metadata-view": "^0.54.0", "@box/react-virtualized": "^9.22.3-rc-box.10", "@box/types": "^0.2.1", - "@box/unified-share-modal": "^0.48.8", + "@box/unified-share-modal": "^0.52.0", "@box/user-selector": "^1.23.25", "@cfaester/enzyme-adapter-react-18": "^0.8.0", "@chromatic-com/storybook": "^4.0.1", @@ -310,7 +310,7 @@ "@box/metadata-view": "^0.54.0", "@box/react-virtualized": "^9.22.3-rc-box.10", "@box/types": "^0.2.1", - "@box/unified-share-modal": "^0.48.8", + "@box/unified-share-modal": "^0.52.0", "@box/user-selector": "^1.23.25", "@hapi/address": "^2.1.4", "@tanstack/react-virtual": "^3.13.12", diff --git a/src/elements/content-sharing/ContentSharing.js b/src/elements/content-sharing/ContentSharing.js index 50663c283b..94b27ffe45 100644 --- a/src/elements/content-sharing/ContentSharing.js +++ b/src/elements/content-sharing/ContentSharing.js @@ -116,15 +116,18 @@ function ContentSharing({ if (isFeatureEnabled(features, 'contentSharingV2')) { return ( - - {children} - + api && ( + + {children} + + ) ); } diff --git a/src/elements/content-sharing/ContentSharingV2.tsx b/src/elements/content-sharing/ContentSharingV2.tsx index 5391c041f4..925352c544 100644 --- a/src/elements/content-sharing/ContentSharingV2.tsx +++ b/src/elements/content-sharing/ContentSharingV2.tsx @@ -1,13 +1,21 @@ import * as React from 'react'; +import isEmpty from 'lodash/isEmpty'; import { UnifiedShareModal } from '@box/unified-share-modal'; +import type { CollaborationRole, Item, SharedLink, User } from '@box/unified-share-modal'; +import API from '../../api'; +import { FIELD_ENTERPRISE, FIELD_HOSTNAME, TYPE_FILE, TYPE_FOLDER } from '../../constants'; import Internationalize from '../common/Internationalize'; import Providers from '../common/Providers'; +import { CONTENT_SHARING_ITEM_FIELDS } from './constants'; +import { convertItemResponse } from './utils'; import type { ItemType, StringMap } from '../../common/types/core'; export interface ContentSharingV2Props { + /** api - API instance */ + api: API; /** children - Children for the element to open the Unified Share Modal */ children?: React.ReactElement; /** itemID - Box file or folder ID */ @@ -15,25 +23,124 @@ export interface ContentSharingV2Props { /** itemType - "file" or "folder" */ itemType: ItemType; /** hasProviders - Whether the element has providers for USM already */ - hasProviders: boolean; + hasProviders?: boolean; /** language - Language used for the element */ language?: string; /** messages - Localized strings used by the element */ messages?: StringMap; } -function ContentSharingV2({ children, itemID, itemType, hasProviders, language, messages }: ContentSharingV2Props) { - // Retrieve item from API later - const mockItem = { - id: itemID, - name: 'Box Development Guide.pdf', - type: itemType, +function ContentSharingV2({ + api, + children, + itemID, + itemType, + hasProviders, + language, + messages, +}: ContentSharingV2Props) { + const [item, setItem] = React.useState(null); + const [sharedLink, setSharedLink] = React.useState(null); + const [currentUser, setCurrentUser] = React.useState(null); + const [collaborationRoles, setCollaborationRoles] = React.useState(null); + + // Handle successful GET requests to /files or /folders + const handleGetItemSuccess = React.useCallback(itemData => { + const { + collaborationRoles: collaborationRolesFromAPI, + item: itemFromAPI, + sharedLink: sharedLinkFromAPI, + } = convertItemResponse(itemData); + setItem(itemFromAPI); + setSharedLink(sharedLinkFromAPI); + setCollaborationRoles(collaborationRolesFromAPI); + }, []); + + // Reset state if the API has changed + React.useEffect(() => { + setItem(null); + setSharedLink(null); + setCurrentUser(null); + setCollaborationRoles(null); + }, [api]); + + // Get initial data for the item + React.useEffect(() => { + const getItem = () => { + if (itemType === TYPE_FILE) { + api.getFileAPI().getFile( + itemID, + handleGetItemSuccess, + {}, + { + fields: CONTENT_SHARING_ITEM_FIELDS, + }, + ); + } else if (itemType === TYPE_FOLDER) { + api.getFolderAPI().getFolderFields( + itemID, + handleGetItemSuccess, + {}, + { + fields: CONTENT_SHARING_ITEM_FIELDS, + }, + ); + } + }; + + if (api && !isEmpty(api) && !item && !sharedLink) { + getItem(); + } + }, [api, item, itemID, itemType, sharedLink, handleGetItemSuccess]); + + // Get initial data for the user + React.useEffect(() => { + const getUserSuccess = userData => { + const { enterprise, id } = userData; + setCurrentUser({ + id, + enterprise: { + name: enterprise ? enterprise.name : '', + }, + }); + }; + + const getUserData = () => { + api.getUsersAPI(false).getUser( + itemID, + getUserSuccess, + {}, + { + params: { + fields: [FIELD_ENTERPRISE, FIELD_HOSTNAME].toString(), + }, + }, + ); + }; + + if (api && !isEmpty(api) && item && sharedLink && !currentUser) { + getUserData(); + } + }, [api, currentUser, item, itemID, itemType, sharedLink]); + + const config = { + sharedLinkEmail: false, }; return ( - {children} + {item && ( + + {children} + + )} ); diff --git a/src/elements/content-sharing/__tests__/ContentSharingV2.test.tsx b/src/elements/content-sharing/__tests__/ContentSharingV2.test.tsx new file mode 100644 index 0000000000..f8165d9064 --- /dev/null +++ b/src/elements/content-sharing/__tests__/ContentSharingV2.test.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { render, RenderResult, screen, waitFor } from '@testing-library/react'; + +import { + DEFAULT_ITEM_API_RESPONSE, + DEFAULT_USER_API_RESPONSE, + MOCK_ITEM, + MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK, + MOCK_ITEM_API_RESPONSE_WITH_CLASSIFICATION, +} from '../utils/__mocks__/ContentSharingV2Mocks'; +import { CONTENT_SHARING_ITEM_FIELDS } from '../constants'; + +import ContentSharingV2 from '../ContentSharingV2'; + +const createAPIMock = (fileAPI, folderAPI, usersAPI) => ({ + getFileAPI: jest.fn().mockReturnValue(fileAPI), + getFolderAPI: jest.fn().mockReturnValue(folderAPI), + getUsersAPI: jest.fn().mockReturnValue(usersAPI), +}); + +const createSuccessMock = responseFromAPI => (id, successFn) => { + return Promise.resolve(responseFromAPI).then(response => { + successFn(response); + }); +}; + +const getDefaultUserMock = jest.fn().mockImplementation(createSuccessMock(DEFAULT_USER_API_RESPONSE)); +const getDefaultFileMock = jest.fn().mockImplementation(createSuccessMock(DEFAULT_ITEM_API_RESPONSE)); +const getFileMockWithSharedLink = jest + .fn() + .mockImplementation(createSuccessMock(MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK)); +const getFileMockWithClassification = jest + .fn() + .mockImplementation(createSuccessMock(MOCK_ITEM_API_RESPONSE_WITH_CLASSIFICATION)); +const getDefaultFolderMock = jest.fn().mockImplementation(createSuccessMock(DEFAULT_ITEM_API_RESPONSE)); +const defaultAPIMock = createAPIMock( + { getFile: getDefaultFileMock }, + { getFolderFields: getDefaultFolderMock }, + { getUser: getDefaultUserMock }, +); + +const getWrapper = (props): RenderResult => + render( + , + ); + +describe('elements/content-sharing/ContentSharingV2', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should see the correct elements for files', async () => { + getWrapper({}); + await waitFor(() => { + expect(getDefaultFileMock).toHaveBeenCalledWith( + MOCK_ITEM.id, + expect.any(Function), + {}, + { + fields: CONTENT_SHARING_ITEM_FIELDS, + }, + ); + }); + + expect(screen.getByRole('heading', { name: 'Share ‘Box Development Guide.pdf’' })).toBeVisible(); + expect(screen.getByRole('combobox', { name: 'Invite People' })).toBeVisible(); + expect(screen.getByRole('switch', { name: 'Shared link' })).toBeVisible(); + }); + + test('should see the correct elements for folders', async () => { + getWrapper({ itemType: 'folder' }); + await waitFor(() => { + expect(getDefaultFolderMock).toHaveBeenCalledWith( + MOCK_ITEM.id, + expect.any(Function), + {}, + { + fields: CONTENT_SHARING_ITEM_FIELDS, + }, + ); + }); + + expect(screen.getByRole('heading', { name: 'Share ‘Box Development Guide.pdf’' })).toBeVisible(); + expect(screen.getByRole('combobox', { name: 'Invite People' })).toBeVisible(); + expect(screen.getByRole('switch', { name: 'Shared link' })).toBeVisible(); + }); + + test('should see the shared link elements if shared link is present', async () => { + getWrapper({ + api: createAPIMock({ getFile: getFileMockWithSharedLink }, null, { getUser: getDefaultUserMock }), + }); + await waitFor(() => { + expect(getFileMockWithSharedLink).toHaveBeenCalledWith( + MOCK_ITEM.id, + expect.any(Function), + {}, + { + fields: CONTENT_SHARING_ITEM_FIELDS, + }, + ); + }); + + expect(await screen.findByLabelText('Shared link URL')).toBeVisible(); + expect(await screen.findByRole('button', { name: 'People with the link' })).toBeVisible(); + expect(await screen.findByRole('button', { name: 'Can view and download' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Link Settings' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Copy' })).toBeVisible(); + }); + + test('should see the classification elements if classification is present', async () => { + getWrapper({ + api: createAPIMock({ getFile: getFileMockWithClassification }, null, { getUser: getDefaultUserMock }), + }); + await waitFor(() => { + expect(getFileMockWithClassification).toHaveBeenCalledWith( + MOCK_ITEM.id, + expect.any(Function), + {}, + { + fields: CONTENT_SHARING_ITEM_FIELDS, + }, + ); + }); + expect(screen.getByText('BLUE')).toBeVisible(); + }); +}); diff --git a/src/elements/content-sharing/stories/ContentSharingV2.stories.tsx b/src/elements/content-sharing/stories/ContentSharingV2.stories.tsx index 166ca26ac5..e653601016 100644 --- a/src/elements/content-sharing/stories/ContentSharingV2.stories.tsx +++ b/src/elements/content-sharing/stories/ContentSharingV2.stories.tsx @@ -1,13 +1,22 @@ import * as React from 'react'; + import { TYPE_FILE, TYPE_FOLDER } from '../../../constants'; +import { mockAPIWithSharedLink, mockAPIWithoutSharedLink } from '../utils/__mocks__/ContentSharingV2Mocks'; import ContentSharingV2 from '../ContentSharingV2'; export const basic = {}; +export const withSharedLink = { + args: { + api: mockAPIWithSharedLink, + }, +}; + export default { title: 'Elements/ContentSharingV2', component: ContentSharingV2, args: { + api: mockAPIWithoutSharedLink, children: , itemType: TYPE_FILE, itemID: global.FILE_ID, diff --git a/src/elements/content-sharing/stories/tests/ContentSharingV2-visual.stories.tsx b/src/elements/content-sharing/stories/tests/ContentSharingV2-visual.stories.tsx index f49a24b1dd..3558c60975 100644 --- a/src/elements/content-sharing/stories/tests/ContentSharingV2-visual.stories.tsx +++ b/src/elements/content-sharing/stories/tests/ContentSharingV2-visual.stories.tsx @@ -1,16 +1,26 @@ +import * as React from 'react'; import { TYPE_FILE } from '../../../../constants'; +import { mockAPIWithSharedLink, mockAPIWithoutSharedLink } from '../../utils/__mocks__/ContentSharingV2Mocks'; import ContentSharingV2 from '../../ContentSharingV2'; export const withModernization = { args: { + api: mockAPIWithoutSharedLink, enableModernizedComponents: true, }, }; +export const withSharedLink = { + args: { + api: mockAPIWithSharedLink, + }, +}; + export default { title: 'Elements/ContentSharingV2/tests/visual-regression-tests', component: ContentSharingV2, args: { + children: , itemType: TYPE_FILE, itemID: global.FILE_ID, }, diff --git a/src/elements/content-sharing/types.js b/src/elements/content-sharing/types.js index 8181e28ddf..130328a442 100644 --- a/src/elements/content-sharing/types.js +++ b/src/elements/content-sharing/types.js @@ -1,4 +1,6 @@ // @flow +import type { CollaborationRole, Item, SharedLink } from '@box/unified-share-modal'; + import type { Access, BoxItemClassification, @@ -152,3 +154,9 @@ export type ConvertCollabOptions = { isCurrentUserOwner: boolean, ownerEmail: ?string, }; + +export interface ItemData { + collaborationRoles: CollaborationRole[]; + item: Item; + sharedLink: SharedLink; +} diff --git a/src/elements/content-sharing/utils/__mocks__/ContentSharingV2Mocks.js b/src/elements/content-sharing/utils/__mocks__/ContentSharingV2Mocks.js new file mode 100644 index 0000000000..fd0eeb6717 --- /dev/null +++ b/src/elements/content-sharing/utils/__mocks__/ContentSharingV2Mocks.js @@ -0,0 +1,97 @@ +export const MOCK_PERMISSIONS = { + can_download: true, + can_invite_collaborator: true, + can_set_share_access: true, + can_share: true, +}; + +export const MOCK_CLASSIFICATION = { + color: '#91c2fd', + definition: 'Blue classification', + name: 'Blue', +}; + +export const MOCK_ITEM = { + id: '123456789', + name: 'Box Development Guide.pdf', + type: 'file', +}; + +export const MOCK_SHARED_LINK = { + access: 'open', + effective_permission: 'can_download', + is_password_enabled: true, + unshared_at: 1704067200000, + url: 'https://example.com/shared-link', + vanity_name: 'vanity-name', + vanity_url: 'https://example.com/vanity-url', +}; + +export const DEFAULT_USER_API_RESPONSE = { + id: '123', + enterprise: { + name: 'Parrot Enterprise', + }, +}; + +export const DEFAULT_ITEM_API_RESPONSE = { + allowed_invitee_roles: ['editor', 'viewer'], + allowed_shared_link_access_levels: ['open', 'company', 'collaborators'], + classification: null, + id: MOCK_ITEM.id, + name: MOCK_ITEM.name, + permissions: MOCK_PERMISSIONS, + shared_link: null, + shared_link_features: { password: true }, + type: MOCK_ITEM.type, +}; + +export const MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK = { + ...DEFAULT_ITEM_API_RESPONSE, + shared_link: MOCK_SHARED_LINK, +}; + +export const MOCK_ITEM_API_RESPONSE_WITH_CLASSIFICATION = { + ...DEFAULT_ITEM_API_RESPONSE, + classification: MOCK_CLASSIFICATION, +}; + +// Mock API class for ContentSharingV2 storybook +export const createMockAPI = (itemResponse = DEFAULT_ITEM_API_RESPONSE, userResponse = DEFAULT_USER_API_RESPONSE) => { + const mockFileAPI = { + getFile: (itemID, successCallback) => { + // Simulate async behavior + setTimeout(() => { + successCallback(itemResponse); + }, 100); + }, + }; + + const mockFolderAPI = { + getFolderFields: (itemID, successCallback) => { + // Simulate async behavior + setTimeout(() => { + successCallback(itemResponse); + }, 100); + }, + }; + + const mockUsersAPI = { + getUser: (itemID, successCallback) => { + // Simulate async behavior + setTimeout(() => { + successCallback(userResponse); + }, 100); + }, + }; + + return { + getFileAPI: () => mockFileAPI, + getFolderAPI: () => mockFolderAPI, + getUsersAPI: () => mockUsersAPI, + }; +}; + +// Pre-configured mock APIs for different scenarios +export const mockAPIWithSharedLink = createMockAPI(MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK); +export const mockAPIWithoutSharedLink = createMockAPI(DEFAULT_ITEM_API_RESPONSE); diff --git a/src/elements/content-sharing/utils/__tests__/convertItemResponse.test.ts b/src/elements/content-sharing/utils/__tests__/convertItemResponse.test.ts new file mode 100644 index 0000000000..077ab26a19 --- /dev/null +++ b/src/elements/content-sharing/utils/__tests__/convertItemResponse.test.ts @@ -0,0 +1,115 @@ +import { + DEFAULT_ITEM_API_RESPONSE, + MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK, + MOCK_ITEM_API_RESPONSE_WITH_CLASSIFICATION, +} from '../__mocks__/ContentSharingV2Mocks'; +import { convertItemResponse } from '../convertItemResponse'; + +jest.mock('../getAllowedAccessLevels', () => ({ + getAllowedAccessLevels: jest.fn().mockReturnValue(['open', 'company', 'collaborators']), +})); + +jest.mock('../getAllowedPermissionLevels', () => ({ + getAllowedPermissionLevels: jest.fn().mockReturnValue(['canDownload', 'canPreview']), +})); + +describe('convertItemResponse', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should convert basic item without shared link', () => { + const result = convertItemResponse(DEFAULT_ITEM_API_RESPONSE); + expect(result).toEqual({ + collaborationRoles: [{ id: 'editor' }, { id: 'viewer' }], + item: { + id: '123456789', + classification: undefined, + name: 'Box Development Guide.pdf', + permissions: { + canInviteCollaborator: true, + canSetShareAccess: true, + canShare: true, + }, + type: 'file', + }, + }); + }); + + test('should handle folder type', () => { + const MOCK_ITEM_API_RESPONSE_WITH_FOLDER_TYPE = { + ...DEFAULT_ITEM_API_RESPONSE, + type: 'folder', + }; + const result = convertItemResponse(MOCK_ITEM_API_RESPONSE_WITH_FOLDER_TYPE); + expect(result.item.type).toBe('folder'); + }); + + test('should handle item with classification', () => { + const result = convertItemResponse(MOCK_ITEM_API_RESPONSE_WITH_CLASSIFICATION); + expect(result.item.classification).toEqual({ + colorId: 4, + definition: 'Blue classification', + name: 'Blue', + }); + }); + + describe('shared link settings', () => { + test('should convert item with shared link', () => { + const result = convertItemResponse(MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK); + expect(result.sharedLink).toEqual({ + access: 'open', + accessLevels: ['open', 'company', 'collaborators'], + expiresAt: 1704067200000, + permission: 'can_download', + permissionLevels: ['canDownload', 'canPreview'], + settings: { + canChangeDownload: true, + canChangeExpiration: true, + canChangePassword: true, + canChangeVanityName: false, + isDownloadAvailable: true, + isDownloadEnabled: true, + isPasswordAvailable: true, + isPasswordEnabled: true, + }, + url: 'https://example.com/shared-link', + vanityDomain: 'https://example.com/vanity-url', + vanityName: 'vanity-name', + }); + }); + + test('should convert shared link settings correctly if user cannot change access level', () => { + const MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK_WITH_PERMISSIONS = { + ...MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK, + permissions: { + ...MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK.permissions, + can_set_share_access: false, + }, + }; + const result = convertItemResponse(MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK_WITH_PERMISSIONS); + expect(result.sharedLink.settings.canChangeDownload).toEqual(false); + expect(result.sharedLink.settings.canChangePassword).toEqual(false); + expect(result.sharedLink.settings.canChangeExpiration).toEqual(false); + }); + + test('should convert shared link settings correctly if user does not have permissions', () => { + const MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK_WITH_PERMISSIONS = { + ...MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK, + allowed_invitee_roles: ['viewer'], + shared_link: { + ...MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK.shared_link, + access: 'collaborators', + }, + shared_link_features: { password: false }, + permissions: { + ...MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK.permissions, + }, + }; + const result = convertItemResponse(MOCK_ITEM_API_RESPONSE_WITH_SHARED_LINK_WITH_PERMISSIONS); + expect(result.sharedLink.settings.canChangeDownload).toEqual(false); + expect(result.sharedLink.settings.canChangePassword).toEqual(false); + expect(result.sharedLink.settings.canChangeExpiration).toEqual(false); + }); + }); +}); diff --git a/src/elements/content-sharing/utils/__tests__/getAllowedAccessLevels.test.ts b/src/elements/content-sharing/utils/__tests__/getAllowedAccessLevels.test.ts new file mode 100644 index 0000000000..169594487d --- /dev/null +++ b/src/elements/content-sharing/utils/__tests__/getAllowedAccessLevels.test.ts @@ -0,0 +1,24 @@ +import { ACCESS_COLLAB, ACCESS_COMPANY, ACCESS_OPEN } from '../../../../constants'; +import { getAllowedAccessLevels } from '../getAllowedAccessLevels'; + +describe('getAllowedAccessLevels', () => { + test('should return default access levels when no levels parameter is provided', () => { + const result = getAllowedAccessLevels(); + expect(result).toEqual([ACCESS_OPEN, ACCESS_COMPANY, ACCESS_COLLAB]); + }); + + test('should return empty array when levels parameter is empty array', () => { + const result = getAllowedAccessLevels([]); + expect(result).toEqual([]); + }); + + test.each([ + [[ACCESS_OPEN, ACCESS_COMPANY, ACCESS_COLLAB]], + [[ACCESS_OPEN, ACCESS_COMPANY]], + [[ACCESS_OPEN]], + [[ACCESS_COMPANY]], + ])('should return the same levels as provided', levels => { + const result = getAllowedAccessLevels(levels); + expect(result).toEqual(levels); + }); +}); diff --git a/src/elements/content-sharing/utils/__tests__/getAllowedPermissionLevels.test.ts b/src/elements/content-sharing/utils/__tests__/getAllowedPermissionLevels.test.ts new file mode 100644 index 0000000000..65963a3615 --- /dev/null +++ b/src/elements/content-sharing/utils/__tests__/getAllowedPermissionLevels.test.ts @@ -0,0 +1,28 @@ +import { PERMISSION_CAN_DOWNLOAD, PERMISSION_CAN_PREVIEW } from '../../../../constants'; +import { getAllowedPermissionLevels } from '../getAllowedPermissionLevels'; + +describe('getAllowedPermissionLevels', () => { + test('should return both permission levels when all conditions are met', () => { + const result = getAllowedPermissionLevels(true, true, PERMISSION_CAN_DOWNLOAD); + expect(result).toEqual([PERMISSION_CAN_DOWNLOAD, PERMISSION_CAN_PREVIEW]); + }); + + test.each([PERMISSION_CAN_DOWNLOAD, PERMISSION_CAN_PREVIEW])( + 'should return only current permission when cannot change access level', + permission => { + const result = getAllowedPermissionLevels(false, true, permission); + expect(result).toEqual([permission]); + }, + ); + + test('should exclude download permission when download setting is not available', () => { + const result = getAllowedPermissionLevels(true, false, PERMISSION_CAN_DOWNLOAD); + expect(result).toEqual([PERMISSION_CAN_PREVIEW]); + }); + + test('should return empty array for unknown permission values when cannot change access level', () => { + const unknownPermission = 'unknown_permission'; + const result = getAllowedPermissionLevels(false, true, unknownPermission); + expect(result).toEqual([]); + }); +}); diff --git a/src/elements/content-sharing/utils/constants.ts b/src/elements/content-sharing/utils/constants.ts new file mode 100644 index 0000000000..71ee401eab --- /dev/null +++ b/src/elements/content-sharing/utils/constants.ts @@ -0,0 +1,31 @@ +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 '../../../features/classification/constants'; +import { + bdlDarkBlue50, + bdlGray20, + bdlGreenLight50, + bdlLightBlue50, + bdlOrange50, + bdlPurpleRain50, + bdlWatermelonRed50, + bdlYellow50, +} from '../../../styles/variables'; + +export 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, +}; diff --git a/src/elements/content-sharing/utils/convertItemResponse.ts b/src/elements/content-sharing/utils/convertItemResponse.ts new file mode 100644 index 0000000000..060ee982b4 --- /dev/null +++ b/src/elements/content-sharing/utils/convertItemResponse.ts @@ -0,0 +1,99 @@ +import { ACCESS_COLLAB, INVITEE_ROLE_EDITOR, PERMISSION_CAN_DOWNLOAD } from '../../../constants'; +import { getAllowedAccessLevels } from './getAllowedAccessLevels'; +import { getAllowedPermissionLevels } from './getAllowedPermissionLevels'; +import { API_TO_USM_CLASSIFICATION_COLORS_MAP } from '../utils/constants'; + +import type { ContentSharingItemAPIResponse, ItemData } from '../types'; + +export const convertItemResponse = (itemAPIData: ContentSharingItemAPIResponse): ItemData => { + const { + allowed_invitee_roles, + allowed_shared_link_access_levels, + classification, + id, + name, + permissions, + shared_link, + shared_link_features, + type, + } = itemAPIData; + + const { password: isPasswordAvailable } = shared_link_features; + + const { + can_download: isDownloadSettingAvailable, + can_invite_collaborator: canInvite, + can_set_share_access: canChangeAccessLevel, + can_share: canShare, + } = permissions; + + // Convert classification data for the item if available + let classificationData; + if (classification) { + const { color, definition, name: classificationName } = classification; + classificationData = { + colorId: API_TO_USM_CLASSIFICATION_COLORS_MAP[color], + definition, + name: classificationName, + }; + } + + const isEditAllowed = allowed_invitee_roles.indexOf(INVITEE_ROLE_EDITOR) !== -1; + + let sharedLink; + if (shared_link) { + const { + access, + effective_permission: permission, + is_password_enabled: isPasswordEnabled, + unshared_at: expirationTimestamp, + url, + vanity_name: vanityName, + vanity_url: vanityUrl, + } = shared_link; + + const isDownloadAllowed = permission === PERMISSION_CAN_DOWNLOAD; + const canChangeDownload = canChangeAccessLevel && isDownloadSettingAvailable && access !== ACCESS_COLLAB; // access must be "company" or "open" + const canChangePassword = canChangeAccessLevel && isPasswordAvailable; + const canChangeExpiration = canChangeAccessLevel && isEditAllowed; + + sharedLink = { + access, + accessLevels: getAllowedAccessLevels(allowed_shared_link_access_levels), + expiresAt: expirationTimestamp, + permission, + permissionLevels: getAllowedPermissionLevels(canChangeAccessLevel, isDownloadSettingAvailable, permission), + settings: { + canChangeDownload, + canChangeExpiration, + canChangePassword, + canChangeVanityName: false, // vanity URLs cannot be set via the API, + isDownloadAvailable: isDownloadSettingAvailable, + isDownloadEnabled: isDownloadAllowed, + isPasswordAvailable: isPasswordAvailable ?? false, + isPasswordEnabled, + }, + url, + vanityDomain: vanityUrl, + vanityName: vanityName || '', + }; + } + + const collaborationRoles = allowed_invitee_roles.map(role => ({ id: role })); + + return { + collaborationRoles, + item: { + id, + classification: classificationData, + name, + permissions: { + canInviteCollaborator: !!canInvite, + canSetShareAccess: canChangeAccessLevel, + canShare: !!canShare, + }, + type, + }, + sharedLink, + }; +}; diff --git a/src/elements/content-sharing/utils/getAllowedAccessLevels.ts b/src/elements/content-sharing/utils/getAllowedAccessLevels.ts new file mode 100644 index 0000000000..8e2602371f --- /dev/null +++ b/src/elements/content-sharing/utils/getAllowedAccessLevels.ts @@ -0,0 +1,6 @@ +import { ACCESS_COLLAB, ACCESS_COMPANY, ACCESS_OPEN } from '../../../constants'; + +export const getAllowedAccessLevels = (levels?: Array): Array | null => { + if (!levels) return [ACCESS_OPEN, ACCESS_COMPANY, ACCESS_COLLAB]; + return [...levels]; +}; diff --git a/src/elements/content-sharing/utils/getAllowedPermissionLevels.ts b/src/elements/content-sharing/utils/getAllowedPermissionLevels.ts new file mode 100644 index 0000000000..2922386832 --- /dev/null +++ b/src/elements/content-sharing/utils/getAllowedPermissionLevels.ts @@ -0,0 +1,21 @@ +import { PERMISSION_CAN_DOWNLOAD, PERMISSION_CAN_PREVIEW } from '../../../constants'; + +export const getAllowedPermissionLevels = ( + canChangeAccessLevel, + isDownloadSettingAvailable, + permission, +): Array => { + let allowedPermissionLevels = [PERMISSION_CAN_DOWNLOAD, PERMISSION_CAN_PREVIEW]; + + if (!canChangeAccessLevel) { + // remove all but current level + allowedPermissionLevels = allowedPermissionLevels.filter(level => level === permission); + } + + // if we cannot set the download value, we remove this option from the dropdown + if (!isDownloadSettingAvailable) { + allowedPermissionLevels = allowedPermissionLevels.filter(level => level !== PERMISSION_CAN_DOWNLOAD); + } + + return allowedPermissionLevels; +}; diff --git a/src/elements/content-sharing/utils/index.ts b/src/elements/content-sharing/utils/index.ts new file mode 100644 index 0000000000..38b674e241 --- /dev/null +++ b/src/elements/content-sharing/utils/index.ts @@ -0,0 +1,3 @@ +export { convertItemResponse } from './convertItemResponse'; +export { getAllowedAccessLevels } from './getAllowedAccessLevels'; +export { getAllowedPermissionLevels } from './getAllowedPermissionLevels'; diff --git a/yarn.lock b/yarn.lock index b8c82fd7b0..c20a76eefc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1419,7 +1419,7 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@box/blueprint-web-assets@^4.68.6": +"@box/blueprint-web-assets@^4.68.0", "@box/blueprint-web-assets@^4.68.6": version "4.68.6" resolved "https://registry.yarnpkg.com/@box/blueprint-web-assets/-/blueprint-web-assets-4.68.6.tgz#81c27616687794032e9dc7ece6857797188e5130" integrity sha512-2UrvvlCzE/PkgQ3yQldqlZxCF6pUXp+UKOuvFGAmAm2B1hWw0v3BfiPDTTJSRfGAnukNnpnItjdMkaq/qXKOpA== @@ -1549,10 +1549,10 @@ resolved "https://registry.yarnpkg.com/@box/types/-/types-0.2.1.tgz#cd0a3915b2306e4cf581f6091b95f5d2db75ea60" integrity sha512-wd6nRR9QxBl7lYKJ/Hix0AKg1PNC3leZWOJ9Nt+d4j45WxCYBiCemZAtY2ekL5BITpVw8vlLmquzSpPhDTeO5A== -"@box/unified-share-modal@^0.48.8": - version "0.48.8" - resolved "https://registry.yarnpkg.com/@box/unified-share-modal/-/unified-share-modal-0.48.8.tgz#d166ec081788e142fd90332f1c96bc17890d79a8" - integrity sha512-zF1kAc9inyQnKkMPyghiRkpqeA5w4NO3fyuRRq0QIXwP0Xt8edZsD/sLj2sXmRzNw/9W2Qz/7wih3p+xzrEUxg== +"@box/unified-share-modal@^0.52.0": + version "0.52.0" + resolved "https://registry.yarnpkg.com/@box/unified-share-modal/-/unified-share-modal-0.52.0.tgz#5ebfb1c9246789ce4650efc9b19283de0c492f71" + integrity sha512-85/xr47n9uCNwJ3nMq5AEGHJ6DOUrClh9ARbwaoJeR39x0sTjv0JGIBAmK4yM2rhPMGZO2jnnBcZ8wbZ+yVoHw== "@box/user-selector@^1.23.25": version "1.23.25"