cozy-sharing 40.1.8 → 40.2.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -0
  3. package/dist/SharingProvider.js +3 -1
  4. package/dist/components/Recipient/PermissionTypeMenuWrapper.spec.js +3 -1
  5. package/dist/components/ShareAutosuggest.js +87 -14
  6. package/dist/components/ShareAutosuggest.spec.js +123 -3
  7. package/dist/components/ShareAutosuggestWrapper.js +61 -5
  8. package/dist/components/ShareAutosuggestWrapper.spec.js +113 -11
  9. package/dist/components/ShareRestrictionModal/helpers.js +2 -1
  10. package/dist/components/ShareRestrictionModal/helpers.spec.js +2 -2
  11. package/dist/helpers/contacts.js +75 -16
  12. package/dist/helpers/contacts.spec.js +150 -1
  13. package/dist/hooks/useResolveRecipients.js +6 -2
  14. package/dist/hooks/useResolveRecipients.spec.js +11 -2
  15. package/dist/queries/queries.js +16 -3
  16. package/package.json +3 -3
  17. package/src/SharingProvider.jsx +6 -1
  18. package/src/components/Recipient/PermissionTypeMenuWrapper.spec.jsx +3 -1
  19. package/src/components/ShareAutosuggest.jsx +45 -7
  20. package/src/components/ShareAutosuggest.spec.jsx +59 -2
  21. package/src/components/ShareAutosuggestWrapper.jsx +34 -4
  22. package/src/components/ShareAutosuggestWrapper.spec.jsx +78 -5
  23. package/src/components/ShareRestrictionModal/helpers.js +2 -1
  24. package/src/components/ShareRestrictionModal/helpers.spec.js +2 -2
  25. package/src/helpers/contacts.js +31 -1
  26. package/src/helpers/contacts.spec.js +102 -1
  27. package/src/hooks/useResolveRecipients.js +5 -2
  28. package/src/hooks/useResolveRecipients.spec.js +8 -2
  29. package/src/queries/queries.js +6 -3
@@ -1,21 +1,27 @@
1
1
  import PropTypes from 'prop-types'
2
2
  import React from 'react'
3
3
 
4
- import { useQueryAll, isQueryLoading } from 'cozy-client'
4
+ import { useClient, useQueryAll, isQueryLoading } from 'cozy-client'
5
+ import { models } from 'cozy-client'
5
6
  import flag from 'cozy-flags'
6
7
 
7
8
  import ShareAutosuggest from './ShareAutosuggest'
8
- import { computeContactsAndGroups } from '../helpers/contacts'
9
+ import { computeContactsAndGroups, suggestContacts } from '../helpers/contacts'
10
+ import { useSharingContext } from '../hooks/useSharingContext'
11
+ import { DOCTYPE_GROUPS } from '../models'
9
12
  import {
10
13
  buildReachableContactsQuery,
11
14
  buildContactGroupsQuery,
12
15
  buildUnreachableContactsWithGroupsQuery
13
16
  } from '../queries/queries'
14
17
 
18
+ const ContactModel = models.contact
19
+
15
20
  /**
16
21
  * ShareAutosuggestWrapper is responsible for fetching contacts and groups.
17
22
  * We retrieve all the contacts that are reachable and the groups they belong to.
18
23
  * In order to display the total number of members in each group, we also retrieve the contacts that are not reachable.
24
+ * With contactsSuggestEnabled, the stack suggests the contacts and we only retrieve the group members.
19
25
  */
20
26
  const ShareAutosuggestWrapper = ({
21
27
  currentRecipients,
@@ -28,7 +34,12 @@ const ShareAutosuggestWrapper = ({
28
34
  autoFocus,
29
35
  endAdornment
30
36
  }) => {
31
- const reachableContactsQuery = buildReachableContactsQuery()
37
+ const client = useClient()
38
+ const contactsSuggestEnabled = useSharingContext()?.contactsSuggestEnabled
39
+
40
+ const reachableContactsQuery = buildReachableContactsQuery({
41
+ inGroups: contactsSuggestEnabled
42
+ })
32
43
  const reachableContactsResult = useQueryAll(
33
44
  reachableContactsQuery.definition,
34
45
  reachableContactsQuery.options
@@ -59,7 +70,7 @@ const ShareAutosuggestWrapper = ({
59
70
  (isQueryLoading(unreachableContactsWithGroupsResult) ||
60
71
  unreachableContactsWithGroupsResult.hasMore))
61
72
 
62
- const contactsAndGroups = computeContactsAndGroups({
73
+ const suggestedContactsAndGroups = computeContactsAndGroups({
63
74
  reachableContacts: reachableContactsResult.data || [],
64
75
  unreachableContactsWithGroups:
65
76
  unreachableContactsWithGroupsResult.data || [],
@@ -68,6 +79,24 @@ const ShareAutosuggestWrapper = ({
68
79
  pendingRecipients: recipients
69
80
  })
70
81
 
82
+ const contactsAndGroups = contactsSuggestEnabled
83
+ ? suggestedContactsAndGroups.filter(({ _type }) => _type === DOCTYPE_GROUPS)
84
+ : suggestedContactsAndGroups
85
+
86
+ // The stack matches the emails whatever their case
87
+ const normalizeEmail = email => email?.trim().toLowerCase()
88
+ const isRecipient = email =>
89
+ currentRecipients.some(r => normalizeEmail(r.email) === email) ||
90
+ recipients.some(
91
+ r => normalizeEmail(ContactModel.getPrimaryEmail(r)) === email
92
+ )
93
+
94
+ const fetchContacts = async q =>
95
+ (await suggestContacts(client, q)).filter(
96
+ contact =>
97
+ !isRecipient(normalizeEmail(ContactModel.getPrimaryEmail(contact)))
98
+ )
99
+
71
100
  return (
72
101
  <ShareAutosuggest
73
102
  loading={isLoading}
@@ -80,6 +109,7 @@ const ShareAutosuggestWrapper = ({
80
109
  placeholder={placeholder}
81
110
  autoFocus={autoFocus}
82
111
  endAdornment={endAdornment}
112
+ fetchContacts={contactsSuggestEnabled ? fetchContacts : undefined}
83
113
  />
84
114
  )
85
115
  }
@@ -12,9 +12,12 @@ jest.mock('cozy-flags', () => ({
12
12
  default: jest.fn(() => null)
13
13
  }))
14
14
 
15
+ let mockAutosuggestProps
15
16
  jest.mock('./ShareAutosuggest', () => ({
16
17
  __esModule: true,
17
- default: ({ contactsAndGroups }) => {
18
+ default: props => {
19
+ mockAutosuggestProps = props
20
+ const { contactsAndGroups } = props
18
21
  return (
19
22
  <ul>
20
23
  {contactsAndGroups.map(contactOrGroup => {
@@ -39,8 +42,8 @@ jest.mock('./ShareAutosuggest', () => ({
39
42
 
40
43
  describe('ShareAutosuggestWrapper component', () => {
41
44
  const setup = (
42
- { contacts, contactGroups, unreachableContact },
43
- { currentRecipients, recipients } = {}
45
+ { contacts, contactGroups, unreachableContact, groupMembers },
46
+ { currentRecipients, recipients, contactsSuggestEnabled } = {}
44
47
  ) => {
45
48
  const mockClient = createMockClient({
46
49
  queries: {
@@ -55,11 +58,18 @@ describe('ShareAutosuggestWrapper component', () => {
55
58
  'io.cozy.contacts/unreachable-with-groups': {
56
59
  doctype: 'io.cozy.contacts',
57
60
  data: unreachableContact
61
+ },
62
+ 'io.cozy.contacts/reachable-with-groups': {
63
+ doctype: 'io.cozy.contacts',
64
+ data: groupMembers
58
65
  }
59
66
  }
60
67
  })
61
68
  render(
62
- <AppLike client={mockClient}>
69
+ <AppLike
70
+ client={mockClient}
71
+ sharingContextValue={{ contactsSuggestEnabled }}
72
+ >
63
73
  <ShareAutosuggestWrapper
64
74
  onPick={() => {}}
65
75
  onRemove={() => {}}
@@ -69,6 +79,7 @@ describe('ShareAutosuggestWrapper component', () => {
69
79
  />
70
80
  </AppLike>
71
81
  )
82
+ return mockClient
72
83
  }
73
84
 
74
85
  it('should include groups and contacts when are loaded', async () => {
@@ -219,7 +230,7 @@ describe('ShareAutosuggestWrapper component', () => {
219
230
  })
220
231
 
221
232
  it('should include unreachable members when flag sharing.show-recipient-groups is activated', async () => {
222
- flag.mockReturnValue(true)
233
+ flag.mockImplementation(name => name === 'sharing.show-recipient-groups')
223
234
 
224
235
  const contacts = [
225
236
  {
@@ -294,4 +305,66 @@ describe('ShareAutosuggestWrapper component', () => {
294
305
  screen.getByText("The Night's Watch - 3 members")
295
306
  ).toBeInTheDocument()
296
307
  })
308
+
309
+ describe('with contactsSuggestEnabled', () => {
310
+ beforeEach(() => {
311
+ flag.mockReset()
312
+ })
313
+
314
+ it('should suggest only groups, with the count of their members', () => {
315
+ const groupMembers = [
316
+ {
317
+ id: '5a3b4ccf-c257',
318
+ _id: '5a3b4ccf-c257',
319
+ _type: 'io.cozy.contacts',
320
+ name: { givenName: 'Teagan', familyName: 'Wolf' },
321
+ relationships: {
322
+ groups: {
323
+ data: [{ _id: 'fe86af20-c6c5', _type: 'io.cozy.contacts.groups' }]
324
+ }
325
+ }
326
+ }
327
+ ]
328
+ const contactGroups = [
329
+ {
330
+ id: 'fe86af20-c6c5',
331
+ _id: 'fe86af20-c6c5',
332
+ _type: 'io.cozy.contacts.groups',
333
+ name: 'Friends'
334
+ }
335
+ ]
336
+
337
+ setup({ contactGroups, groupMembers }, { contactsSuggestEnabled: true })
338
+
339
+ expect(screen.getByText('Friends - 1 members')).toBeInTheDocument()
340
+ expect(screen.queryByText('Teagan Wolf')).not.toBeInTheDocument()
341
+ })
342
+
343
+ it('should fetch suggestions without the current and pending recipients, whatever the case of their email', async () => {
344
+ const suggestion = email => ({
345
+ type: 'io.cozy.contacts',
346
+ attributes: { fullname: email, email: [{ address: email }] }
347
+ })
348
+ const mockClient = setup(
349
+ { contactGroups: [], groupMembers: [] },
350
+ {
351
+ currentRecipients: [{ email: 'Current@Cozy.cc' }],
352
+ recipients: [{ email: 'pending@cozy.cc' }],
353
+ contactsSuggestEnabled: true
354
+ }
355
+ )
356
+ const fetchJSON = jest.fn().mockResolvedValue({
357
+ data: [
358
+ suggestion('current@cozy.cc'),
359
+ suggestion('PENDING@cozy.cc'),
360
+ suggestion('new@cozy.cc')
361
+ ]
362
+ })
363
+ jest.spyOn(mockClient, 'getStackClient').mockReturnValue({ fetchJSON })
364
+
365
+ const contacts = await mockAutosuggestProps.fetchContacts('coz')
366
+
367
+ expect(contacts.map(c => c.fullname)).toEqual(['new@cozy.cc'])
368
+ })
369
+ })
297
370
  })
@@ -206,7 +206,8 @@ export const getLinkAccessOptions = ({
206
206
  verbs: editingRights === 'write' ? WRITE_PERMS : READ_ONLY_PERMS,
207
207
  expiresAt: expirationDate?.toISOString() || '',
208
208
  ttl: expirationDate ? makeTTL(expirationDate) : undefined,
209
- password: passwordEnabled ? password : ''
209
+ // An empty field means "unchanged": undefined is left out of the request
210
+ password: passwordEnabled ? password || undefined : ''
210
211
  }
211
212
  }
212
213
 
@@ -124,7 +124,7 @@ describe('ShareRestrictionModal/helpers', () => {
124
124
  })
125
125
  })
126
126
 
127
- it('should call updateDocumentPermissions with empty date & password if enabled without values', async () => {
127
+ it('should call updateDocumentPermissions with an empty date and keep the password if enabled without values', async () => {
128
128
  const updateDocumentPermissions = jest.fn()
129
129
  const file = { _id: '123' }
130
130
 
@@ -143,7 +143,7 @@ describe('ShareRestrictionModal/helpers', () => {
143
143
 
144
144
  expect(updateDocumentPermissions).toHaveBeenCalledWith(file, {
145
145
  expiresAt: '',
146
- password: '',
146
+ password: undefined,
147
147
  verbs: ['GET']
148
148
  })
149
149
  })
@@ -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
+ })
@@ -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
  })
@@ -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