cozy-sharing 33.3.4 → 33.4.1
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 +17 -0
- package/dist/SharingProvider.js +176 -84
- package/dist/SharingProvider.spec.js +353 -54
- package/dist/components/Avatar/MemberAvatar.js +21 -5
- package/dist/components/Avatar/MemberAvatar.spec.js +77 -0
- package/dist/components/FederatedFolder/FederatedFolderModal.js +70 -7
- package/dist/components/FederatedFolder/FederatedFolderModal.spec.js +234 -85
- package/dist/components/Recipient/OwnerRecipientDefault.js +8 -1
- package/dist/components/Recipient/OwnerRecipientDefaultLite.js +5 -1
- package/package.json +4 -4
- package/src/SharingProvider.jsx +49 -7
- package/src/SharingProvider.spec.jsx +204 -0
- package/src/components/Avatar/MemberAvatar.jsx +19 -4
- package/src/components/Avatar/MemberAvatar.spec.jsx +80 -0
- package/src/components/FederatedFolder/FederatedFolderModal.jsx +44 -3
- package/src/components/FederatedFolder/FederatedFolderModal.spec.jsx +87 -2
- package/src/components/Recipient/OwnerRecipientDefault.jsx +8 -0
- package/src/components/Recipient/OwnerRecipientDefaultLite.jsx +5 -0
|
@@ -2,7 +2,10 @@ import React from 'react';
|
|
|
2
2
|
import { useClient, useQuery, hasQueryBeenLoaded } from 'cozy-client';
|
|
3
3
|
import MemberRecipientLite from './MemberRecipientLite';
|
|
4
4
|
import withLocales from '../../hoc/withLocales';
|
|
5
|
-
import { buildInstanceSettingsQuery } from '../../queries/queries';
|
|
5
|
+
import { buildInstanceSettingsQuery } from '../../queries/queries'; // See OwnerRecipientDefault: show the instance public avatar (with an initials
|
|
6
|
+
// fallback) when the owner has no per-sharing avatar to reference.
|
|
7
|
+
|
|
8
|
+
var OWNER_PUBLIC_AVATAR_PATH = '/public/avatar?fallback=initials';
|
|
6
9
|
|
|
7
10
|
var OwnerRecipientDefaultLite = function OwnerRecipientDefaultLite() {
|
|
8
11
|
var _instanceSettingsResu;
|
|
@@ -14,6 +17,7 @@ var OwnerRecipientDefaultLite = function OwnerRecipientDefaultLite() {
|
|
|
14
17
|
recipient: {
|
|
15
18
|
status: 'owner',
|
|
16
19
|
instance: client.options.uri,
|
|
20
|
+
avatarPath: OWNER_PUBLIC_AVATAR_PATH,
|
|
17
21
|
public_name: instanceSettingsResult === null || instanceSettingsResult === void 0 || (_instanceSettingsResu = instanceSettingsResult.data) === null || _instanceSettingsResu === void 0 || (_instanceSettingsResu = _instanceSettingsResu.attributes) === null || _instanceSettingsResu === void 0 ? void 0 : _instanceSettingsResu.public_name
|
|
18
22
|
},
|
|
19
23
|
isOwner: true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozy-sharing",
|
|
3
|
-
"version": "33.
|
|
3
|
+
"version": "33.4.1",
|
|
4
4
|
"description": "Provides sharing login for React applications.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"author": "Cozy",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"babel-jest": "30.3.0",
|
|
54
54
|
"babel-plugin-css-modules-transform": "1.6.2",
|
|
55
55
|
"babel-plugin-inline-react-svg": "1.1.2",
|
|
56
|
-
"cozy-client": "^60.
|
|
56
|
+
"cozy-client": "^60.28.0",
|
|
57
57
|
"cozy-device-helper": "^4.0.0",
|
|
58
58
|
"cozy-intent": "^2.31.1",
|
|
59
59
|
"cozy-minilog": "^3.10.1",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"twake-i18n": "^0.4.1"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"cozy-client": ">=60.
|
|
71
|
+
"cozy-client": ">=60.28.0",
|
|
72
72
|
"cozy-device-helper": ">=4.0.0",
|
|
73
73
|
"cozy-flags": ">=4.8.1",
|
|
74
74
|
"cozy-intent": ">=2.29.1",
|
|
@@ -83,5 +83,5 @@
|
|
|
83
83
|
"sideEffects": [
|
|
84
84
|
"*.css"
|
|
85
85
|
],
|
|
86
|
-
"gitHead": "
|
|
86
|
+
"gitHead": "a2e29d6c728ebf0fdc890666a9ec3505e90551ed"
|
|
87
87
|
}
|
package/src/SharingProvider.jsx
CHANGED
|
@@ -87,6 +87,7 @@ export class SharingProvider extends Component {
|
|
|
87
87
|
revokeSelf: this.revokeSelf,
|
|
88
88
|
shareByLink: this.shareByLink,
|
|
89
89
|
getFederatedShareLink: this.getFederatedShareLink,
|
|
90
|
+
fetchSharedDriveSharingLinks: this.fetchSharedDriveSharingLinks,
|
|
90
91
|
updateDocumentPermissions: this.updateDocumentPermissions,
|
|
91
92
|
revokeSharingLink: this.revokeSharingLink,
|
|
92
93
|
hasLoadedAtLeastOnePage: false,
|
|
@@ -373,17 +374,30 @@ export class SharingProvider extends Component {
|
|
|
373
374
|
}
|
|
374
375
|
|
|
375
376
|
shareByLink = async (document, options) => {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
377
|
+
try {
|
|
378
|
+
const resp = document.driveId
|
|
379
|
+
? await this.createSharedDriveSharingLink(document, options)
|
|
380
|
+
: await this.permissionCol.createSharingLink(document, options)
|
|
381
|
+
this.dispatch(addSharingLink(resp.data))
|
|
382
|
+
return resp
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (document.driveId && error && error.status === 409) {
|
|
385
|
+
const permissions = await this.fetchSharedDriveSharingLinks(document)
|
|
386
|
+
if (permissions && permissions.length > 0) {
|
|
387
|
+
return { data: permissions[0] }
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
throw error
|
|
391
|
+
}
|
|
381
392
|
}
|
|
382
393
|
|
|
383
394
|
getFederatedShareLink = document => {
|
|
384
|
-
if (!document
|
|
395
|
+
if (!document?.driveId) return null
|
|
396
|
+
|
|
397
|
+
const documentId = document._id || document.id
|
|
398
|
+
if (!documentId) return null
|
|
385
399
|
|
|
386
|
-
const permissions = getDocumentPermissions(this.state,
|
|
400
|
+
const permissions = getDocumentPermissions(this.state, documentId)
|
|
387
401
|
const perm = permissions.find(p => getShortcode(p))
|
|
388
402
|
if (!perm) return null
|
|
389
403
|
|
|
@@ -413,6 +427,34 @@ export class SharingProvider extends Component {
|
|
|
413
427
|
return resp
|
|
414
428
|
}
|
|
415
429
|
|
|
430
|
+
fetchSharedDriveSharingLinks = async document => {
|
|
431
|
+
if (!document || !document.driveId) return []
|
|
432
|
+
|
|
433
|
+
const { client } = this.props
|
|
434
|
+
const drivePermissionCollection = client.collection('io.cozy.permissions', {
|
|
435
|
+
driveId: document.driveId
|
|
436
|
+
})
|
|
437
|
+
const documentId = document._id || document.id
|
|
438
|
+
if (!documentId) return []
|
|
439
|
+
|
|
440
|
+
const resp = await drivePermissionCollection.findLinksByIds([documentId])
|
|
441
|
+
const permissions = resp.data || []
|
|
442
|
+
|
|
443
|
+
const existingPermissions = getDocumentPermissions(this.state, documentId)
|
|
444
|
+
const existingPermissionIds = existingPermissions.map(
|
|
445
|
+
permission => permission.id
|
|
446
|
+
)
|
|
447
|
+
const newPermissions = permissions.filter(
|
|
448
|
+
permission => !existingPermissionIds.includes(permission.id)
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
if (newPermissions.length > 0) {
|
|
452
|
+
this.dispatch(addSharingLink(newPermissions))
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return permissions
|
|
456
|
+
}
|
|
457
|
+
|
|
416
458
|
updateSharedDrivePermissions = async (
|
|
417
459
|
document,
|
|
418
460
|
permissionDocument,
|
|
@@ -291,6 +291,210 @@ describe('updateDocumentPermissions', () => {
|
|
|
291
291
|
})
|
|
292
292
|
})
|
|
293
293
|
|
|
294
|
+
describe('fetchSharedDriveSharingLinks', () => {
|
|
295
|
+
const PERM_DRIVE_FILE = {
|
|
296
|
+
type: 'io.cozy.permissions',
|
|
297
|
+
id: 'perm_drive_file',
|
|
298
|
+
attributes: {
|
|
299
|
+
type: 'share',
|
|
300
|
+
permissions: {
|
|
301
|
+
rule0: {
|
|
302
|
+
type: 'io.cozy.files',
|
|
303
|
+
verbs: ['GET'],
|
|
304
|
+
values: ['file_in_drive']
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
shortcodes: { code: 'shortcode123' }
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const driveFile = {
|
|
312
|
+
_id: 'file_in_drive',
|
|
313
|
+
id: 'file_in_drive',
|
|
314
|
+
driveId: 'drive_123'
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
it('fetches shared drive sharing links by file id', async () => {
|
|
318
|
+
const mockFindLinksByIds = jest
|
|
319
|
+
.fn()
|
|
320
|
+
.mockResolvedValue({ data: [PERM_DRIVE_FILE] })
|
|
321
|
+
const mockClient = createMockClient({})
|
|
322
|
+
mockClient.collection = jest.fn().mockReturnValue({
|
|
323
|
+
findLinksByIds: mockFindLinksByIds
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const provider = new SharingProvider({ client: mockClient })
|
|
327
|
+
provider.state = reducer()
|
|
328
|
+
provider.dispatch = jest.fn(action => {
|
|
329
|
+
provider.state = reducer(provider.state, action)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
const result = await provider.fetchSharedDriveSharingLinks(driveFile)
|
|
333
|
+
|
|
334
|
+
expect(mockClient.collection).toHaveBeenCalledWith('io.cozy.permissions', {
|
|
335
|
+
driveId: 'drive_123'
|
|
336
|
+
})
|
|
337
|
+
expect(mockFindLinksByIds).toHaveBeenCalledWith(['file_in_drive'])
|
|
338
|
+
expect(provider.dispatch).toHaveBeenCalled()
|
|
339
|
+
const permissions = getDocumentPermissions(provider.state, 'file_in_drive')
|
|
340
|
+
expect(permissions).toHaveLength(1)
|
|
341
|
+
expect(permissions[0].id).toBe(PERM_DRIVE_FILE.id)
|
|
342
|
+
expect(result).toEqual([PERM_DRIVE_FILE])
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('returns an empty array and skips the API call for non-shared-drive documents', async () => {
|
|
346
|
+
const mockClient = createMockClient({})
|
|
347
|
+
const findLinksByIds = jest.fn()
|
|
348
|
+
mockClient.collection = jest.fn().mockReturnValue({
|
|
349
|
+
findLinksByIds
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
const provider = new SharingProvider({ client: mockClient })
|
|
353
|
+
provider.dispatch = jest.fn()
|
|
354
|
+
|
|
355
|
+
const result = await provider.fetchSharedDriveSharingLinks({
|
|
356
|
+
_id: 'regular_file'
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
expect(result).toEqual([])
|
|
360
|
+
expect(findLinksByIds).not.toHaveBeenCalled()
|
|
361
|
+
expect(provider.dispatch).not.toHaveBeenCalled()
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('uses document.id as a fallback when _id is missing', async () => {
|
|
365
|
+
const mockFindLinksByIds = jest
|
|
366
|
+
.fn()
|
|
367
|
+
.mockResolvedValue({ data: [PERM_DRIVE_FILE] })
|
|
368
|
+
const mockClient = createMockClient({})
|
|
369
|
+
mockClient.collection = jest.fn().mockReturnValue({
|
|
370
|
+
findLinksByIds: mockFindLinksByIds
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const provider = new SharingProvider({ client: mockClient })
|
|
374
|
+
provider.state = reducer()
|
|
375
|
+
provider.dispatch = jest.fn(action => {
|
|
376
|
+
provider.state = reducer(provider.state, action)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
await provider.fetchSharedDriveSharingLinks({
|
|
380
|
+
id: 'file_in_drive',
|
|
381
|
+
driveId: 'drive_123'
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
expect(mockFindLinksByIds).toHaveBeenCalledWith(['file_in_drive'])
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('does not dispatch when the response has no data', async () => {
|
|
388
|
+
const mockFindLinksByIds = jest.fn().mockResolvedValue({ data: [] })
|
|
389
|
+
const mockClient = createMockClient({})
|
|
390
|
+
mockClient.collection = jest.fn().mockReturnValue({
|
|
391
|
+
findLinksByIds: mockFindLinksByIds
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
const provider = new SharingProvider({ client: mockClient })
|
|
395
|
+
provider.dispatch = jest.fn()
|
|
396
|
+
|
|
397
|
+
const result = await provider.fetchSharedDriveSharingLinks(driveFile)
|
|
398
|
+
|
|
399
|
+
expect(result).toEqual([])
|
|
400
|
+
expect(provider.dispatch).not.toHaveBeenCalled()
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
describe('shareByLink shared drive 409 recovery', () => {
|
|
405
|
+
const PERM_DRIVE_FILE = {
|
|
406
|
+
type: 'io.cozy.permissions',
|
|
407
|
+
id: 'perm_drive_file',
|
|
408
|
+
attributes: {
|
|
409
|
+
type: 'share',
|
|
410
|
+
permissions: {
|
|
411
|
+
rule0: {
|
|
412
|
+
type: 'io.cozy.files',
|
|
413
|
+
verbs: ['GET'],
|
|
414
|
+
values: ['file_in_drive']
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
shortcodes: { code: 'shortcode123' }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const driveFile = {
|
|
422
|
+
_id: 'file_in_drive',
|
|
423
|
+
id: 'file_in_drive',
|
|
424
|
+
driveId: 'drive_123'
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
it('falls back to fetching existing links when create returns 409', async () => {
|
|
428
|
+
const conflict = Object.assign(new Error('Conflict'), { status: 409 })
|
|
429
|
+
const mockCreateSharingLink = jest.fn().mockRejectedValue(conflict)
|
|
430
|
+
const mockFindLinksByIds = jest
|
|
431
|
+
.fn()
|
|
432
|
+
.mockResolvedValue({ data: [PERM_DRIVE_FILE] })
|
|
433
|
+
const mockClient = createMockClient({})
|
|
434
|
+
mockClient.collection = jest.fn().mockReturnValue({
|
|
435
|
+
createSharingLink: mockCreateSharingLink,
|
|
436
|
+
findLinksByIds: mockFindLinksByIds
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
const provider = new SharingProvider({ client: mockClient })
|
|
440
|
+
provider.state = reducer()
|
|
441
|
+
provider.dispatch = jest.fn(action => {
|
|
442
|
+
provider.state = reducer(provider.state, action)
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
const resp = await provider.shareByLink(driveFile, { verbs: ['GET'] })
|
|
446
|
+
|
|
447
|
+
expect(mockCreateSharingLink).toHaveBeenCalled()
|
|
448
|
+
expect(mockFindLinksByIds).toHaveBeenCalledWith(['file_in_drive'])
|
|
449
|
+
expect(resp.data.id).toBe(PERM_DRIVE_FILE.id)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('does not duplicate existing links when 409 recovery refetches them', async () => {
|
|
453
|
+
const conflict = Object.assign(new Error('Conflict'), { status: 409 })
|
|
454
|
+
const mockCreateSharingLink = jest.fn().mockRejectedValue(conflict)
|
|
455
|
+
const mockFindLinksByIds = jest
|
|
456
|
+
.fn()
|
|
457
|
+
.mockResolvedValue({ data: [PERM_DRIVE_FILE] })
|
|
458
|
+
const mockClient = createMockClient({})
|
|
459
|
+
mockClient.collection = jest.fn().mockReturnValue({
|
|
460
|
+
createSharingLink: mockCreateSharingLink,
|
|
461
|
+
findLinksByIds: mockFindLinksByIds
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
const provider = new SharingProvider({ client: mockClient })
|
|
465
|
+
provider.state = reducer(undefined, addSharingLink(PERM_DRIVE_FILE))
|
|
466
|
+
provider.dispatch = jest.fn(action => {
|
|
467
|
+
provider.state = reducer(provider.state, action)
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
const resp = await provider.shareByLink(driveFile, { verbs: ['GET'] })
|
|
471
|
+
|
|
472
|
+
const permissions = getDocumentPermissions(provider.state, 'file_in_drive')
|
|
473
|
+
expect(resp.data.id).toBe(PERM_DRIVE_FILE.id)
|
|
474
|
+
expect(provider.dispatch).not.toHaveBeenCalled()
|
|
475
|
+
expect(permissions).toHaveLength(1)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('rethrows the original error when 409 happens and no link is found', async () => {
|
|
479
|
+
const conflict = Object.assign(new Error('Conflict'), { status: 409 })
|
|
480
|
+
const mockCreateSharingLink = jest.fn().mockRejectedValue(conflict)
|
|
481
|
+
const mockFindLinksByIds = jest.fn().mockResolvedValue({ data: [] })
|
|
482
|
+
const mockClient = createMockClient({})
|
|
483
|
+
mockClient.collection = jest.fn().mockReturnValue({
|
|
484
|
+
createSharingLink: mockCreateSharingLink,
|
|
485
|
+
findLinksByIds: mockFindLinksByIds
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
const provider = new SharingProvider({ client: mockClient })
|
|
489
|
+
provider.state = reducer()
|
|
490
|
+
provider.dispatch = jest.fn()
|
|
491
|
+
|
|
492
|
+
await expect(
|
|
493
|
+
provider.shareByLink(driveFile, { verbs: ['GET'] })
|
|
494
|
+
).rejects.toBe(conflict)
|
|
495
|
+
})
|
|
496
|
+
})
|
|
497
|
+
|
|
294
498
|
describe('updateSharingMemberType', () => {
|
|
295
499
|
const mockSharing = {
|
|
296
500
|
id: 'sharing-123',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useClient } from 'cozy-client'
|
|
4
4
|
import Avatar from 'cozy-ui/transpiled/react/Avatar'
|
|
@@ -8,6 +8,10 @@ import { getInitials } from '../../models'
|
|
|
8
8
|
|
|
9
9
|
const MemberAvatar = ({ recipient, ...rest }) => {
|
|
10
10
|
const client = useClient()
|
|
11
|
+
// Remember which image url failed to load so we can fall back to the
|
|
12
|
+
// initials. Storing the url (instead of a boolean) means a new url is
|
|
13
|
+
// retried automatically, without an effect to reset the error state.
|
|
14
|
+
const [erroredSrc, setErroredSrc] = useState(null)
|
|
11
15
|
|
|
12
16
|
// There are cases when due to apparent memory leaks, the recipient does not exist
|
|
13
17
|
// This will trigger an unhanded error in the Avatar component and crash the app
|
|
@@ -31,13 +35,24 @@ const MemberAvatar = ({ recipient, ...rest }) => {
|
|
|
31
35
|
*/
|
|
32
36
|
const image =
|
|
33
37
|
recipient.avatarPath && recipient.status
|
|
34
|
-
? `${client.options.uri}${recipient.avatarPath}
|
|
38
|
+
? `${client.options.uri}${recipient.avatarPath}${
|
|
39
|
+
recipient.avatarPath.includes('?') ? '&' : '?'
|
|
40
|
+
}v=${recipient.status}`
|
|
35
41
|
: null
|
|
36
42
|
|
|
43
|
+
// The avatar can become unreachable (eg. the related sharing is revoked
|
|
44
|
+
// while its members are still rendered), in which case the image request
|
|
45
|
+
// fails. Without a fallback the browser shows a broken-image icon, so we
|
|
46
|
+
// render the initials instead.
|
|
37
47
|
return (
|
|
38
48
|
<Avatar {...rest}>
|
|
39
|
-
{image ? (
|
|
40
|
-
<img
|
|
49
|
+
{image && erroredSrc !== image ? (
|
|
50
|
+
<img
|
|
51
|
+
width="100%"
|
|
52
|
+
height="100%"
|
|
53
|
+
src={image}
|
|
54
|
+
onError={() => setErroredSrc(image)}
|
|
55
|
+
/>
|
|
41
56
|
) : (
|
|
42
57
|
getInitials(recipient)
|
|
43
58
|
)}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { createMockClient } from 'cozy-client'
|
|
5
|
+
|
|
6
|
+
import { MemberAvatar } from './MemberAvatar'
|
|
7
|
+
import AppLike from '../../../test/AppLike'
|
|
8
|
+
|
|
9
|
+
describe('MemberAvatar', () => {
|
|
10
|
+
const client = createMockClient({})
|
|
11
|
+
client.options = { uri: 'http://cozy.example.com' }
|
|
12
|
+
|
|
13
|
+
const setup = recipient =>
|
|
14
|
+
render(
|
|
15
|
+
<AppLike client={client}>
|
|
16
|
+
<MemberAvatar recipient={recipient} />
|
|
17
|
+
</AppLike>
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
it('renders nothing when the recipient is missing', () => {
|
|
21
|
+
const { container } = setup(undefined)
|
|
22
|
+
|
|
23
|
+
// The component returns null; only the provider wrappers remain, with no
|
|
24
|
+
// avatar rendered inside.
|
|
25
|
+
expect(container.querySelector('[class*="MuiAvatar-root"]')).toBeNull()
|
|
26
|
+
expect(container.querySelector('img')).toBeNull()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('renders the avatar image when the recipient has an avatarPath and a status', () => {
|
|
30
|
+
const { container } = setup({
|
|
31
|
+
public_name: 'Bob',
|
|
32
|
+
status: 'owner',
|
|
33
|
+
avatarPath: '/sharings/123/recipients/0/avatar'
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const img = container.querySelector('img')
|
|
37
|
+
expect(img).toBeTruthy()
|
|
38
|
+
expect(img.getAttribute('src')).toBe(
|
|
39
|
+
'http://cozy.example.com/sharings/123/recipients/0/avatar?v=owner'
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('appends the cache-busting param with & when avatarPath already has a query string', () => {
|
|
44
|
+
const { container } = setup({
|
|
45
|
+
public_name: 'Bob',
|
|
46
|
+
status: 'owner',
|
|
47
|
+
avatarPath: '/public/avatar?fallback=initials'
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
expect(container.querySelector('img').getAttribute('src')).toBe(
|
|
51
|
+
'http://cozy.example.com/public/avatar?fallback=initials&v=owner'
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('renders the initials when there is no avatarPath', () => {
|
|
56
|
+
const { container, getByText } = setup({
|
|
57
|
+
public_name: 'Bob',
|
|
58
|
+
status: 'owner'
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(container.querySelector('img')).toBeNull()
|
|
62
|
+
expect(getByText('B')).toBeTruthy()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('falls back to the initials when the avatar image fails to load', () => {
|
|
66
|
+
const { container, getByText } = setup({
|
|
67
|
+
public_name: 'Bob',
|
|
68
|
+
status: 'owner',
|
|
69
|
+
avatarPath: '/sharings/123/recipients/0/avatar'
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const img = container.querySelector('img')
|
|
73
|
+
expect(img).toBeTruthy()
|
|
74
|
+
|
|
75
|
+
fireEvent.error(img)
|
|
76
|
+
|
|
77
|
+
expect(container.querySelector('img')).toBeNull()
|
|
78
|
+
expect(getByText('B')).toBeTruthy()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import PropTypes from 'prop-types'
|
|
2
|
-
import React, { useCallback, useState } from 'react'
|
|
2
|
+
import React, { useCallback, useEffect, useState } from 'react'
|
|
3
3
|
|
|
4
4
|
import { useClient } from 'cozy-client'
|
|
5
|
+
import minilog from 'cozy-minilog'
|
|
5
6
|
import Button from 'cozy-ui/transpiled/react/Buttons'
|
|
6
7
|
import { FixedDialog } from 'cozy-ui/transpiled/react/CozyDialogs'
|
|
7
8
|
import Typography from 'cozy-ui/transpiled/react/Typography'
|
|
@@ -21,6 +22,8 @@ import { default as DumbShareByEmail } from '../ShareByEmail'
|
|
|
21
22
|
import ShareByLink from '../ShareByLink'
|
|
22
23
|
import WhoHasAccess from '../WhoHasAccess'
|
|
23
24
|
|
|
25
|
+
const log = minilog('FederatedFolderModal')
|
|
26
|
+
|
|
24
27
|
const FederatedFolderModalContent = ({
|
|
25
28
|
onClose,
|
|
26
29
|
document: existingDocument,
|
|
@@ -34,6 +37,8 @@ const FederatedFolderModalContent = ({
|
|
|
34
37
|
getSharingById,
|
|
35
38
|
getSharingLink,
|
|
36
39
|
getFederatedShareLink,
|
|
40
|
+
getDocumentPermissions,
|
|
41
|
+
fetchSharedDriveSharingLinks,
|
|
37
42
|
getRecipients,
|
|
38
43
|
hasSharedChild,
|
|
39
44
|
hasSharedParent,
|
|
@@ -56,6 +61,9 @@ const FederatedFolderModalContent = ({
|
|
|
56
61
|
// in members before the modal closes. That's why when clicking on "Share"
|
|
57
62
|
// we do not use the reactive existingDocument and existingRecipients.
|
|
58
63
|
const [isSending, setIsSending] = useState(false)
|
|
64
|
+
const [isFetchingSharingLinks, setIsFetchingSharingLinks] = useState(false)
|
|
65
|
+
const [fetchedSharingLinksDocumentId, setFetchedSharingLinksDocumentId] =
|
|
66
|
+
useState(null)
|
|
59
67
|
const [frozenDoc, setFrozenDoc] = useState(null)
|
|
60
68
|
const [frozenRecipients, setFrozenRecipients] = useState(null)
|
|
61
69
|
|
|
@@ -169,13 +177,46 @@ const FederatedFolderModalContent = ({
|
|
|
169
177
|
}
|
|
170
178
|
}
|
|
171
179
|
|
|
172
|
-
const
|
|
173
|
-
|
|
180
|
+
const documentId = existingDocument?._id || existingDocument?.id
|
|
181
|
+
const existingRecipients = documentId ? getRecipients(documentId) : []
|
|
182
|
+
const documentPermissions = documentId
|
|
183
|
+
? getDocumentPermissions(documentId)
|
|
174
184
|
: []
|
|
175
185
|
const displayedLink = existingDocument?.driveId
|
|
176
186
|
? getFederatedShareLink(existingDocument)
|
|
177
187
|
: getSharingLink(existingDocument?._id)
|
|
178
188
|
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!existingDocument?.driveId) return
|
|
191
|
+
if (!documentId) return
|
|
192
|
+
if (!fetchSharedDriveSharingLinks) return
|
|
193
|
+
if (documentPermissions.length > 0) return
|
|
194
|
+
if (isFetchingSharingLinks) return
|
|
195
|
+
if (fetchedSharingLinksDocumentId === documentId) return
|
|
196
|
+
|
|
197
|
+
const fetchSharingLinks = async () => {
|
|
198
|
+
setIsFetchingSharingLinks(true)
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await fetchSharedDriveSharingLinks(existingDocument)
|
|
202
|
+
} catch (error) {
|
|
203
|
+
log.error('Failed to fetch shared drive sharing links', error)
|
|
204
|
+
} finally {
|
|
205
|
+
setFetchedSharingLinksDocumentId(documentId)
|
|
206
|
+
setIsFetchingSharingLinks(false)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fetchSharingLinks()
|
|
211
|
+
}, [
|
|
212
|
+
documentId,
|
|
213
|
+
existingDocument,
|
|
214
|
+
documentPermissions.length,
|
|
215
|
+
fetchedSharingLinksDocumentId,
|
|
216
|
+
fetchSharedDriveSharingLinks,
|
|
217
|
+
isFetchingSharingLinks
|
|
218
|
+
])
|
|
219
|
+
|
|
179
220
|
const modalTitle = t('FederatedFolder.shareTitle', { name: folderName })
|
|
180
221
|
|
|
181
222
|
return (
|
|
@@ -17,6 +17,8 @@ const mockGetRecipients = jest.fn().mockReturnValue([])
|
|
|
17
17
|
const mockGetSharingById = jest.fn()
|
|
18
18
|
const mockHasSharedChild = jest.fn()
|
|
19
19
|
const mockHasSharedParent = jest.fn()
|
|
20
|
+
const mockFetchSharedDriveSharingLinks = jest.fn()
|
|
21
|
+
const mockGetDocumentPermissions = jest.fn().mockReturnValue([])
|
|
20
22
|
const mockShowAlert = jest.fn()
|
|
21
23
|
const mockOnClose = jest.fn()
|
|
22
24
|
|
|
@@ -27,7 +29,8 @@ jest.mock('../../hooks/useSharingContext', () => ({
|
|
|
27
29
|
revoke: mockRevoke,
|
|
28
30
|
getSharingLink: mockGetSharingLink,
|
|
29
31
|
getFederatedShareLink: mockGetFederatedShareLink,
|
|
30
|
-
getDocumentPermissions:
|
|
32
|
+
getDocumentPermissions: mockGetDocumentPermissions,
|
|
33
|
+
fetchSharedDriveSharingLinks: mockFetchSharedDriveSharingLinks,
|
|
31
34
|
getOwner: jest.fn(),
|
|
32
35
|
getRecipients: mockGetRecipients,
|
|
33
36
|
getSharingById: mockGetSharingById,
|
|
@@ -65,7 +68,8 @@ const createSharingContextValue = () => ({
|
|
|
65
68
|
hasWriteAccess: jest.fn(),
|
|
66
69
|
getRecipients: jest.fn(),
|
|
67
70
|
getSharingLink: mockGetSharingLink,
|
|
68
|
-
getDocumentPermissions:
|
|
71
|
+
getDocumentPermissions: mockGetDocumentPermissions,
|
|
72
|
+
fetchSharedDriveSharingLinks: mockFetchSharedDriveSharingLinks,
|
|
69
73
|
share: mockShare
|
|
70
74
|
})
|
|
71
75
|
|
|
@@ -96,6 +100,8 @@ describe('FederatedFolderModal', () => {
|
|
|
96
100
|
|
|
97
101
|
beforeEach(() => {
|
|
98
102
|
jest.clearAllMocks()
|
|
103
|
+
mockGetDocumentPermissions.mockReturnValue([])
|
|
104
|
+
mockFetchSharedDriveSharingLinks.mockResolvedValue([])
|
|
99
105
|
mockGetSharingLink.mockReturnValue('https://example.com/share/abc123')
|
|
100
106
|
mockGetFederatedShareLink.mockReturnValue(
|
|
101
107
|
'https://example.com/share/federated-abc123'
|
|
@@ -227,6 +233,85 @@ describe('FederatedFolderModal', () => {
|
|
|
227
233
|
})
|
|
228
234
|
})
|
|
229
235
|
|
|
236
|
+
describe('shared drive link fetch on mount', () => {
|
|
237
|
+
it('should fetch shared drive sharing links when document has a driveId and no permissions yet', async () => {
|
|
238
|
+
mockGetDocumentPermissions.mockReturnValue([])
|
|
239
|
+
|
|
240
|
+
setup({
|
|
241
|
+
document: { ...mockDocument, driveId: 'federated-folder-id' }
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
await waitFor(() => {
|
|
245
|
+
expect(mockFetchSharedDriveSharingLinks).toHaveBeenCalledTimes(1)
|
|
246
|
+
})
|
|
247
|
+
expect(mockFetchSharedDriveSharingLinks).toHaveBeenCalledWith(
|
|
248
|
+
expect.objectContaining({
|
|
249
|
+
_id: mockDocument._id,
|
|
250
|
+
driveId: 'federated-folder-id'
|
|
251
|
+
})
|
|
252
|
+
)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('should use document.id when _id is missing', async () => {
|
|
256
|
+
const document = {
|
|
257
|
+
id: 'folder-id-only',
|
|
258
|
+
name: 'My Test Folder',
|
|
259
|
+
path: '/test/folder',
|
|
260
|
+
driveId: 'federated-folder-id'
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
setup({ document })
|
|
264
|
+
|
|
265
|
+
await waitFor(() => {
|
|
266
|
+
expect(mockFetchSharedDriveSharingLinks).toHaveBeenCalledWith(document)
|
|
267
|
+
})
|
|
268
|
+
expect(mockGetDocumentPermissions).toHaveBeenCalledWith('folder-id-only')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('should not fetch shared drive sharing links when document has no driveId', async () => {
|
|
272
|
+
const { findByText } = setup({ document: mockDocument })
|
|
273
|
+
|
|
274
|
+
await findByText('Copy link')
|
|
275
|
+
|
|
276
|
+
expect(mockFetchSharedDriveSharingLinks).not.toHaveBeenCalled()
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('should not refetch after one attempt when permissions stay empty', async () => {
|
|
280
|
+
const document = { ...mockDocument, driveId: 'federated-folder-id' }
|
|
281
|
+
const { rerender } = setup({ document })
|
|
282
|
+
|
|
283
|
+
await waitFor(() => {
|
|
284
|
+
expect(mockFetchSharedDriveSharingLinks).toHaveBeenCalledTimes(1)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
rerender(
|
|
288
|
+
<AppLike client={client} sharingContextValue={sharingContextValue}>
|
|
289
|
+
<FederatedFolderModal document={document} onClose={mockOnClose} />
|
|
290
|
+
</AppLike>
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
expect(mockFetchSharedDriveSharingLinks).toHaveBeenCalledTimes(1)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should not fetch shared drive sharing links when permissions are already loaded', async () => {
|
|
297
|
+
mockGetDocumentPermissions.mockReturnValue([
|
|
298
|
+
{
|
|
299
|
+
id: 'perm-1',
|
|
300
|
+
type: 'io.cozy.permissions',
|
|
301
|
+
attributes: { shortcodes: { code: 'abc' } }
|
|
302
|
+
}
|
|
303
|
+
])
|
|
304
|
+
|
|
305
|
+
const { findByText } = setup({
|
|
306
|
+
document: { ...mockDocument, driveId: 'federated-folder-id' }
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
await findByText('Copy link')
|
|
310
|
+
|
|
311
|
+
expect(mockFetchSharedDriveSharingLinks).not.toHaveBeenCalled()
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
230
315
|
describe('onSend callback', () => {
|
|
231
316
|
const pendingRecipient = { name: 'Alice', email: 'alice@example.com' }
|
|
232
317
|
|