Skip to content

Commit e64a61b

Browse files
authored
feat(content-sharing): Create sharing service changeSharedLinkPermission (#4332)
* feat(content-sharing): Create sharing service changeSharedLinkPermission * feat(content-sharing): Create sharing service changeSharedLinkPermission * feat(content-sharing): Create sharing service changeSharedLinkPermission * feat(content-sharing): nits
1 parent c08d9b5 commit e64a61b

File tree

9 files changed

+326
-17
lines changed

9 files changed

+326
-17
lines changed

src/elements/content-sharing/ContentSharingV2.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import API from '../../api';
88
import Internationalize from '../common/Internationalize';
99
import Providers from '../common/Providers';
1010
import { fetchAvatars, fetchCollaborators, fetchCurrentUser, fetchItem } from './apis';
11+
import { useSharingService } from './hooks/useSharingService';
1112
import { convertCollabsResponse, convertItemResponse } from './utils';
1213

1314
import type { Collaborations, ItemType, StringMap } from '../../common/types/core';
@@ -48,18 +49,20 @@ function ContentSharingV2({
4849
const [collaboratorsData, setCollaboratorsData] = React.useState<Collaborations | null>(null);
4950
const [owner, setOwner] = React.useState({ id: '', email: '', name: '' });
5051

52+
const { sharingService } = useSharingService(api, item, itemID, itemType, setItem, setSharedLink);
53+
5154
// Handle successful GET requests to /files or /folders
5255
const handleGetItemSuccess = React.useCallback(itemData => {
5356
const {
54-
collaborationRoles: collaborationRolesFromAPI,
55-
item: itemFromAPI,
57+
collaborationRoles: collaborationRolesFromApi,
58+
item: itemFromApi,
5659
ownedBy,
57-
sharedLink: sharedLinkFromAPI,
60+
sharedLink: sharedLinkFromApi,
5861
} = convertItemResponse(itemData);
5962

60-
setItem(itemFromAPI);
61-
setSharedLink(sharedLinkFromAPI);
62-
setCollaborationRoles(collaborationRolesFromAPI);
63+
setItem(itemFromApi);
64+
setSharedLink(sharedLinkFromApi);
65+
setCollaborationRoles(collaborationRolesFromApi);
6366
setOwner({ id: ownedBy.id, email: ownedBy.login, name: ownedBy.name });
6467
}, []);
6568

@@ -151,6 +154,7 @@ function ContentSharingV2({
151154
currentUser={currentUser}
152155
item={item}
153156
sharedLink={sharedLink}
157+
sharingService={sharingService}
154158
>
155159
{children}
156160
</UnifiedShareModal>

src/elements/content-sharing/__tests__/ContentSharingV2.test.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
2-
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
2+
import { render, type RenderResult, screen, waitFor } from '@testing-library/react';
33

4+
import { useSharingService } from '../hooks/useSharingService';
45
import {
56
DEFAULT_ITEM_API_RESPONSE,
67
DEFAULT_USER_API_RESPONSE,
@@ -46,7 +47,11 @@ const defaultAPIMock = createAPIMock(
4647
{ getCollaborations: getCollaborationsMock },
4748
);
4849

49-
const getWrapper = (props): RenderResult =>
50+
jest.mock('../hooks/useSharingService', () => ({
51+
useSharingService: jest.fn().mockReturnValue({ sharingService: null }),
52+
}));
53+
54+
const renderComponent = (props = {}): RenderResult =>
5055
render(
5156
<ContentSharingV2
5257
api={defaultAPIMock}
@@ -63,7 +68,7 @@ describe('elements/content-sharing/ContentSharingV2', () => {
6368
});
6469

6570
test('should see the correct elements for files', async () => {
66-
getWrapper({});
71+
renderComponent();
6772
await waitFor(() => {
6873
expect(getDefaultFileMock).toHaveBeenCalledWith(MOCK_ITEM.id, expect.any(Function), expect.any(Function), {
6974
fields: CONTENT_SHARING_ITEM_FIELDS,
@@ -75,7 +80,7 @@ describe('elements/content-sharing/ContentSharingV2', () => {
7580
});
7681

7782
test('should see the correct elements for folders', async () => {
78-
getWrapper({ itemType: 'folder' });
83+
renderComponent({ itemType: 'folder' });
7984
await waitFor(() => {
8085
expect(getDefaultFolderMock).toHaveBeenCalledWith(
8186
MOCK_ITEM.id,
@@ -96,7 +101,7 @@ describe('elements/content-sharing/ContentSharingV2', () => {
96101
...defaultAPIMock,
97102
getFileAPI: jest.fn().mockReturnValue({ getFile: getFileMockWithSharedLink }),
98103
};
99-
getWrapper({ api: apiWithSharedLink });
104+
renderComponent({ api: apiWithSharedLink });
100105
await waitFor(() => {
101106
expect(getFileMockWithSharedLink).toHaveBeenCalledWith(
102107
MOCK_ITEM.id,
@@ -120,7 +125,8 @@ describe('elements/content-sharing/ContentSharingV2', () => {
120125
...defaultAPIMock,
121126
getFileAPI: jest.fn().mockReturnValue({ getFile: getFileMockWithClassification }),
122127
};
123-
getWrapper({ api: apiWithClassification });
128+
129+
renderComponent({ api: apiWithClassification });
124130
await waitFor(() => {
125131
expect(getFileMockWithClassification).toHaveBeenCalledWith(
126132
MOCK_ITEM.id,
@@ -135,7 +141,7 @@ describe('elements/content-sharing/ContentSharingV2', () => {
135141
});
136142

137143
test('should process collaborators with avatars correctly', async () => {
138-
getWrapper({});
144+
renderComponent();
139145

140146
await waitFor(() => {
141147
expect(getCollaborationsMock).toHaveBeenCalledWith(
@@ -148,4 +154,19 @@ describe('elements/content-sharing/ContentSharingV2', () => {
148154
expect(getAvatarUrlMock).toHaveBeenCalledWith('458', MOCK_ITEM.id);
149155
});
150156
});
157+
158+
test('should render UnifiedShareModal when sharingService is available', async () => {
159+
const mockSharingService = {
160+
changeSharedLinkPermission: jest.fn(),
161+
};
162+
163+
(useSharingService as jest.Mock).mockReturnValue({
164+
sharingService: mockSharingService,
165+
});
166+
167+
renderComponent();
168+
await waitFor(() => {
169+
expect(screen.getByRole('heading', { name: /Box Development Guide.pdf/i })).toBeVisible();
170+
});
171+
});
151172
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { PERMISSION_CAN_DOWNLOAD, PERMISSION_CAN_PREVIEW } from '../../../constants';
2+
import { CONTENT_SHARING_SHARED_LINK_UPDATE_PARAMS } from '../constants';
3+
import { convertSharedLinkPermissions, createSharingService } from '../sharingService';
4+
5+
describe('elements/content-sharing/sharingService', () => {
6+
describe('convertSharedLinkPermissions', () => {
7+
test.each([
8+
[PERMISSION_CAN_DOWNLOAD, { [PERMISSION_CAN_DOWNLOAD]: true, [PERMISSION_CAN_PREVIEW]: false }],
9+
[PERMISSION_CAN_PREVIEW, { [PERMISSION_CAN_DOWNLOAD]: false, [PERMISSION_CAN_PREVIEW]: true }],
10+
])('should return correct permissions for download permission level', (permissionLevel, expected) => {
11+
const result = convertSharedLinkPermissions(permissionLevel);
12+
expect(result).toEqual(expected);
13+
});
14+
15+
test('should handle empty string permission level', () => {
16+
const result = convertSharedLinkPermissions('');
17+
expect(result).toEqual({});
18+
});
19+
});
20+
21+
describe('createSharingService', () => {
22+
const mockItemApiInstance = {
23+
updateSharedLink: jest.fn(),
24+
};
25+
const mockItemData = { id: '123' };
26+
const mockOnSuccess = jest.fn();
27+
28+
afterEach(() => {
29+
jest.clearAllMocks();
30+
});
31+
32+
test('should return an object with changeSharedLinkPermission method', () => {
33+
const service = createSharingService({
34+
itemApiInstance: mockItemApiInstance,
35+
itemData: mockItemData,
36+
onSuccess: mockOnSuccess,
37+
});
38+
39+
expect(service).toHaveProperty('changeSharedLinkPermission');
40+
expect(typeof service.changeSharedLinkPermission).toBe('function');
41+
});
42+
43+
test('should call updateSharedLink with correct parameters when changeSharedLinkPermission is called', async () => {
44+
const service = createSharingService({
45+
itemApiInstance: mockItemApiInstance,
46+
itemData: mockItemData,
47+
onSuccess: mockOnSuccess,
48+
});
49+
50+
const permissionLevel = PERMISSION_CAN_DOWNLOAD;
51+
const expectedPermissions = {
52+
[PERMISSION_CAN_DOWNLOAD]: true,
53+
[PERMISSION_CAN_PREVIEW]: false,
54+
};
55+
56+
await service.changeSharedLinkPermission(permissionLevel);
57+
58+
expect(mockItemApiInstance.updateSharedLink).toHaveBeenCalledWith(
59+
mockItemData,
60+
{ permissions: expectedPermissions },
61+
mockOnSuccess,
62+
{},
63+
CONTENT_SHARING_SHARED_LINK_UPDATE_PARAMS,
64+
);
65+
});
66+
});
67+
});

src/elements/content-sharing/__tests__/useSharedLink.test.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@ function FakeComponent({
3636
const [changeSharedLinkAccessLevel, setChangeSharedLinkAccessLevel] = React.useState<null | SharedLinkUpdateFnType>(
3737
null,
3838
);
39-
const [
40-
changeSharedLinkPermissionLevel,
41-
setChangeSharedLinkPermissionLevel,
42-
] = React.useState<null | SharedLinkUpdateFnType>(null);
39+
const [changeSharedLinkPermissionLevel, setChangeSharedLinkPermissionLevel] =
40+
React.useState<null | SharedLinkUpdateFnType>(null);
4341
const [onSubmitSettings, setOnSubmitSettings] = React.useState<null | Function>(null);
4442
const [generatedFunctions, setGeneratedFunctions] = React.useState<boolean>(false);
4543

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { renderHook } from '@testing-library/react';
2+
3+
import { TYPE_FILE, TYPE_FOLDER } from '../../../../constants';
4+
import { createSharingService } from '../../sharingService';
5+
import { convertItemResponse } from '../../utils/convertItemResponse';
6+
import { useSharingService } from '../useSharingService';
7+
8+
jest.mock('../../utils/convertItemResponse');
9+
jest.mock('../../sharingService');
10+
11+
const mockApi = {
12+
getFileAPI: jest.fn(),
13+
getFolderAPI: jest.fn(),
14+
};
15+
const mockItemApiInstance = {
16+
updateSharedLink: jest.fn(),
17+
};
18+
const mockSharingService = {
19+
changeSharedLinkPermission: jest.fn(),
20+
};
21+
22+
const mockItemId = '123';
23+
const mockItem = {
24+
id: mockItemId,
25+
permissions: {
26+
can_download: true,
27+
can_preview: false,
28+
},
29+
};
30+
31+
const mockSetItem = jest.fn();
32+
const mockSetSharedLink = jest.fn();
33+
34+
describe('elements/content-sharing/hooks/useSharingService', () => {
35+
beforeEach(() => {
36+
(createSharingService as jest.Mock).mockReturnValue(mockSharingService);
37+
(convertItemResponse as jest.Mock).mockReturnValue({
38+
item: mockItem,
39+
sharedLink: {},
40+
});
41+
});
42+
43+
afterEach(() => {
44+
jest.clearAllMocks();
45+
});
46+
47+
test('should return null itemApiInstance and sharingService when item is null', () => {
48+
const { result } = renderHook(() =>
49+
useSharingService(mockApi, null, mockItemId, TYPE_FILE, mockSetItem, mockSetSharedLink),
50+
);
51+
52+
expect(result.current.sharingService).toBeNull();
53+
expect(mockApi.getFileAPI).not.toHaveBeenCalled();
54+
expect(mockApi.getFolderAPI).not.toHaveBeenCalled();
55+
expect(createSharingService).not.toHaveBeenCalled();
56+
});
57+
58+
test('should return null itemApiInstance and sharingService when itemType is neither TYPE_FILE nor TYPE_FOLDER', () => {
59+
const { result } = renderHook(() =>
60+
useSharingService(mockApi, mockItem, mockItemId, 'hubs', mockSetItem, mockSetSharedLink),
61+
);
62+
63+
expect(result.current.sharingService).toBeNull();
64+
expect(mockApi.getFileAPI).not.toHaveBeenCalled();
65+
expect(mockApi.getFolderAPI).not.toHaveBeenCalled();
66+
expect(createSharingService).not.toHaveBeenCalled();
67+
});
68+
69+
describe('when itemType is TYPE_FILE', () => {
70+
beforeEach(() => {
71+
mockApi.getFileAPI.mockReturnValue(mockItemApiInstance);
72+
});
73+
74+
test('should create file API instance and sharing service', () => {
75+
const { result } = renderHook(() =>
76+
useSharingService(mockApi, mockItem, mockItemId, TYPE_FILE, mockSetItem, mockSetSharedLink),
77+
);
78+
79+
expect(mockApi.getFileAPI).toHaveBeenCalled();
80+
expect(mockApi.getFolderAPI).not.toHaveBeenCalled();
81+
expect(result.current.sharingService).toBe(mockSharingService);
82+
expect(createSharingService).toHaveBeenCalledWith({
83+
itemApiInstance: mockItemApiInstance,
84+
itemData: {
85+
id: mockItemId,
86+
permissions: mockItem.permissions,
87+
},
88+
onSuccess: expect.any(Function),
89+
});
90+
});
91+
92+
test('should handle success callback correctly', () => {
93+
const mockConvertedData = {
94+
item: {
95+
id: mockItemId,
96+
permissions: { can_download: false },
97+
},
98+
sharedLink: {},
99+
};
100+
101+
(convertItemResponse as jest.Mock).mockReturnValue(mockConvertedData);
102+
renderHook(() =>
103+
useSharingService(mockApi, mockItem, mockItemId, TYPE_FILE, mockSetItem, mockSetSharedLink),
104+
);
105+
106+
// Get the onSuccess callback that was passed to mock createSharingService
107+
const onSuccessCallback = (createSharingService as jest.Mock).mock.calls[0][0].onSuccess;
108+
onSuccessCallback(mockConvertedData);
109+
110+
expect(mockSetItem).toHaveBeenCalledTimes(1);
111+
expect(mockSetSharedLink).toHaveBeenCalledTimes(1);
112+
});
113+
});
114+
115+
describe('when itemType is TYPE_FOLDER', () => {
116+
beforeEach(() => {
117+
mockApi.getFolderAPI.mockReturnValue(mockItemApiInstance);
118+
});
119+
120+
test('should create folder API instance and sharing service', () => {
121+
const { result } = renderHook(() =>
122+
useSharingService(mockApi, mockItem, mockItemId, TYPE_FOLDER, mockSetItem, mockSetSharedLink),
123+
);
124+
125+
expect(mockApi.getFolderAPI).toHaveBeenCalled();
126+
expect(mockApi.getFileAPI).not.toHaveBeenCalled();
127+
expect(result.current.sharingService).toBe(mockSharingService);
128+
expect(createSharingService).toHaveBeenCalledWith({
129+
itemApiInstance: mockItemApiInstance,
130+
itemData: {
131+
id: mockItemId,
132+
permissions: mockItem.permissions,
133+
},
134+
onSuccess: expect.any(Function),
135+
});
136+
});
137+
});
138+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from 'react';
2+
3+
import { TYPE_FILE, TYPE_FOLDER } from '../../../constants';
4+
import { convertItemResponse } from '../utils/convertItemResponse';
5+
import { createSharingService } from '../sharingService';
6+
7+
export const useSharingService = (api, item, itemId, itemType, setItem, setSharedLink) => {
8+
const itemApiInstance = React.useMemo(() => {
9+
if (!item) {
10+
return null;
11+
}
12+
13+
if (itemType === TYPE_FILE) {
14+
return api.getFileAPI();
15+
}
16+
17+
if (itemType === TYPE_FOLDER) {
18+
return api.getFolderAPI();
19+
}
20+
21+
return null;
22+
}, [api, item, itemType]);
23+
24+
const sharingService = React.useMemo(() => {
25+
if (!itemApiInstance) {
26+
return null;
27+
}
28+
29+
const itemData = {
30+
id: itemId,
31+
permissions: item.permissions,
32+
};
33+
34+
const handleSuccess = updatedItemData => {
35+
const { item: updatedItem, sharedLink: updatedSharedLink } = convertItemResponse(updatedItemData);
36+
setItem(prevItem => ({ ...prevItem, ...updatedItem }));
37+
setSharedLink(prevSharedLink => ({ ...prevSharedLink, ...updatedSharedLink }));
38+
};
39+
40+
return createSharingService({ itemApiInstance, itemData, onSuccess: handleSuccess });
41+
}, [itemApiInstance, item, itemId, setItem, setSharedLink]);
42+
43+
return { sharingService };
44+
};

0 commit comments

Comments
 (0)