cozy-sharing 40.1.9 → 40.2.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -0
  3. package/dist/SharingOwnerAvatar.js +2 -2
  4. package/dist/SharingProvider.js +3 -1
  5. package/dist/components/Avatar/AvatarList.js +2 -2
  6. package/dist/components/Avatar/MemberAvatar.js +8 -33
  7. package/dist/components/Avatar/MemberAvatar.spec.js +17 -77
  8. package/dist/components/Avatar/MemberAvatarWrapper.js +20 -0
  9. package/dist/components/ConfirmTrustedRecipientsDialog.js +5 -1
  10. package/dist/components/ContactSuggestion.js +5 -2
  11. package/dist/components/Recipient/MemberRecipient.js +6 -3
  12. package/dist/components/Recipient/MemberRecipient.spec.js +1 -9
  13. package/dist/components/Recipient/MemberRecipientLite.js +2 -2
  14. package/dist/components/Recipient/PermissionTypeMenuWrapper.spec.js +3 -1
  15. package/dist/components/ShareAutosuggest.js +93 -16
  16. package/dist/components/ShareAutosuggest.spec.js +126 -3
  17. package/dist/components/ShareAutosuggest.stories.js +3 -0
  18. package/dist/components/ShareAutosuggestWrapper.js +65 -5
  19. package/dist/components/ShareAutosuggestWrapper.spec.js +113 -11
  20. package/dist/components/WhoHasAccessWrapper.js +6 -2
  21. package/dist/helpers/contacts.js +75 -16
  22. package/dist/helpers/contacts.spec.js +150 -1
  23. package/dist/helpers/recipients.js +38 -0
  24. package/dist/helpers/recipients.spec.js +53 -1
  25. package/dist/hooks/useResolveRecipients.js +6 -2
  26. package/dist/hooks/useResolveRecipients.spec.js +11 -2
  27. package/dist/index.js +1 -1
  28. package/dist/queries/queries.js +16 -3
  29. package/package.json +3 -3
  30. package/src/SharingOwnerAvatar.jsx +2 -2
  31. package/src/SharingProvider.jsx +6 -1
  32. package/src/components/Avatar/AvatarList.jsx +2 -2
  33. package/src/components/Avatar/MemberAvatar.jsx +4 -34
  34. package/src/components/Avatar/MemberAvatar.spec.jsx +13 -73
  35. package/src/components/Avatar/MemberAvatarWrapper.jsx +22 -0
  36. package/src/components/ConfirmTrustedRecipientsDialog.jsx +7 -0
  37. package/src/components/ContactSuggestion.jsx +8 -3
  38. package/src/components/Recipient/MemberRecipient.jsx +4 -2
  39. package/src/components/Recipient/MemberRecipient.spec.jsx +1 -8
  40. package/src/components/Recipient/MemberRecipientLite.jsx +2 -2
  41. package/src/components/Recipient/PermissionTypeMenuWrapper.spec.jsx +3 -1
  42. package/src/components/ShareAutosuggest.jsx +56 -9
  43. package/src/components/ShareAutosuggest.spec.jsx +60 -2
  44. package/src/components/ShareAutosuggest.stories.jsx +1 -0
  45. package/src/components/ShareAutosuggestWrapper.jsx +36 -4
  46. package/src/components/ShareAutosuggestWrapper.spec.jsx +78 -5
  47. package/src/components/WhoHasAccessWrapper.jsx +14 -2
  48. package/src/helpers/contacts.js +31 -1
  49. package/src/helpers/contacts.spec.js +102 -1
  50. package/src/helpers/recipients.js +40 -0
  51. package/src/helpers/recipients.spec.js +83 -1
  52. package/src/hooks/useResolveRecipients.js +5 -2
  53. package/src/hooks/useResolveRecipients.spec.js +8 -2
  54. package/src/index.jsx +1 -1
  55. package/src/queries/queries.js +6 -3
@@ -2,13 +2,30 @@ import { models } from 'cozy-client'
2
2
  import flag from 'cozy-flags'
3
3
 
4
4
  import { validateEmail } from '../helpers/email'
5
+ import { DOCTYPE_CONTACTS, DOCTYPE_GROUPS } from '../models'
5
6
 
6
7
  const ContactModel = models.contact
7
8
 
8
9
  // Return the contact object if present
9
10
  // Else check if a contact exist with this email address, if yes return it
10
11
  // if not create it
11
- export const getOrCreateFromArray = async (client, contacts, createContact) => {
12
+ // With contactsSuggestEnabled, people are sent by email and the
13
+ // stack finds or creates the contact: an org member's id is not on this instance
14
+ export const getOrCreateFromArray = async (
15
+ client,
16
+ contacts,
17
+ createContact,
18
+ contactsSuggestEnabled
19
+ ) => {
20
+ if (contactsSuggestEnabled) {
21
+ return contacts.map(contact => {
22
+ if (contact._type === DOCTYPE_GROUPS) return contact
23
+ const email = ContactModel.getPrimaryEmail(contact)
24
+ if (validateEmail(email)) return { _type: DOCTYPE_CONTACTS, email }
25
+ return contact._id ? contact : undefined
26
+ })
27
+ }
28
+
12
29
  return await Promise.all(
13
30
  contacts.map(async contact => {
14
31
  if (contact.id) {
@@ -45,6 +62,19 @@ export const getOrCreateFromArray = async (client, contacts, createContact) => {
45
62
  )
46
63
  }
47
64
 
65
+ // A suggestion without id has no contact document, and only a fullname,
66
+ // which getDisplayName does not read
67
+ export const suggestContacts = async (client, q) => {
68
+ const { data } = await client
69
+ .getStackClient()
70
+ .fetchJSON('GET', `/contacts/suggest?q=${encodeURIComponent(q)}`)
71
+ return data.map(({ id, type, attributes }) => ({
72
+ ...attributes,
73
+ ...(id ? { _id: id, id } : { displayName: attributes.fullname }),
74
+ _type: type
75
+ }))
76
+ }
77
+
48
78
  export const getContactsFromGroupId = (contacts = [], groupId) => {
49
79
  return contacts.filter(contact => {
50
80
  return contact.relationships?.groups?.data?.some(
@@ -1,6 +1,10 @@
1
1
  import flag from 'cozy-flags'
2
2
 
3
- import { computeContactsAndGroups, getOrCreateFromArray } from './contacts'
3
+ import {
4
+ computeContactsAndGroups,
5
+ getOrCreateFromArray,
6
+ suggestContacts
7
+ } from './contacts'
4
8
 
5
9
  jest.mock('cozy-flags', () => ({
6
10
  __esModule: true,
@@ -128,3 +132,100 @@ describe('computeContactsAndGroups', () => {
128
132
  expect(contactsAndGroups.map(c => c._id)).toEqual(['alice', 'bob'])
129
133
  })
130
134
  })
135
+
136
+ describe('getOrCreateFromArray with contactsSuggestEnabled', () => {
137
+ it('should send people by email and keep groups, without creating contacts', async () => {
138
+ const createContact = jest.fn()
139
+ const group = { _id: 'group-1', _type: 'io.cozy.contacts.groups' }
140
+
141
+ const recipients = await getOrCreateFromArray(
142
+ mockClient,
143
+ [
144
+ {
145
+ _id: 'org-member-1',
146
+ _type: 'io.cozy.contacts',
147
+ email: [{ address: 'member@org.example', primary: true }],
148
+ meta: { kind: 'member' }
149
+ },
150
+ { email: 'unknown@friends.example' },
151
+ group
152
+ ],
153
+ createContact,
154
+ true
155
+ )
156
+
157
+ expect(recipients).toEqual([
158
+ { _type: 'io.cozy.contacts', email: 'member@org.example' },
159
+ { _type: 'io.cozy.contacts', email: 'unknown@friends.example' },
160
+ group
161
+ ])
162
+ expect(createContact).not.toHaveBeenCalled()
163
+ })
164
+
165
+ it('should keep a contact without email by id, and drop a recipient with neither', async () => {
166
+ const cozyOnlyContact = {
167
+ _id: 'contact-1',
168
+ _type: 'io.cozy.contacts',
169
+ cozy: [{ url: 'https://bob.example' }]
170
+ }
171
+
172
+ const recipients = await getOrCreateFromArray(
173
+ mockClient,
174
+ [cozyOnlyContact, { instance: 'https://alice.example' }],
175
+ jest.fn(),
176
+ true
177
+ )
178
+
179
+ expect(recipients).toEqual([cozyOnlyContact, undefined])
180
+ })
181
+ })
182
+
183
+ describe('suggestContacts', () => {
184
+ it('should map the suggestions of the stack to contacts', async () => {
185
+ const fetchJSON = jest.fn().mockResolvedValue({
186
+ data: [
187
+ {
188
+ type: 'io.cozy.contacts',
189
+ id: 'org-member-1',
190
+ attributes: {
191
+ fullname: 'Jean Dupont',
192
+ name: { givenName: 'Jean', familyName: 'Dupont' },
193
+ email: [{ address: 'jean@org.example', primary: true }]
194
+ },
195
+ meta: { kind: 'member' }
196
+ },
197
+ {
198
+ type: 'io.cozy.contacts',
199
+ attributes: {
200
+ fullname: 'Paul Dupuis',
201
+ email: [{ address: 'paul@friends.example', primary: true }]
202
+ }
203
+ }
204
+ ]
205
+ })
206
+ const client = { getStackClient: () => ({ fetchJSON }) }
207
+
208
+ const contacts = await suggestContacts(client, 'dup & co')
209
+
210
+ expect(fetchJSON).toHaveBeenCalledWith(
211
+ 'GET',
212
+ '/contacts/suggest?q=dup%20%26%20co'
213
+ )
214
+ expect(contacts).toEqual([
215
+ {
216
+ _id: 'org-member-1',
217
+ id: 'org-member-1',
218
+ _type: 'io.cozy.contacts',
219
+ fullname: 'Jean Dupont',
220
+ name: { givenName: 'Jean', familyName: 'Dupont' },
221
+ email: [{ address: 'jean@org.example', primary: true }]
222
+ },
223
+ {
224
+ _type: 'io.cozy.contacts',
225
+ fullname: 'Paul Dupuis',
226
+ displayName: 'Paul Dupuis',
227
+ email: [{ address: 'paul@friends.example', primary: true }]
228
+ }
229
+ ])
230
+ })
231
+ })
@@ -1,3 +1,4 @@
1
+ import { getPrimaryCozy } from 'cozy-client/dist/models/contact'
1
2
  import flag from 'cozy-flags'
2
3
 
3
4
  import { DOCTYPE_GROUPS } from '../models'
@@ -143,3 +144,42 @@ export const hasMemberActions = ({
143
144
  canManageMembers &&
144
145
  status !== 'owner' &&
145
146
  ((isCurrentUser && !isOwner) || canManageSharing)
147
+
148
+ /**
149
+ * makeAvatarUrl derives the avatar image URL for the recipient.
150
+ * - If the recipient has an avatarPath (sharing context), it is
151
+ * used directly with a base URI prefix (unless it is already
152
+ * a full URL for a remote Cozy).
153
+ * - If the recipient has no avatarPath but has a cozy URL
154
+ * (contact context), it points to that Cozy's public avatar.
155
+ * - The status is used as a cache-buster in the URL so the
156
+ * browser refreshes the image when the sharing status changes.
157
+ * For non-sharing recipients, 'contact' is the stable default.
158
+ */
159
+ export const makeAvatarUrl = (recipient, instanceUri) => {
160
+ const avatarPath = recipient.avatarPath
161
+ const cozyUrl = !avatarPath ? getPrimaryCozy(recipient) : null
162
+ if (!avatarPath && !cozyUrl) return null
163
+
164
+ const finalPath =
165
+ avatarPath ||
166
+ `${cozyUrl.replace(/\/$/, '')}/public/avatar?fallback=initials`
167
+ const status = recipient.status || 'contact'
168
+ const isFullPath = finalPath.startsWith('http')
169
+ const base = isFullPath ? '' : instanceUri
170
+ return `${base}${finalPath}${finalPath.includes('?') ? '&' : '?'}v=${status}`
171
+ }
172
+
173
+ /**
174
+ * Adds to the recipients, except groups, the URL of their avatar
175
+ *
176
+ * @param {object[]} recipients - Recipients of a sharing
177
+ * @param {string} instanceUri - URI of the current user's instance
178
+ * @returns {object[]} The recipients with their `avatarUrl`
179
+ */
180
+ export const addAvatarUrls = (recipients, instanceUri) =>
181
+ recipients.map(recipient =>
182
+ recipient.members
183
+ ? recipient
184
+ : { ...recipient, avatarUrl: makeAvatarUrl(recipient, instanceUri) }
185
+ )
@@ -1,4 +1,9 @@
1
- import { hasMemberActions, markCurrentUser } from './recipients'
1
+ import {
2
+ addAvatarUrls,
3
+ hasMemberActions,
4
+ makeAvatarUrl,
5
+ markCurrentUser
6
+ } from './recipients'
2
7
 
3
8
  const instanceUri = 'https://alice.cozy.cc'
4
9
 
@@ -80,3 +85,80 @@ describe('hasMemberActions', () => {
80
85
  expect(hasMemberActions({ ...memberRights, status: 'ready' })).toBe(false)
81
86
  })
82
87
  })
88
+
89
+ describe('makeAvatarUrl', () => {
90
+ const instanceUri = 'http://cozy.example.com'
91
+
92
+ it('prefixes the avatarPath with the instance URI and the status', () => {
93
+ expect(
94
+ makeAvatarUrl(
95
+ { status: 'owner', avatarPath: '/sharings/123/recipients/0/avatar' },
96
+ instanceUri
97
+ )
98
+ ).toBe('http://cozy.example.com/sharings/123/recipients/0/avatar?v=owner')
99
+ })
100
+
101
+ it('appends the status with & when avatarPath already has a query string', () => {
102
+ expect(
103
+ makeAvatarUrl(
104
+ { status: 'owner', avatarPath: '/public/avatar?fallback=initials' },
105
+ instanceUri
106
+ )
107
+ ).toBe('http://cozy.example.com/public/avatar?fallback=initials&v=owner')
108
+ })
109
+
110
+ it('does not prefix a full URL avatarPath with the instance URI', () => {
111
+ expect(
112
+ makeAvatarUrl(
113
+ {
114
+ status: 'contact',
115
+ avatarPath:
116
+ 'https://jonsnow.mycozy.cloud/public/avatar?fallback=initials'
117
+ },
118
+ instanceUri
119
+ )
120
+ ).toBe(
121
+ 'https://jonsnow.mycozy.cloud/public/avatar?fallback=initials&v=contact'
122
+ )
123
+ })
124
+
125
+ it('points to the public avatar of the contact cozy when there is no avatarPath', () => {
126
+ expect(
127
+ makeAvatarUrl(
128
+ {
129
+ _type: 'io.cozy.contacts',
130
+ cozy: [{ url: 'https://jonsnow.mycozy.cloud' }]
131
+ },
132
+ instanceUri
133
+ )
134
+ ).toBe(
135
+ 'https://jonsnow.mycozy.cloud/public/avatar?fallback=initials&v=contact'
136
+ )
137
+ })
138
+
139
+ it('returns null when there is no avatarPath and no cozy URL', () => {
140
+ expect(
141
+ makeAvatarUrl(
142
+ { _type: 'io.cozy.contacts', public_name: 'Bob' },
143
+ instanceUri
144
+ )
145
+ ).toBe(null)
146
+ })
147
+ })
148
+
149
+ describe('addAvatarUrls', () => {
150
+ it('adds the avatar URL to the members but not to the groups', () => {
151
+ const [member, group] = addAvatarUrls(
152
+ [
153
+ { status: 'ready', avatarPath: '/sharings/123/recipients/1/avatar' },
154
+ { name: 'Team', members: [] }
155
+ ],
156
+ 'http://cozy.example.com'
157
+ )
158
+
159
+ expect(member.avatarUrl).toBe(
160
+ 'http://cozy.example.com/sharings/123/recipients/1/avatar?v=ready'
161
+ )
162
+ expect(group).not.toHaveProperty('avatarUrl')
163
+ })
164
+ })
@@ -2,6 +2,7 @@ import { useCallback } from 'react'
2
2
 
3
3
  import { useClient } from 'cozy-client'
4
4
 
5
+ import { useSharingContext } from './useSharingContext'
5
6
  import { getOrCreateFromArray } from '../helpers/contacts'
6
7
  import { DOCTYPE_CONTACTS } from '../models'
7
8
 
@@ -14,6 +15,7 @@ import { DOCTYPE_CONTACTS } from '../models'
14
15
  */
15
16
  export const useResolveRecipients = () => {
16
17
  const client = useClient()
18
+ const contactsSuggestEnabled = useSharingContext()?.contactsSuggestEnabled
17
19
 
18
20
  return useCallback(
19
21
  async (
@@ -24,7 +26,8 @@ export const useResolveRecipients = () => {
24
26
  const contacts = await getOrCreateFromArray(
25
27
  client,
26
28
  pendingRecipients,
27
- createContact
29
+ createContact,
30
+ contactsSuggestEnabled
28
31
  )
29
32
  const isReadOnly = selectedOption === 'readOnly'
30
33
 
@@ -33,6 +36,6 @@ export const useResolveRecipients = () => {
33
36
  readOnlyRecipients: isReadOnly ? contacts : []
34
37
  }
35
38
  },
36
- [client]
39
+ [client, contactsSuggestEnabled]
37
40
  )
38
41
  }
@@ -13,6 +13,10 @@ jest.mock('../helpers/contacts', () => ({
13
13
  getOrCreateFromArray: jest.fn()
14
14
  }))
15
15
 
16
+ jest.mock('./useSharingContext', () => ({
17
+ useSharingContext: () => ({ contactsSuggestEnabled: true })
18
+ }))
19
+
16
20
  const contacts = [{ _id: 'alice' }, { _id: 'bob' }]
17
21
 
18
22
  describe('useResolveRecipients', () => {
@@ -48,7 +52,8 @@ describe('useResolveRecipients', () => {
48
52
  expect(getOrCreateFromArray).toHaveBeenCalledWith(
49
53
  mockClient,
50
54
  pendingRecipients,
51
- expect.any(Function)
55
+ expect.any(Function),
56
+ true
52
57
  )
53
58
  expect(mockClient.create).toHaveBeenCalledWith('io.cozy.contacts', {
54
59
  email: [{ address: 'alice@cozy.cc' }]
@@ -64,7 +69,8 @@ describe('useResolveRecipients', () => {
64
69
  expect(getOrCreateFromArray).toHaveBeenCalledWith(
65
70
  mockClient,
66
71
  [],
67
- createContact
72
+ createContact,
73
+ true
68
74
  )
69
75
  })
70
76
  })
package/src/index.jsx CHANGED
@@ -10,7 +10,7 @@ export { useSharingContext } from './hooks/useSharingContext'
10
10
  export { useFetchDocumentPath } from './hooks/useFetchDocumentPath'
11
11
 
12
12
  export { GroupAvatar } from './components/Avatar/GroupAvatar'
13
- export { MemberAvatar } from './components/Avatar/MemberAvatar'
13
+ export { MemberAvatarWrapper as MemberAvatar } from './components/Avatar/MemberAvatarWrapper'
14
14
  export { SharedDocument } from './SharedDocument'
15
15
  export { SharedStatus } from './SharedStatus'
16
16
  export { SharedBadge } from './SharedBadge'
@@ -16,7 +16,7 @@ export const buildSharingsByIdQuery = sharingId => ({
16
16
  }
17
17
  })
18
18
 
19
- export const buildReachableContactsQuery = () => ({
19
+ export const buildReachableContactsQuery = ({ inGroups } = {}) => ({
20
20
  definition: Q(DOCTYPE_CONTACTS)
21
21
  .where({
22
22
  _id: {
@@ -42,12 +42,15 @@ export const buildReachableContactsQuery = () => ({
42
42
  }
43
43
  }
44
44
  }
45
- ]
45
+ ],
46
+ ...(inGroups && { 'relationships.groups.data': { $exists: true } })
46
47
  })
47
48
  .indexFields(['_id'])
48
49
  .limitBy(1000),
49
50
  options: {
50
- as: 'io.cozy.contacts/reachable'
51
+ as: inGroups
52
+ ? 'io.cozy.contacts/reachable-with-groups'
53
+ : 'io.cozy.contacts/reachable'
51
54
  }
52
55
  })
53
56