cozy-sharing 28.4.4 → 28.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/SharingProvider.js +296 -66
- package/dist/SharingProvider.spec.js +332 -2
- package/dist/components/FederatedFolder/DumbFederatedFolderModal.js +7 -4
- package/dist/components/FederatedFolder/FederatedFolderModal.js +79 -23
- package/dist/components/Recipient/LinkRecipient.js +6 -3
- package/dist/components/Recipient/LinkRecipient.stories.js +0 -1
- package/dist/components/Recipient/MemberRecipient.spec.js +13 -0
- package/dist/components/Recipient/MemberRecipientPermissions.js +80 -20
- package/dist/components/Recipient/MemberRecipientPermissions.spec.js +214 -0
- package/dist/components/Recipient/actions/setReadOnlySharedPermission.js +0 -1
- package/dist/components/Recipient/actions/setReadOnlySharedPermission.spec.js +72 -0
- package/dist/components/Recipient/actions/setReadWriteSharedPermission.js +0 -1
- package/dist/components/Recipient/actions/setReadWriteSharedPermission.spec.js +72 -0
- package/dist/components/ShareByLink.js +5 -1
- package/dist/components/ShareDialogCozyToCozy.spec.js +9 -0
- package/dist/components/ShareRestrictionModal/helpers.js +66 -29
- package/dist/components/SharedDrive/DumbSharedDriveModal.js +1 -1
- package/dist/components/SharedFolder/DumbBatchSharedFolderModal.js +17 -4
- package/dist/components/WhoHasAccess.js +0 -1
- package/dist/helpers/shortcodes.js +24 -0
- package/dist/state.js +3 -10
- package/locales/en.json +4 -1
- package/locales/fr.json +4 -1
- package/locales/ru.json +4 -1
- package/locales/vi.json +4 -1
- package/package.json +4 -4
- package/src/SharingProvider.jsx +128 -9
- package/src/SharingProvider.spec.jsx +224 -1
- package/src/components/FederatedFolder/DumbFederatedFolderModal.jsx +6 -3
- package/src/components/FederatedFolder/FederatedFolderModal.jsx +39 -6
- package/src/components/Recipient/LinkRecipient.jsx +10 -4
- package/src/components/Recipient/LinkRecipient.stories.jsx +0 -1
- package/src/components/Recipient/MemberRecipient.spec.jsx +11 -0
- package/src/components/Recipient/MemberRecipientPermissions.jsx +69 -16
- package/src/components/Recipient/MemberRecipientPermissions.spec.jsx +165 -0
- package/src/components/Recipient/actions/setReadOnlySharedPermission.js +1 -1
- package/src/components/Recipient/actions/setReadOnlySharedPermission.spec.jsx +95 -0
- package/src/components/Recipient/actions/setReadWriteSharedPermission.js +1 -1
- package/src/components/Recipient/actions/setReadWriteSharedPermission.spec.jsx +95 -0
- package/src/components/ShareByLink.jsx +3 -1
- package/src/components/ShareDialogCozyToCozy.spec.jsx +6 -0
- package/src/components/ShareRestrictionModal/helpers.js +42 -4
- package/src/components/SharedDrive/DumbSharedDriveModal.jsx +1 -1
- package/src/components/SharedFolder/DumbBatchSharedFolderModal.jsx +32 -16
- package/src/components/WhoHasAccess.jsx +0 -1
- package/src/helpers/shortcodes.js +28 -0
- package/src/state.js +3 -14
|
@@ -5,7 +5,7 @@ import { createMockClient } from 'cozy-client'
|
|
|
5
5
|
|
|
6
6
|
import { SharingProvider } from './SharingProvider'
|
|
7
7
|
import SharingContext from './context'
|
|
8
|
-
|
|
8
|
+
import reducer, { addSharingLink, getDocumentPermissions } from './state'
|
|
9
9
|
import AppLike from '../test/AppLike'
|
|
10
10
|
|
|
11
11
|
const AppWrapper = ({ children, client, isPublic }) => {
|
|
@@ -105,6 +105,229 @@ describe('SharingProvider', () => {
|
|
|
105
105
|
})
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
+
describe('shareByLink', () => {
|
|
109
|
+
const PERM_DRIVE_FILE = {
|
|
110
|
+
type: 'io.cozy.permissions',
|
|
111
|
+
id: 'perm_drive_file',
|
|
112
|
+
attributes: {
|
|
113
|
+
type: 'share',
|
|
114
|
+
permissions: {
|
|
115
|
+
rule0: {
|
|
116
|
+
type: 'io.cozy.files',
|
|
117
|
+
verbs: ['GET'],
|
|
118
|
+
values: ['file_in_drive']
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
shortcodes: { code: 'shortcode123' }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const driveFile = {
|
|
126
|
+
_id: 'file_in_drive',
|
|
127
|
+
id: 'file_in_drive',
|
|
128
|
+
driveId: 'drive_123'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
it('dispatches addSharingLink exactly once for a shared drive file', async () => {
|
|
132
|
+
const mockCreateSharingLink = jest
|
|
133
|
+
.fn()
|
|
134
|
+
.mockResolvedValue({ data: PERM_DRIVE_FILE })
|
|
135
|
+
const mockClient = createMockClient({})
|
|
136
|
+
mockClient.collection = jest.fn().mockReturnValue({
|
|
137
|
+
createSharingLink: mockCreateSharingLink
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const provider = new SharingProvider({ client: mockClient })
|
|
141
|
+
provider.state = reducer()
|
|
142
|
+
provider.dispatch = jest.fn(action => {
|
|
143
|
+
provider.state = reducer(provider.state, action)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
await provider.shareByLink(driveFile, { verbs: ['GET'] })
|
|
147
|
+
|
|
148
|
+
expect(provider.dispatch).toHaveBeenCalledTimes(1)
|
|
149
|
+
const permissions = getDocumentPermissions(provider.state, driveFile._id)
|
|
150
|
+
expect(permissions).toHaveLength(1)
|
|
151
|
+
expect(permissions[0].id).toBe(PERM_DRIVE_FILE.id)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('dispatches addSharingLink once for a regular file (non shared drive)', async () => {
|
|
155
|
+
const PERM_REGULAR_FILE = {
|
|
156
|
+
type: 'io.cozy.permissions',
|
|
157
|
+
id: 'perm_regular_file',
|
|
158
|
+
attributes: {
|
|
159
|
+
type: 'share',
|
|
160
|
+
permissions: {
|
|
161
|
+
rule0: {
|
|
162
|
+
type: 'io.cozy.files',
|
|
163
|
+
verbs: ['GET'],
|
|
164
|
+
values: ['regular_file_id']
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
shortcodes: { code: 'shortcode456' }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const regularFile = { _id: 'regular_file_id', id: 'regular_file_id' }
|
|
171
|
+
const mockCreateSharingLink = jest
|
|
172
|
+
.fn()
|
|
173
|
+
.mockResolvedValue({ data: PERM_REGULAR_FILE })
|
|
174
|
+
const mockClient = createMockClient({})
|
|
175
|
+
mockClient.collection = jest.fn().mockReturnValue({
|
|
176
|
+
createSharingLink: mockCreateSharingLink
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const provider = new SharingProvider({ client: mockClient })
|
|
180
|
+
provider.state = reducer()
|
|
181
|
+
provider.permissionCol = { createSharingLink: mockCreateSharingLink }
|
|
182
|
+
provider.dispatch = jest.fn(action => {
|
|
183
|
+
provider.state = reducer(provider.state, action)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
await provider.shareByLink(regularFile, { verbs: ['GET'] })
|
|
187
|
+
|
|
188
|
+
expect(provider.dispatch).toHaveBeenCalledTimes(1)
|
|
189
|
+
const permissions = getDocumentPermissions(provider.state, regularFile._id)
|
|
190
|
+
expect(permissions).toHaveLength(1)
|
|
191
|
+
expect(permissions[0].id).toBe(PERM_REGULAR_FILE.id)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('updateDocumentPermissions', () => {
|
|
196
|
+
const PERM_DRIVE_FILE = {
|
|
197
|
+
type: 'io.cozy.permissions',
|
|
198
|
+
id: 'perm_drive_file',
|
|
199
|
+
attributes: {
|
|
200
|
+
type: 'share',
|
|
201
|
+
permissions: {
|
|
202
|
+
rule0: {
|
|
203
|
+
type: 'io.cozy.files',
|
|
204
|
+
verbs: ['GET'],
|
|
205
|
+
values: ['file_in_drive']
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
shortcodes: { code: 'shortcode123' }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
it('sends only one PATCH request for a shared drive file', async () => {
|
|
213
|
+
const driveFile = {
|
|
214
|
+
_id: 'file_in_drive',
|
|
215
|
+
id: 'file_in_drive',
|
|
216
|
+
driveId: 'drive_123'
|
|
217
|
+
}
|
|
218
|
+
const mockAdd = jest.fn().mockResolvedValue({ data: PERM_DRIVE_FILE })
|
|
219
|
+
const mockClient = createMockClient({})
|
|
220
|
+
mockClient.collection = jest.fn().mockReturnValue({ add: mockAdd })
|
|
221
|
+
|
|
222
|
+
const provider = new SharingProvider({ client: mockClient })
|
|
223
|
+
// Seed the state with exactly one permission (as it would be after a correct shareByLink)
|
|
224
|
+
provider.state = reducer(undefined, addSharingLink(PERM_DRIVE_FILE))
|
|
225
|
+
provider.dispatch = jest.fn()
|
|
226
|
+
|
|
227
|
+
await provider.updateDocumentPermissions(driveFile, {
|
|
228
|
+
verbs: ['GET'],
|
|
229
|
+
expiresAt: '',
|
|
230
|
+
password: ''
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
expect(mockAdd).toHaveBeenCalledTimes(1)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('updateSharingMemberType', () => {
|
|
238
|
+
const mockContact = { _id: 'contact-1', email: [{ address: 'bob@bob.cozy' }] }
|
|
239
|
+
const mockSharing = {
|
|
240
|
+
id: 'sharing-123',
|
|
241
|
+
type: 'io.cozy.sharings',
|
|
242
|
+
attributes: {
|
|
243
|
+
members: [
|
|
244
|
+
{ status: 'owner', email: 'owner@cozy.local' },
|
|
245
|
+
{ status: 'ready', email: 'bob@bob.cozy' }
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let instance
|
|
251
|
+
let mockContactsCollection
|
|
252
|
+
|
|
253
|
+
beforeEach(() => {
|
|
254
|
+
mockContactsCollection = {
|
|
255
|
+
find: jest.fn().mockResolvedValue({ data: [mockContact] })
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const mockClient = {
|
|
259
|
+
getStackClient: () => ({ uri: 'http://cozy.local' }),
|
|
260
|
+
collection: jest.fn().mockReturnValue(mockContactsCollection)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
instance = new SharingProvider({ client: mockClient })
|
|
264
|
+
instance.state = {
|
|
265
|
+
...instance.state,
|
|
266
|
+
sharings: [mockSharing]
|
|
267
|
+
}
|
|
268
|
+
instance.sharingCol = {
|
|
269
|
+
revokeRecipient: jest.fn().mockResolvedValue({})
|
|
270
|
+
}
|
|
271
|
+
jest.spyOn(instance, 'addRecipients').mockResolvedValue({})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('should throw when sharing is not found', async () => {
|
|
275
|
+
await expect(
|
|
276
|
+
instance.updateSharingMemberType('unknown-id', 1, 'one-way')
|
|
277
|
+
).rejects.toThrow('Sharing not found')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should throw when member is not found', async () => {
|
|
281
|
+
await expect(
|
|
282
|
+
instance.updateSharingMemberType('sharing-123', 99, 'one-way')
|
|
283
|
+
).rejects.toThrow('Member not found')
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('should throw when contact is not found', async () => {
|
|
287
|
+
mockContactsCollection.find.mockResolvedValue({ data: [] })
|
|
288
|
+
|
|
289
|
+
await expect(
|
|
290
|
+
instance.updateSharingMemberType('sharing-123', 1, 'one-way')
|
|
291
|
+
).rejects.toThrow('Contact not found')
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should revoke and re-add with readOnly when switching to one-way', async () => {
|
|
295
|
+
await instance.updateSharingMemberType('sharing-123', 1, 'one-way')
|
|
296
|
+
|
|
297
|
+
expect(instance.sharingCol.revokeRecipient).toHaveBeenCalledWith(
|
|
298
|
+
mockSharing,
|
|
299
|
+
1
|
|
300
|
+
)
|
|
301
|
+
expect(instance.addRecipients).toHaveBeenCalledWith({
|
|
302
|
+
document: mockSharing,
|
|
303
|
+
recipients: [],
|
|
304
|
+
readOnlyRecipients: [mockContact]
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('should revoke and re-add as read-write when switching to two-way', async () => {
|
|
309
|
+
await instance.updateSharingMemberType('sharing-123', 1, 'two-way')
|
|
310
|
+
|
|
311
|
+
expect(instance.sharingCol.revokeRecipient).toHaveBeenCalledWith(
|
|
312
|
+
mockSharing,
|
|
313
|
+
1
|
|
314
|
+
)
|
|
315
|
+
expect(instance.addRecipients).toHaveBeenCalledWith({
|
|
316
|
+
document: mockSharing,
|
|
317
|
+
recipients: [mockContact],
|
|
318
|
+
readOnlyRecipients: []
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('should rethrow error if addRecipients fails after revocation', async () => {
|
|
323
|
+
instance.addRecipients.mockRejectedValue(new Error('Network error'))
|
|
324
|
+
|
|
325
|
+
await expect(
|
|
326
|
+
instance.updateSharingMemberType('sharing-123', 1, 'one-way')
|
|
327
|
+
).rejects.toThrow('Network error')
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
108
331
|
// TODO Convert with react-testing-library
|
|
109
332
|
// describe('hasWriteAccess', () => {
|
|
110
333
|
// it('tells if a doc is writtable', () => {
|
|
@@ -3,7 +3,7 @@ import React from 'react'
|
|
|
3
3
|
import { useI18n } from 'twake-i18n'
|
|
4
4
|
|
|
5
5
|
import withLocales from '../../hoc/withLocales'
|
|
6
|
-
import DumbBatchSharedFolderModal from '../SharedFolder/DumbBatchSharedFolderModal'
|
|
6
|
+
import { DumbBatchSharedFolderModal } from '../SharedFolder/DumbBatchSharedFolderModal'
|
|
7
7
|
|
|
8
8
|
export const DumbFederatedFolderModal = withLocales(
|
|
9
9
|
({
|
|
@@ -18,7 +18,8 @@ export const DumbFederatedFolderModal = withLocales(
|
|
|
18
18
|
onSend,
|
|
19
19
|
onClose,
|
|
20
20
|
onShare,
|
|
21
|
-
sharingLink
|
|
21
|
+
sharingLink,
|
|
22
|
+
showShareByEmail
|
|
22
23
|
}) => {
|
|
23
24
|
const { t } = useI18n()
|
|
24
25
|
return (
|
|
@@ -39,6 +40,7 @@ export const DumbFederatedFolderModal = withLocales(
|
|
|
39
40
|
addPeopleLabel={t('FederatedFolder.addPeople')}
|
|
40
41
|
addButtonLabel={t('FederatedFolder.add')}
|
|
41
42
|
shareLabel={t('FederatedFolder.share')}
|
|
43
|
+
showShareByEmail={showShareByEmail}
|
|
42
44
|
/>
|
|
43
45
|
)
|
|
44
46
|
}
|
|
@@ -56,7 +58,8 @@ DumbFederatedFolderModal.propTypes = {
|
|
|
56
58
|
onSend: PropTypes.func.isRequired,
|
|
57
59
|
onClose: PropTypes.func.isRequired,
|
|
58
60
|
onShare: PropTypes.func.isRequired,
|
|
59
|
-
sharingLink: PropTypes.string
|
|
61
|
+
sharingLink: PropTypes.string,
|
|
62
|
+
showShareByEmail: PropTypes.bool
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
export default DumbFederatedFolderModal
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import PropTypes from 'prop-types'
|
|
2
|
-
import React, { useState } from 'react'
|
|
2
|
+
import React, { useState, useEffect } from 'react'
|
|
3
3
|
import { useI18n } from 'twake-i18n'
|
|
4
4
|
|
|
5
5
|
import { useClient } from 'cozy-client'
|
|
@@ -19,13 +19,43 @@ export const FederatedFolderModal = withLocales(
|
|
|
19
19
|
({ onClose, document: existingDocument }) => {
|
|
20
20
|
const client = useClient()
|
|
21
21
|
const { t } = useI18n()
|
|
22
|
-
const {
|
|
22
|
+
const {
|
|
23
|
+
share,
|
|
24
|
+
getSharingLink,
|
|
25
|
+
getFederatedShareLink,
|
|
26
|
+
getDocumentPermissions,
|
|
27
|
+
getOwner,
|
|
28
|
+
getSharingById
|
|
29
|
+
} = useSharingContext()
|
|
23
30
|
const { showAlert } = useAlert()
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
const [sharingLink, setSharingLink] = useState(null)
|
|
33
|
+
|
|
34
|
+
const documentPermissions = existingDocument
|
|
35
|
+
? getDocumentPermissions(existingDocument._id)
|
|
36
|
+
: []
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const fetchSharingLink = async () => {
|
|
40
|
+
if (!existingDocument) return
|
|
41
|
+
|
|
42
|
+
if (existingDocument.driveId) {
|
|
43
|
+
const link = await getFederatedShareLink(existingDocument)
|
|
44
|
+
setSharingLink(link)
|
|
45
|
+
} else {
|
|
46
|
+
setSharingLink(getSharingLink(existingDocument._id))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fetchSharingLink()
|
|
51
|
+
}, [
|
|
52
|
+
existingDocument,
|
|
53
|
+
documentPermissions,
|
|
54
|
+
getFederatedShareLink,
|
|
55
|
+
getSharingLink,
|
|
56
|
+
getOwner,
|
|
57
|
+
getSharingById
|
|
58
|
+
]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
29
59
|
|
|
30
60
|
const [federatedRecipients, setFederatedRecipients] = useState({
|
|
31
61
|
recipients: [],
|
|
@@ -92,6 +122,8 @@ export const FederatedFolderModal = withLocales(
|
|
|
92
122
|
|
|
93
123
|
const modalTitle = t('FederatedFolder.shareTitle', { name: folderName })
|
|
94
124
|
|
|
125
|
+
const isSharedDrive = Boolean(existingDocument?.driveId)
|
|
126
|
+
|
|
95
127
|
return (
|
|
96
128
|
<DumbFederatedFolderModal
|
|
97
129
|
title={modalTitle}
|
|
@@ -106,6 +138,7 @@ export const FederatedFolderModal = withLocales(
|
|
|
106
138
|
onClose={onClose}
|
|
107
139
|
onShare={onShare}
|
|
108
140
|
sharingLink={sharingLink}
|
|
141
|
+
showShareByEmail={!isSharedDrive}
|
|
109
142
|
/>
|
|
110
143
|
)
|
|
111
144
|
}
|
|
@@ -26,8 +26,7 @@ const LinkRecipient = props => {
|
|
|
26
26
|
const { isMobile } = useBreakpoints()
|
|
27
27
|
const { getDocumentPermissions } = useSharingContext()
|
|
28
28
|
|
|
29
|
-
const { recipientConfirmationData, verifyRecipient,
|
|
30
|
-
props
|
|
29
|
+
const { recipientConfirmationData, verifyRecipient, fadeIn, document } = props
|
|
31
30
|
|
|
32
31
|
const permissions = getDocumentPermissions(document?._id)
|
|
33
32
|
const hasPassword = checkIsPermissionHasPassword(permissions)
|
|
@@ -44,7 +43,7 @@ const LinkRecipient = props => {
|
|
|
44
43
|
? t('Share.recipients.expires', {
|
|
45
44
|
date: dateFormatted
|
|
46
45
|
})
|
|
47
|
-
:
|
|
46
|
+
: ''
|
|
48
47
|
|
|
49
48
|
const RightPart = recipientConfirmationData ? (
|
|
50
49
|
<RecipientConfirm
|
|
@@ -74,7 +73,14 @@ const LinkRecipient = props => {
|
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
LinkRecipient.propTypes = {
|
|
77
|
-
|
|
76
|
+
recipientConfirmationData: PropTypes.oneOfType([
|
|
77
|
+
PropTypes.bool,
|
|
78
|
+
PropTypes.object
|
|
79
|
+
]),
|
|
80
|
+
verifyRecipient: PropTypes.func,
|
|
81
|
+
fadeIn: PropTypes.bool,
|
|
82
|
+
document: PropTypes.object,
|
|
83
|
+
permissions: PropTypes.arrayOf(PropTypes.object)
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
export default LinkRecipient
|
|
@@ -6,6 +6,17 @@ import { createMockClient } from 'cozy-client'
|
|
|
6
6
|
import MemberRecipient from './MemberRecipient'
|
|
7
7
|
import AppLike from '../../../test/AppLike'
|
|
8
8
|
|
|
9
|
+
const mockUpdateDocumentPermissions = jest.fn()
|
|
10
|
+
const mockUpdateSharingMemberType = jest.fn()
|
|
11
|
+
|
|
12
|
+
jest.mock('../../hooks/useSharingContext', () => ({
|
|
13
|
+
// eslint-disable-next-line react/display-name
|
|
14
|
+
useSharingContext: () => ({
|
|
15
|
+
updateDocumentPermissions: mockUpdateDocumentPermissions,
|
|
16
|
+
updateSharingMemberType: mockUpdateSharingMemberType
|
|
17
|
+
})
|
|
18
|
+
}))
|
|
19
|
+
|
|
9
20
|
describe('MemberRecipient component', () => {
|
|
10
21
|
const client = createMockClient({})
|
|
11
22
|
client.options = {
|
|
@@ -2,6 +2,7 @@ import React, { useState, useRef, useCallback } from 'react'
|
|
|
2
2
|
import { useI18n } from 'twake-i18n'
|
|
3
3
|
|
|
4
4
|
import { useClient } from 'cozy-client'
|
|
5
|
+
import minilog from 'cozy-minilog'
|
|
5
6
|
import ActionsMenu from 'cozy-ui/transpiled/react/ActionsMenu'
|
|
6
7
|
import {
|
|
7
8
|
makeActions,
|
|
@@ -10,9 +11,14 @@ import {
|
|
|
10
11
|
import DropdownButton from 'cozy-ui/transpiled/react/DropdownButton'
|
|
11
12
|
import Spinner from 'cozy-ui/transpiled/react/Spinner'
|
|
12
13
|
import Typography from 'cozy-ui/transpiled/react/Typography'
|
|
14
|
+
import { useAlert } from 'cozy-ui/transpiled/react/providers/Alert'
|
|
13
15
|
|
|
14
|
-
import { permission } from './actions/permission'
|
|
15
16
|
import { revokeMember } from './actions/revokeMember'
|
|
17
|
+
import { setReadOnlySharedPermission } from './actions/setReadOnlySharedPermission'
|
|
18
|
+
import { setReadWriteSharedPermission } from './actions/setReadWriteSharedPermission'
|
|
19
|
+
import { useSharingContext } from '../../hooks/useSharingContext'
|
|
20
|
+
|
|
21
|
+
const log = minilog('MemberRecipientPermissions')
|
|
16
22
|
|
|
17
23
|
const MemberRecipientPermissions = ({
|
|
18
24
|
isOwner,
|
|
@@ -30,6 +36,8 @@ const MemberRecipientPermissions = ({
|
|
|
30
36
|
const { t } = useI18n()
|
|
31
37
|
const buttonRef = useRef()
|
|
32
38
|
const client = useClient()
|
|
39
|
+
const { updateSharingMemberType } = useSharingContext()
|
|
40
|
+
const { showAlert } = useAlert()
|
|
33
41
|
|
|
34
42
|
const [revoking, setRevoking] = useState(false)
|
|
35
43
|
const [isMenuDisplayed, setMenuDisplayed] = useState(false)
|
|
@@ -42,27 +50,72 @@ const MemberRecipientPermissions = ({
|
|
|
42
50
|
!contactIsOwner &&
|
|
43
51
|
((instanceMatchesClient && !isOwner) || isOwner)
|
|
44
52
|
|
|
45
|
-
const toggleMenu = () =>
|
|
46
|
-
|
|
53
|
+
const toggleMenu = useCallback(() => {
|
|
54
|
+
setMenuDisplayed(displayed => !displayed)
|
|
55
|
+
}, [])
|
|
56
|
+
const hideMenu = useCallback(() => {
|
|
57
|
+
setMenuDisplayed(false)
|
|
58
|
+
}, [])
|
|
47
59
|
|
|
48
60
|
const handleRevocation = useCallback(async () => {
|
|
49
61
|
setRevoking(true)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
try {
|
|
63
|
+
if (isOwner) {
|
|
64
|
+
await onRevoke(document, sharingId, memberIndex)
|
|
65
|
+
} else {
|
|
66
|
+
await onRevokeSelf(document)
|
|
67
|
+
}
|
|
68
|
+
} finally {
|
|
69
|
+
setRevoking(false)
|
|
54
70
|
}
|
|
55
|
-
setRevoking(false)
|
|
56
71
|
}, [isOwner, onRevoke, onRevokeSelf, document, sharingId, memberIndex])
|
|
57
72
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
const setType = useCallback(
|
|
74
|
+
async newType => {
|
|
75
|
+
if (newType === type) {
|
|
76
|
+
hideMenu()
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await updateSharingMemberType(sharingId, memberIndex, newType)
|
|
81
|
+
} catch (error) {
|
|
82
|
+
log.error('Failed to change member permission type', error)
|
|
83
|
+
showAlert({
|
|
84
|
+
message: t('Share.members.error.changePermission'),
|
|
85
|
+
severity: 'error',
|
|
86
|
+
variant: 'filled'
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
hideMenu()
|
|
90
|
+
},
|
|
91
|
+
[
|
|
92
|
+
hideMenu,
|
|
93
|
+
memberIndex,
|
|
94
|
+
sharingId,
|
|
95
|
+
showAlert,
|
|
96
|
+
t,
|
|
97
|
+
type,
|
|
98
|
+
updateSharingMemberType
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const actions = makeActions(
|
|
103
|
+
[
|
|
104
|
+
setReadOnlySharedPermission,
|
|
105
|
+
setReadWriteSharedPermission,
|
|
106
|
+
divider,
|
|
107
|
+
revokeMember
|
|
108
|
+
],
|
|
109
|
+
{
|
|
110
|
+
client,
|
|
111
|
+
t,
|
|
112
|
+
type: type ?? 'one-way',
|
|
113
|
+
isOwner,
|
|
114
|
+
isSharedDrive,
|
|
115
|
+
setType,
|
|
116
|
+
handleRevocation
|
|
117
|
+
}
|
|
118
|
+
)
|
|
66
119
|
|
|
67
120
|
return (
|
|
68
121
|
<div className={className}>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { render, fireEvent, screen, act, waitFor } from '@testing-library/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { createMockClient } from 'cozy-client'
|
|
5
|
+
|
|
6
|
+
import MemberRecipientPermissions from './MemberRecipientPermissions'
|
|
7
|
+
import AppLike from '../../../test/AppLike'
|
|
8
|
+
|
|
9
|
+
const mockUpdateSharingMemberType = jest.fn()
|
|
10
|
+
const mockShowAlert = jest.fn()
|
|
11
|
+
|
|
12
|
+
jest.mock('../../hooks/useSharingContext', () => ({
|
|
13
|
+
useSharingContext: () => ({
|
|
14
|
+
updateSharingMemberType: mockUpdateSharingMemberType
|
|
15
|
+
})
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
jest.mock('cozy-ui/transpiled/react/providers/Alert', () => ({
|
|
19
|
+
__esModule: true,
|
|
20
|
+
default: ({ children }) => children,
|
|
21
|
+
useAlert: () => ({
|
|
22
|
+
showAlert: mockShowAlert
|
|
23
|
+
})
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
describe('MemberRecipientPermissions component', () => {
|
|
27
|
+
const client = createMockClient({})
|
|
28
|
+
client.options = {
|
|
29
|
+
uri: 'http://cozy.local:8080'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const defaultProps = {
|
|
33
|
+
isOwner: true,
|
|
34
|
+
isSharedDrive: false,
|
|
35
|
+
status: 'ready',
|
|
36
|
+
instance: 'http://cozy.local:8080',
|
|
37
|
+
type: 'two-way',
|
|
38
|
+
document: { id: 'doc-123', type: 'io.cozy.files', path: '/test' },
|
|
39
|
+
sharingId: 'sharing-123',
|
|
40
|
+
memberIndex: 1,
|
|
41
|
+
onRevoke: jest.fn(),
|
|
42
|
+
onRevokeSelf: jest.fn()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
jest.clearAllMocks()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const setup = props => {
|
|
50
|
+
return render(
|
|
51
|
+
<AppLike client={client}>
|
|
52
|
+
<MemberRecipientPermissions {...defaultProps} {...props} />
|
|
53
|
+
</AppLike>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let createRangeBackup
|
|
58
|
+
|
|
59
|
+
beforeAll(() => {
|
|
60
|
+
createRangeBackup = global.document.createRange
|
|
61
|
+
global.document.createRange = jest.fn(() => ({
|
|
62
|
+
setStart: () => {},
|
|
63
|
+
setEnd: () => {},
|
|
64
|
+
commonAncestorContainer: {
|
|
65
|
+
nodeName: 'BODY',
|
|
66
|
+
ownerDocument: document
|
|
67
|
+
}
|
|
68
|
+
}))
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
afterAll(() => {
|
|
72
|
+
global.document.createRange = createRangeBackup
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should render the current permission type', () => {
|
|
76
|
+
setup()
|
|
77
|
+
expect(screen.getByText('can change')).toBeInTheDocument()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should call updateSharingMemberType when changing to one-way', async () => {
|
|
81
|
+
mockUpdateSharingMemberType.mockResolvedValue(undefined)
|
|
82
|
+
|
|
83
|
+
const { getByText } = setup({ type: 'two-way' })
|
|
84
|
+
|
|
85
|
+
fireEvent.click(getByText('can change'))
|
|
86
|
+
fireEvent.click(getByText('Can view'))
|
|
87
|
+
|
|
88
|
+
expect(mockUpdateSharingMemberType).toHaveBeenCalledWith(
|
|
89
|
+
'sharing-123',
|
|
90
|
+
1,
|
|
91
|
+
'one-way'
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should call updateSharingMemberType when changing to two-way', async () => {
|
|
96
|
+
mockUpdateSharingMemberType.mockResolvedValue(undefined)
|
|
97
|
+
|
|
98
|
+
const { getByText } = setup({ type: 'one-way' })
|
|
99
|
+
|
|
100
|
+
fireEvent.click(getByText('can view'))
|
|
101
|
+
fireEvent.click(getByText('Can change'))
|
|
102
|
+
|
|
103
|
+
expect(mockUpdateSharingMemberType).toHaveBeenCalledWith(
|
|
104
|
+
'sharing-123',
|
|
105
|
+
1,
|
|
106
|
+
'two-way'
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should show error alert when updateSharingMemberType fails', async () => {
|
|
111
|
+
mockUpdateSharingMemberType.mockRejectedValue(new Error('Network error'))
|
|
112
|
+
|
|
113
|
+
const { getByText } = setup({ type: 'two-way' })
|
|
114
|
+
|
|
115
|
+
await act(async () => {
|
|
116
|
+
fireEvent.click(getByText('can change'))
|
|
117
|
+
fireEvent.click(getByText('Can view'))
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
await waitFor(() => {
|
|
121
|
+
expect(mockShowAlert).toHaveBeenCalledWith({
|
|
122
|
+
message:
|
|
123
|
+
'An error occurred while changing the permissions. Please try again.',
|
|
124
|
+
severity: 'error',
|
|
125
|
+
variant: 'filled'
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should not call updateSharingMemberType when selecting same type', async () => {
|
|
131
|
+
mockUpdateSharingMemberType.mockResolvedValue(undefined)
|
|
132
|
+
|
|
133
|
+
const { getByText } = setup({ type: 'two-way' })
|
|
134
|
+
|
|
135
|
+
fireEvent.click(getByText('can change'))
|
|
136
|
+
fireEvent.click(getByText('Can change'))
|
|
137
|
+
|
|
138
|
+
expect(mockUpdateSharingMemberType).not.toHaveBeenCalled()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should not render menu when user is not owner and not the shared drive contact', () => {
|
|
142
|
+
const { queryByText } = setup({
|
|
143
|
+
isOwner: false,
|
|
144
|
+
instance: 'http://other-cozy.local:8080'
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
expect(queryByText('can change')).not.toBeInTheDocument()
|
|
148
|
+
expect(queryByText('can view')).not.toBeInTheDocument()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should render menu for owner', () => {
|
|
152
|
+
const { getByText } = setup({ isOwner: true })
|
|
153
|
+
|
|
154
|
+
expect(getByText('can change')).toBeInTheDocument()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should render menu for the user themselves', () => {
|
|
158
|
+
const { getByText } = setup({
|
|
159
|
+
isOwner: false,
|
|
160
|
+
instance: 'http://cozy.local:8080'
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
expect(getByText('can change')).toBeInTheDocument()
|
|
164
|
+
})
|
|
165
|
+
})
|