cozy-sharing 40.1.6 → 40.1.8
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 +8 -0
- package/dist/components/FederatedFolder/FederatedFolderModal.js +10 -12
- package/dist/components/FederatedFolder/FederatedFolderModal.spec.js +2 -2
- package/dist/components/Recipient/DowngradePermissionConfirmDialog.js +3 -12
- package/dist/components/Recipient/GroupRecipient.js +1 -4
- package/dist/components/Recipient/GroupRecipient.stories.js +3 -1
- package/dist/components/Recipient/GroupRecipientDetailWithAccess.js +1 -4
- package/dist/components/Recipient/GroupRecipientPermissions.js +5 -8
- package/dist/components/Recipient/GroupRecipientPermissions.stories.js +2 -0
- package/dist/components/Recipient/LinkRecipient.js +2 -6
- package/dist/components/Recipient/LinkRecipient.stories.js +4 -0
- package/dist/components/Recipient/LinkRecipientPermissions.js +13 -12
- package/dist/components/Recipient/LinkRecipientPermissions.stories.js +5 -1
- package/dist/components/Recipient/MemberRecipient.js +7 -6
- package/dist/components/Recipient/MemberRecipient.spec.js +7 -12
- package/dist/components/Recipient/MemberRecipientPermissions.js +28 -13
- package/dist/components/Recipient/MemberRecipientPermissions.spec.js +5 -14
- package/dist/components/Recipient/MemberRecipientPermissions.stories.js +2 -1
- package/dist/components/Recipient/OwnerRecipient.js +4 -17
- package/dist/components/Recipient/OwnerRecipient.stories.js +3 -1
- package/dist/components/Recipient/OwnerRecipientDefaultLite.js +1 -1
- package/dist/components/Recipient/PermissionTypeMenu.js +32 -155
- package/dist/components/Recipient/PermissionTypeMenuWrapper.js +37 -0
- package/dist/components/Recipient/{PermissionTypeMenu.spec.js → PermissionTypeMenuWrapper.spec.js} +2 -2
- package/dist/components/Recipient/RecipientList.js +10 -0
- package/dist/components/Recipient/RecipientList.stories.js +3 -0
- package/dist/components/{ShareRecipientsInput.js → ShareAutosuggestWrapper.js} +12 -54
- package/dist/components/{ShareRecipientsInput.spec.js → ShareAutosuggestWrapper.spec.js} +3 -3
- package/dist/components/ShareByEmail.js +2 -2
- package/dist/components/ShareDialogCozyToCozy.js +20 -19
- package/dist/components/ShareDialogCozyToCozy.spec.js +3 -0
- package/dist/components/ShareDialogOnlyByLink.js +3 -5
- package/dist/components/ShareDialogOnlyByLink.spec.js +0 -1
- package/dist/components/ShareModal/SharingDetailsModal.js +2 -2
- package/dist/components/SharedDrive/EditSharedDriveModal.js +11 -10
- package/dist/components/SharedDrive/useSharedDrive.js +8 -11
- package/dist/components/SharingBanner/components/PublicBanner.js +8 -6
- package/dist/components/SharingBanner/components/PublicBanner.spec.js +1 -9
- package/dist/components/SharingBanner/components/SharingBanner.js +3 -0
- package/dist/components/SharingBanner/index.js +3 -0
- package/dist/components/SharingContent.js +3 -5
- package/dist/components/WhoHasAccess.js +27 -11
- package/dist/components/WhoHasAccessWrapper.js +122 -0
- package/dist/helpers/contacts.js +79 -1
- package/dist/helpers/contacts.spec.js +122 -1
- package/dist/helpers/recipients.js +46 -0
- package/dist/helpers/recipients.spec.js +89 -0
- package/dist/hooks/useMemberPermissionType.js +191 -0
- package/dist/hooks/useResolveRecipients.js +53 -0
- package/dist/hooks/useResolveRecipients.spec.js +140 -0
- package/dist/index.js +1 -1
- package/docs/share-modal-architecture.md +2 -2
- package/package.json +3 -3
- package/src/components/FederatedFolder/FederatedFolderModal.jsx +6 -12
- package/src/components/FederatedFolder/FederatedFolderModal.spec.jsx +1 -0
- package/src/components/Recipient/DowngradePermissionConfirmDialog.jsx +3 -10
- package/src/components/Recipient/GroupRecipient.jsx +1 -5
- package/src/components/Recipient/GroupRecipient.stories.jsx +3 -1
- package/src/components/Recipient/GroupRecipientDetailWithAccess.jsx +1 -4
- package/src/components/Recipient/GroupRecipientPermissions.jsx +5 -5
- package/src/components/Recipient/GroupRecipientPermissions.stories.jsx +2 -0
- package/src/components/Recipient/LinkRecipient.jsx +2 -4
- package/src/components/Recipient/LinkRecipient.stories.jsx +4 -0
- package/src/components/Recipient/LinkRecipientPermissions.jsx +16 -12
- package/src/components/Recipient/LinkRecipientPermissions.stories.jsx +5 -1
- package/src/components/Recipient/MemberRecipient.jsx +4 -4
- package/src/components/Recipient/MemberRecipient.spec.jsx +7 -10
- package/src/components/Recipient/MemberRecipientPermissions.jsx +29 -13
- package/src/components/Recipient/MemberRecipientPermissions.spec.jsx +5 -11
- package/src/components/Recipient/MemberRecipientPermissions.stories.jsx +2 -1
- package/src/components/Recipient/OwnerRecipient.jsx +4 -17
- package/src/components/Recipient/OwnerRecipient.stories.jsx +1 -1
- package/src/components/Recipient/OwnerRecipientDefaultLite.jsx +1 -1
- package/src/components/Recipient/PermissionTypeMenu.jsx +21 -81
- package/src/components/Recipient/PermissionTypeMenuWrapper.jsx +32 -0
- package/src/components/Recipient/{PermissionTypeMenu.spec.jsx → PermissionTypeMenuWrapper.spec.jsx} +2 -2
- package/src/components/Recipient/RecipientList.jsx +10 -0
- package/src/components/Recipient/RecipientList.stories.jsx +3 -0
- package/src/components/{ShareRecipientsInput.jsx → ShareAutosuggestWrapper.jsx} +14 -65
- package/src/components/{ShareRecipientsInput.spec.jsx → ShareAutosuggestWrapper.spec.jsx} +3 -3
- package/src/components/ShareByEmail.jsx +2 -2
- package/src/components/ShareDialogCozyToCozy.jsx +10 -11
- package/src/components/ShareDialogCozyToCozy.spec.jsx +1 -0
- package/src/components/ShareDialogOnlyByLink.jsx +2 -4
- package/src/components/ShareDialogOnlyByLink.spec.jsx +0 -1
- package/src/components/ShareModal/SharingDetailsModal.jsx +2 -2
- package/src/components/SharedDrive/EditSharedDriveModal.jsx +6 -11
- package/src/components/SharedDrive/useSharedDrive.js +4 -11
- package/src/components/SharingBanner/components/PublicBanner.jsx +6 -6
- package/src/components/SharingBanner/components/PublicBanner.spec.jsx +1 -8
- package/src/components/SharingBanner/components/SharingBanner.jsx +7 -1
- package/src/components/SharingBanner/index.jsx +4 -0
- package/src/components/SharingContent.jsx +2 -4
- package/src/components/WhoHasAccess.jsx +26 -12
- package/src/components/WhoHasAccessWrapper.jsx +117 -0
- package/src/helpers/contacts.js +83 -0
- package/src/helpers/contacts.spec.js +78 -1
- package/src/helpers/recipients.js +44 -0
- package/src/helpers/recipients.spec.js +82 -0
- package/src/hooks/useMemberPermissionType.js +110 -0
- package/src/hooks/useResolveRecipients.js +38 -0
- package/src/hooks/useResolveRecipients.spec.js +70 -0
- package/src/index.jsx +1 -1
- package/dist/components/Recipient/OwnerRecipientDefault.js +0 -27
- package/dist/components/Recipient/OwnerRecipientDefault.stories.js +0 -10
- package/src/components/Recipient/OwnerRecipientDefault.jsx +0 -37
- package/src/components/Recipient/OwnerRecipientDefault.stories.jsx +0 -13
package/src/helpers/contacts.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { models } from 'cozy-client'
|
|
2
|
+
import flag from 'cozy-flags'
|
|
3
|
+
|
|
1
4
|
import { validateEmail } from '../helpers/email'
|
|
2
5
|
|
|
6
|
+
const ContactModel = models.contact
|
|
7
|
+
|
|
3
8
|
// Return the contact object if present
|
|
4
9
|
// Else check if a contact exist with this email address, if yes return it
|
|
5
10
|
// if not create it
|
|
@@ -59,3 +64,81 @@ export const getContactsFromGroupId = (contacts = [], groupId) => {
|
|
|
59
64
|
export const isContactToBeCreated = contact => {
|
|
60
65
|
return !contact._id
|
|
61
66
|
}
|
|
67
|
+
|
|
68
|
+
const isCurrentUser = contact => contact.me === true
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Computes the contacts and groups to suggest when adding recipients to a
|
|
72
|
+
* sharing. The unreachable contacts only count in the members of the groups,
|
|
73
|
+
* when flag sharing.show-recipient-groups is enabled.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} params
|
|
76
|
+
* @param {object[]} params.reachableContacts - Contacts that can be shared with
|
|
77
|
+
* @param {object[]} params.unreachableContactsWithGroups - Contacts that cannot be shared with but belong to a group
|
|
78
|
+
* @param {object[]} params.contactGroups - Groups of contacts
|
|
79
|
+
* @param {object[]} params.currentRecipients - Recipients already in the sharing
|
|
80
|
+
* @param {object[]} params.pendingRecipients - Recipients picked but not yet shared with
|
|
81
|
+
* @returns {object[]} The contacts, then the groups with their members
|
|
82
|
+
*/
|
|
83
|
+
export const computeContactsAndGroups = ({
|
|
84
|
+
reachableContacts,
|
|
85
|
+
unreachableContactsWithGroups,
|
|
86
|
+
contactGroups,
|
|
87
|
+
currentRecipients,
|
|
88
|
+
pendingRecipients
|
|
89
|
+
}) => {
|
|
90
|
+
const suggestedContacts = reachableContacts.filter(
|
|
91
|
+
contact =>
|
|
92
|
+
!isCurrentUser(contact) &&
|
|
93
|
+
// We do not want contact already in the sharing
|
|
94
|
+
!currentRecipients.find(
|
|
95
|
+
r => r.email === ContactModel.getPrimaryEmail(contact)
|
|
96
|
+
) &&
|
|
97
|
+
// We do not want contact currently in pending contacts (in chips)
|
|
98
|
+
!pendingRecipients.find(r => r._id === contact._id)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const unreachableContacts = unreachableContactsWithGroups.filter(
|
|
102
|
+
contact => !isCurrentUser(contact)
|
|
103
|
+
)
|
|
104
|
+
const currentRecipientGroups = currentRecipients
|
|
105
|
+
.map(({ id }) => id)
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
const suggestedGroups = contactGroups
|
|
108
|
+
.filter(
|
|
109
|
+
({ _id }) =>
|
|
110
|
+
!flag('sharing.show-recipient-groups') ||
|
|
111
|
+
!currentRecipientGroups.includes(_id)
|
|
112
|
+
)
|
|
113
|
+
.map(contactGroup => {
|
|
114
|
+
const reachableMembers = getContactsFromGroupId(
|
|
115
|
+
suggestedContacts,
|
|
116
|
+
contactGroup.id
|
|
117
|
+
).map(contact => ({
|
|
118
|
+
...contact,
|
|
119
|
+
isReachable: true
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
if (flag('sharing.show-recipient-groups') !== true) {
|
|
123
|
+
return {
|
|
124
|
+
...contactGroup,
|
|
125
|
+
members: reachableMembers
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const unreachableMembers = getContactsFromGroupId(
|
|
130
|
+
unreachableContacts,
|
|
131
|
+
contactGroup.id
|
|
132
|
+
).map(contact => ({
|
|
133
|
+
...contact,
|
|
134
|
+
isReachable: false
|
|
135
|
+
}))
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
...contactGroup,
|
|
139
|
+
members: [...reachableMembers, ...unreachableMembers]
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
return [...suggestedContacts, ...suggestedGroups]
|
|
144
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import flag from 'cozy-flags'
|
|
2
|
+
|
|
3
|
+
import { computeContactsAndGroups, getOrCreateFromArray } from './contacts'
|
|
4
|
+
|
|
5
|
+
jest.mock('cozy-flags', () => ({
|
|
6
|
+
__esModule: true,
|
|
7
|
+
default: jest.fn(() => null)
|
|
8
|
+
}))
|
|
2
9
|
|
|
3
10
|
const findMock = jest
|
|
4
11
|
.fn()
|
|
@@ -51,3 +58,73 @@ describe('getOrCreateFromArray', () => {
|
|
|
51
58
|
])
|
|
52
59
|
})
|
|
53
60
|
})
|
|
61
|
+
|
|
62
|
+
describe('computeContactsAndGroups', () => {
|
|
63
|
+
const group = { id: 'friends', _id: 'friends', name: 'Friends' }
|
|
64
|
+
const inGroup = { relationships: { groups: { data: [{ _id: 'friends' }] } } }
|
|
65
|
+
const alice = {
|
|
66
|
+
_id: 'alice',
|
|
67
|
+
email: [{ address: 'alice@cozy.cc', primary: true }],
|
|
68
|
+
...inGroup
|
|
69
|
+
}
|
|
70
|
+
const bob = { _id: 'bob', email: [{ address: 'bob@cozy.cc' }], ...inGroup }
|
|
71
|
+
const me = { _id: 'me', me: true, ...inGroup }
|
|
72
|
+
const carol = { _id: 'carol', ...inGroup }
|
|
73
|
+
|
|
74
|
+
const setup = ({
|
|
75
|
+
currentRecipients = [],
|
|
76
|
+
pendingRecipients = [],
|
|
77
|
+
showRecipientGroups = null
|
|
78
|
+
} = {}) => {
|
|
79
|
+
flag.mockReturnValue(showRecipientGroups)
|
|
80
|
+
return computeContactsAndGroups({
|
|
81
|
+
reachableContacts: [alice, bob, me],
|
|
82
|
+
unreachableContactsWithGroups: [carol, me],
|
|
83
|
+
contactGroups: [group],
|
|
84
|
+
currentRecipients,
|
|
85
|
+
pendingRecipients
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
it('suggests the contacts, then the groups with their reachable members', () => {
|
|
90
|
+
const [first, second, friends] = setup()
|
|
91
|
+
|
|
92
|
+
expect([first._id, second._id, friends._id]).toEqual([
|
|
93
|
+
'alice',
|
|
94
|
+
'bob',
|
|
95
|
+
'friends'
|
|
96
|
+
])
|
|
97
|
+
expect(friends.members.map(m => [m._id, m.isReachable])).toEqual([
|
|
98
|
+
['alice', true],
|
|
99
|
+
['bob', true]
|
|
100
|
+
])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('does not suggest the recipients already shared with or picked', () => {
|
|
104
|
+
const contactsAndGroups = setup({
|
|
105
|
+
currentRecipients: [{ email: 'alice@cozy.cc' }],
|
|
106
|
+
pendingRecipients: [bob]
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
expect(contactsAndGroups.map(c => c._id)).toEqual(['friends'])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('adds the unreachable members when flag sharing.show-recipient-groups is enabled', () => {
|
|
113
|
+
const [, , friends] = setup({ showRecipientGroups: true })
|
|
114
|
+
|
|
115
|
+
expect(friends.members.map(m => [m._id, m.isReachable])).toEqual([
|
|
116
|
+
['alice', true],
|
|
117
|
+
['bob', true],
|
|
118
|
+
['carol', false]
|
|
119
|
+
])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('does not suggest the groups already in the sharing when flag sharing.show-recipient-groups is enabled', () => {
|
|
123
|
+
const contactsAndGroups = setup({
|
|
124
|
+
currentRecipients: [{ id: 'friends' }],
|
|
125
|
+
showRecipientGroups: true
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
expect(contactsAndGroups.map(c => c._id)).toEqual(['alice', 'bob'])
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -99,3 +99,47 @@ export const getGroupRecipientSecondaryText = ({
|
|
|
99
99
|
(isUserInsideMembers ? ` (${t('GroupRecipient.secondary_you')})` : '')
|
|
100
100
|
)
|
|
101
101
|
}
|
|
102
|
+
|
|
103
|
+
const isInstance = (recipient, instanceUri) =>
|
|
104
|
+
recipient.instance !== undefined && recipient.instance === instanceUri
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Flags with `isCurrentUser` the recipients, and the members of group
|
|
108
|
+
* recipients, whose instance is the current user's one
|
|
109
|
+
*
|
|
110
|
+
* @param {object[]} recipients - Recipients of a sharing
|
|
111
|
+
* @param {string} instanceUri - URI of the current user's instance
|
|
112
|
+
* @returns {object[]} The recipients with their `isCurrentUser` flag
|
|
113
|
+
*/
|
|
114
|
+
export const markCurrentUser = (recipients, instanceUri) =>
|
|
115
|
+
recipients.map(recipient => ({
|
|
116
|
+
...recipient,
|
|
117
|
+
isCurrentUser: isInstance(recipient, instanceUri),
|
|
118
|
+
...(recipient.members && {
|
|
119
|
+
members: recipient.members.map(member => ({
|
|
120
|
+
...member,
|
|
121
|
+
isCurrentUser: isInstance(member, instanceUri)
|
|
122
|
+
}))
|
|
123
|
+
})
|
|
124
|
+
}))
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {object} params
|
|
128
|
+
* @param {string} params.status - Status of the member in the sharing
|
|
129
|
+
* @param {boolean} params.isCurrentUser - Whether the member is the current user
|
|
130
|
+
* @param {boolean} params.isOwner - Whether the current user owns the sharing
|
|
131
|
+
* @param {boolean} params.canManageSharing - Whether the current user can manage the sharing
|
|
132
|
+
* @param {boolean} params.canManageMembers - Whether the members can be managed
|
|
133
|
+
* @returns {boolean} Whether the current user can change the member's
|
|
134
|
+
* permission type or revoke them
|
|
135
|
+
*/
|
|
136
|
+
export const hasMemberActions = ({
|
|
137
|
+
status,
|
|
138
|
+
isCurrentUser,
|
|
139
|
+
isOwner,
|
|
140
|
+
canManageSharing,
|
|
141
|
+
canManageMembers
|
|
142
|
+
}) =>
|
|
143
|
+
canManageMembers &&
|
|
144
|
+
status !== 'owner' &&
|
|
145
|
+
((isCurrentUser && !isOwner) || canManageSharing)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { hasMemberActions, markCurrentUser } from './recipients'
|
|
2
|
+
|
|
3
|
+
const instanceUri = 'https://alice.cozy.cc'
|
|
4
|
+
|
|
5
|
+
describe('markCurrentUser', () => {
|
|
6
|
+
it('flags the recipient whose instance is the current one', () => {
|
|
7
|
+
const recipients = [
|
|
8
|
+
{ email: 'alice@cozy.cc', instance: instanceUri },
|
|
9
|
+
{ email: 'bob@cozy.cc', instance: 'https://bob.cozy.cc' },
|
|
10
|
+
{ email: 'carol@cozy.cc' }
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
expect(
|
|
14
|
+
markCurrentUser(recipients, instanceUri).map(r => r.isCurrentUser)
|
|
15
|
+
).toEqual([true, false, false])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('does not flag a recipient without instance when the instance is unknown', () => {
|
|
19
|
+
expect(markCurrentUser([{ email: 'carol@cozy.cc' }], undefined)).toEqual([
|
|
20
|
+
{ email: 'carol@cozy.cc', isCurrentUser: false }
|
|
21
|
+
])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('flags the members of group recipients', () => {
|
|
25
|
+
const recipients = [
|
|
26
|
+
{
|
|
27
|
+
name: 'Team',
|
|
28
|
+
members: [
|
|
29
|
+
{ instance: 'https://bob.cozy.cc' },
|
|
30
|
+
{ instance: instanceUri }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const [group] = markCurrentUser(recipients, instanceUri)
|
|
36
|
+
|
|
37
|
+
expect(group.isCurrentUser).toBe(false)
|
|
38
|
+
expect(group.members.map(m => m.isCurrentUser)).toEqual([false, true])
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('hasMemberActions', () => {
|
|
43
|
+
const ownerRights = {
|
|
44
|
+
isOwner: true,
|
|
45
|
+
canManageSharing: true,
|
|
46
|
+
canManageMembers: true
|
|
47
|
+
}
|
|
48
|
+
const memberRights = {
|
|
49
|
+
isOwner: false,
|
|
50
|
+
canManageSharing: false,
|
|
51
|
+
canManageMembers: true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
it('gives actions on the members to whom can manage the sharing', () => {
|
|
55
|
+
expect(hasMemberActions({ ...ownerRights, status: 'ready' })).toBe(true)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('gives no action on the owner', () => {
|
|
59
|
+
expect(hasMemberActions({ ...ownerRights, status: 'owner' })).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('gives no action when the members cannot be managed', () => {
|
|
63
|
+
expect(
|
|
64
|
+
hasMemberActions({
|
|
65
|
+
...ownerRights,
|
|
66
|
+
canManageMembers: false,
|
|
67
|
+
status: 'ready'
|
|
68
|
+
})
|
|
69
|
+
).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('gives actions on themselves to a member who cannot manage the sharing', () => {
|
|
73
|
+
expect(
|
|
74
|
+
hasMemberActions({
|
|
75
|
+
...memberRights,
|
|
76
|
+
status: 'ready',
|
|
77
|
+
isCurrentUser: true
|
|
78
|
+
})
|
|
79
|
+
).toBe(true)
|
|
80
|
+
expect(hasMemberActions({ ...memberRights, status: 'ready' })).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useClient } from 'cozy-client'
|
|
4
|
+
import flag from 'cozy-flags'
|
|
5
|
+
|
|
6
|
+
import { useFetchDocumentPath } from './useFetchDocumentPath'
|
|
7
|
+
import { useResolveRecipients } from './useResolveRecipients'
|
|
8
|
+
import { useSharingContext } from './useSharingContext'
|
|
9
|
+
import { getSharingDocIds } from '../state'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Gives what is needed to change the permission type of the members
|
|
13
|
+
* of a document's sharing
|
|
14
|
+
*
|
|
15
|
+
* @param {object} document - Shared document
|
|
16
|
+
* @param {object} [options]
|
|
17
|
+
* @param {boolean} [options.shouldFetchPath] - Whether to fetch the document
|
|
18
|
+
* path, needed to know the shared parent
|
|
19
|
+
* @returns {{ isInsideSharedParent: boolean, sharedParentName: string|null, setMemberType: Function }}
|
|
20
|
+
* `setMemberType({ sharingId, memberIndex, type, recipient }, newType, { withAncestors })`
|
|
21
|
+
* also downgrades the member on the read-write ancestor sharings when `withAncestors` is set
|
|
22
|
+
*/
|
|
23
|
+
export const useMemberPermissionType = (
|
|
24
|
+
document,
|
|
25
|
+
{ shouldFetchPath = true } = {}
|
|
26
|
+
) => {
|
|
27
|
+
const client = useClient()
|
|
28
|
+
const {
|
|
29
|
+
hasSharedParent,
|
|
30
|
+
getSharedParentPath,
|
|
31
|
+
updateSharingMemberType,
|
|
32
|
+
getSharingById,
|
|
33
|
+
share
|
|
34
|
+
} = useSharingContext()
|
|
35
|
+
const resolveRecipients = useResolveRecipients()
|
|
36
|
+
|
|
37
|
+
const documentPath = useFetchDocumentPath(
|
|
38
|
+
client,
|
|
39
|
+
shouldFetchPath ? document : null
|
|
40
|
+
)
|
|
41
|
+
const isInsideSharedParent =
|
|
42
|
+
Boolean(documentPath) && Boolean(hasSharedParent?.(documentPath))
|
|
43
|
+
const sharedParentPath = documentPath
|
|
44
|
+
? getSharedParentPath?.(documentPath)
|
|
45
|
+
: null
|
|
46
|
+
|
|
47
|
+
const setMemberType = useCallback(
|
|
48
|
+
async (
|
|
49
|
+
{ sharingId, memberIndex, type, recipient },
|
|
50
|
+
newType,
|
|
51
|
+
{ withAncestors = false } = {}
|
|
52
|
+
) => {
|
|
53
|
+
if (withAncestors) {
|
|
54
|
+
// Ancestors first: only the target's own update refetches its
|
|
55
|
+
// effective recipients, which must already see them downgraded.
|
|
56
|
+
const ancestors = (recipient?.sources || []).filter(
|
|
57
|
+
s => s.kind === 'ancestor' && !s.read_only
|
|
58
|
+
)
|
|
59
|
+
for (const ancestor of ancestors) {
|
|
60
|
+
await updateSharingMemberType(
|
|
61
|
+
ancestor.sharing_id,
|
|
62
|
+
ancestor.member_index,
|
|
63
|
+
newType
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const documentId = document?._id || document?.id
|
|
69
|
+
if (
|
|
70
|
+
flag('drive.federated-shared-folder.enabled') &&
|
|
71
|
+
type === 'one-way' &&
|
|
72
|
+
newType === 'two-way' &&
|
|
73
|
+
documentId
|
|
74
|
+
) {
|
|
75
|
+
const sourceSharing = getSharingById(sharingId)
|
|
76
|
+
if (
|
|
77
|
+
sourceSharing &&
|
|
78
|
+
!getSharingDocIds(sourceSharing).includes(documentId)
|
|
79
|
+
) {
|
|
80
|
+
const { recipients } = await resolveRecipients([recipient])
|
|
81
|
+
if (!recipients[0]) throw new Error('Recipient contact not found')
|
|
82
|
+
await share({
|
|
83
|
+
document: { ...document, id: documentId },
|
|
84
|
+
description: document.name,
|
|
85
|
+
recipients,
|
|
86
|
+
readOnlyRecipients: [],
|
|
87
|
+
sharedDrive: true,
|
|
88
|
+
openSharing: false
|
|
89
|
+
})
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await updateSharingMemberType(sharingId, memberIndex, newType, document)
|
|
95
|
+
},
|
|
96
|
+
[
|
|
97
|
+
document,
|
|
98
|
+
getSharingById,
|
|
99
|
+
resolveRecipients,
|
|
100
|
+
share,
|
|
101
|
+
updateSharingMemberType
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
isInsideSharedParent,
|
|
107
|
+
sharedParentName: sharedParentPath?.split('/').pop() ?? null,
|
|
108
|
+
setMemberType
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useClient } from 'cozy-client'
|
|
4
|
+
|
|
5
|
+
import { getOrCreateFromArray } from '../helpers/contacts'
|
|
6
|
+
import { DOCTYPE_CONTACTS } from '../models'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Turns the recipients picked in a sharing form into contacts, creating the
|
|
10
|
+
* missing ones, and splits them by the selected sharing right.
|
|
11
|
+
*
|
|
12
|
+
* @returns {Function} `resolveRecipients(pendingRecipients, selectedOption, createContact?)`
|
|
13
|
+
* resolving to `{ recipients, readOnlyRecipients }`, ready to be passed to `share`
|
|
14
|
+
*/
|
|
15
|
+
export const useResolveRecipients = () => {
|
|
16
|
+
const client = useClient()
|
|
17
|
+
|
|
18
|
+
return useCallback(
|
|
19
|
+
async (
|
|
20
|
+
pendingRecipients,
|
|
21
|
+
selectedOption = 'readWrite',
|
|
22
|
+
createContact = contact => client.create(DOCTYPE_CONTACTS, contact)
|
|
23
|
+
) => {
|
|
24
|
+
const contacts = await getOrCreateFromArray(
|
|
25
|
+
client,
|
|
26
|
+
pendingRecipients,
|
|
27
|
+
createContact
|
|
28
|
+
)
|
|
29
|
+
const isReadOnly = selectedOption === 'readOnly'
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
recipients: isReadOnly ? [] : contacts,
|
|
33
|
+
readOnlyRecipients: isReadOnly ? contacts : []
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
[client]
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react-hooks'
|
|
2
|
+
|
|
3
|
+
import { useResolveRecipients } from './useResolveRecipients'
|
|
4
|
+
import { getOrCreateFromArray } from '../helpers/contacts'
|
|
5
|
+
|
|
6
|
+
const mockClient = { create: jest.fn() }
|
|
7
|
+
|
|
8
|
+
jest.mock('cozy-client', () => ({
|
|
9
|
+
useClient: () => mockClient
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
jest.mock('../helpers/contacts', () => ({
|
|
13
|
+
getOrCreateFromArray: jest.fn()
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const contacts = [{ _id: 'alice' }, { _id: 'bob' }]
|
|
17
|
+
|
|
18
|
+
describe('useResolveRecipients', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks()
|
|
21
|
+
getOrCreateFromArray.mockResolvedValue(contacts)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('gives read-write rights by default', async () => {
|
|
25
|
+
const { result } = renderHook(() => useResolveRecipients())
|
|
26
|
+
|
|
27
|
+
const resolved = await result.current([{ email: 'alice@cozy.cc' }])
|
|
28
|
+
|
|
29
|
+
expect(resolved).toEqual({ recipients: contacts, readOnlyRecipients: [] })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('gives read-only rights when the read-only option is selected', async () => {
|
|
33
|
+
const { result } = renderHook(() => useResolveRecipients())
|
|
34
|
+
|
|
35
|
+
const resolved = await result.current([], 'readOnly')
|
|
36
|
+
|
|
37
|
+
expect(resolved).toEqual({ recipients: [], readOnlyRecipients: contacts })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('creates missing contacts with the client by default', async () => {
|
|
41
|
+
const { result } = renderHook(() => useResolveRecipients())
|
|
42
|
+
const pendingRecipients = [{ email: 'alice@cozy.cc' }]
|
|
43
|
+
|
|
44
|
+
await result.current(pendingRecipients)
|
|
45
|
+
const createContact = getOrCreateFromArray.mock.calls[0][2]
|
|
46
|
+
createContact({ email: [{ address: 'alice@cozy.cc' }] })
|
|
47
|
+
|
|
48
|
+
expect(getOrCreateFromArray).toHaveBeenCalledWith(
|
|
49
|
+
mockClient,
|
|
50
|
+
pendingRecipients,
|
|
51
|
+
expect.any(Function)
|
|
52
|
+
)
|
|
53
|
+
expect(mockClient.create).toHaveBeenCalledWith('io.cozy.contacts', {
|
|
54
|
+
email: [{ address: 'alice@cozy.cc' }]
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('creates missing contacts with the given function', async () => {
|
|
59
|
+
const { result } = renderHook(() => useResolveRecipients())
|
|
60
|
+
const createContact = jest.fn()
|
|
61
|
+
|
|
62
|
+
await result.current([], 'readWrite', createContact)
|
|
63
|
+
|
|
64
|
+
expect(getOrCreateFromArray).toHaveBeenCalledWith(
|
|
65
|
+
mockClient,
|
|
66
|
+
[],
|
|
67
|
+
createContact
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
})
|
package/src/index.jsx
CHANGED
|
@@ -36,7 +36,7 @@ export { ShareButtonWithRecipients } from './ShareButtonWithRecipients'
|
|
|
36
36
|
export { DOCUMENT_TYPE } from './helpers/documentType'
|
|
37
37
|
export { getRecipientsFromSharing } from './state'
|
|
38
38
|
export { default as OpenSharingLinkButton } from './OpenSharingLinkButton'
|
|
39
|
-
export { PermissionTypeMenu } from './components/Recipient/
|
|
39
|
+
export { default as PermissionTypeMenu } from './components/Recipient/PermissionTypeMenuWrapper'
|
|
40
40
|
export { default as OpenSharingLinkFabButton } from './OpenSharingLinkFabButton'
|
|
41
41
|
export {
|
|
42
42
|
NativeFileSharingProvider,
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useClient, useQuery, hasQueryBeenLoaded } from 'cozy-client';
|
|
3
|
-
import MemberRecipient from './MemberRecipient';
|
|
4
|
-
import { buildInstanceSettingsQuery } from '../../queries/queries'; // When the owner is not part of a sharing's members (eg. the folder only has
|
|
5
|
-
// a link), there is no per-sharing avatar to reference. The instance's public
|
|
6
|
-
// avatar still holds the owner's picture. fallback=initials makes the stack
|
|
7
|
-
// serve a generated initials avatar (instead of the app icon) when no picture
|
|
8
|
-
// has been uploaded, matching how avatars are rendered everywhere else.
|
|
9
|
-
|
|
10
|
-
var OWNER_PUBLIC_AVATAR_PATH = '/public/avatar?fallback=initials';
|
|
11
|
-
|
|
12
|
-
var OwnerRecipientDefault = function OwnerRecipientDefault() {
|
|
13
|
-
var _instanceSettingsResu;
|
|
14
|
-
|
|
15
|
-
var client = useClient();
|
|
16
|
-
var instanceSettingsQuery = buildInstanceSettingsQuery();
|
|
17
|
-
var instanceSettingsResult = useQuery(instanceSettingsQuery.definition, instanceSettingsQuery.options);
|
|
18
|
-
return hasQueryBeenLoaded(instanceSettingsResult) && /*#__PURE__*/React.createElement(MemberRecipient, {
|
|
19
|
-
isOwner: true,
|
|
20
|
-
status: "owner",
|
|
21
|
-
instance: client.options.uri,
|
|
22
|
-
avatarPath: OWNER_PUBLIC_AVATAR_PATH,
|
|
23
|
-
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
|
|
24
|
-
});
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export default OwnerRecipientDefault;
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
|
|
3
|
-
import { useClient, useQuery, hasQueryBeenLoaded } from 'cozy-client'
|
|
4
|
-
|
|
5
|
-
import MemberRecipient from './MemberRecipient'
|
|
6
|
-
import { buildInstanceSettingsQuery } from '../../queries/queries'
|
|
7
|
-
|
|
8
|
-
// When the owner is not part of a sharing's members (eg. the folder only has
|
|
9
|
-
// a link), there is no per-sharing avatar to reference. The instance's public
|
|
10
|
-
// avatar still holds the owner's picture. fallback=initials makes the stack
|
|
11
|
-
// serve a generated initials avatar (instead of the app icon) when no picture
|
|
12
|
-
// has been uploaded, matching how avatars are rendered everywhere else.
|
|
13
|
-
const OWNER_PUBLIC_AVATAR_PATH = '/public/avatar?fallback=initials'
|
|
14
|
-
|
|
15
|
-
const OwnerRecipientDefault = () => {
|
|
16
|
-
const client = useClient()
|
|
17
|
-
|
|
18
|
-
const instanceSettingsQuery = buildInstanceSettingsQuery()
|
|
19
|
-
const instanceSettingsResult = useQuery(
|
|
20
|
-
instanceSettingsQuery.definition,
|
|
21
|
-
instanceSettingsQuery.options
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
hasQueryBeenLoaded(instanceSettingsResult) && (
|
|
26
|
-
<MemberRecipient
|
|
27
|
-
isOwner={true}
|
|
28
|
-
status="owner"
|
|
29
|
-
instance={client.options.uri}
|
|
30
|
-
avatarPath={OWNER_PUBLIC_AVATAR_PATH}
|
|
31
|
-
public_name={instanceSettingsResult?.data?.attributes?.public_name}
|
|
32
|
-
/>
|
|
33
|
-
)
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export default OwnerRecipientDefault
|