cozy-sharing 33.0.0 → 33.0.2

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 CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [33.0.2](https://github.com/cozy/cozy-libs/compare/cozy-sharing@33.0.1...cozy-sharing@33.0.2) (2026-06-02)
7
+
8
+ ### Bug Fixes
9
+
10
+ - Avoid suggesting an already added or a pending recipient ([a9f7b19](https://github.com/cozy/cozy-libs/commit/a9f7b1934ef9d666b9515fe1cf28538a466bf1ff))
11
+
12
+ ## [33.0.1](https://github.com/cozy/cozy-libs/compare/cozy-sharing@33.0.0...cozy-sharing@33.0.1) (2026-06-02)
13
+
14
+ ### Bug Fixes
15
+
16
+ - Disable new contact creation for shared drives forms ([08abf8a](https://github.com/cozy/cozy-libs/commit/08abf8a21a8d37db190866d68a73744b8bcbf0aa))
17
+
6
18
  # [33.0.0](https://github.com/cozy/cozy-libs/compare/cozy-sharing@32.3.2...cozy-sharing@33.0.0) (2026-06-02)
7
19
 
8
20
  ### Bug Fixes
@@ -258,7 +258,8 @@ var FederatedFolderModalContent = function FederatedFolderModalContent(_ref) {
258
258
  pendingRecipients: pendingRecipients,
259
259
  onPendingRecipientsChange: setPendingRecipients,
260
260
  selectedOption: selectedOption,
261
- onSelectedOptionChange: setSelectedOption
261
+ onSelectedOptionChange: setSelectedOption,
262
+ enableCreateContact: true
262
263
  })), /*#__PURE__*/React.createElement(WhoHasAccess, {
263
264
  isOwner: true,
264
265
  isSharedDrive: true,
@@ -8,6 +8,7 @@ import Chip from 'cozy-ui/transpiled/react/Chips';
8
8
  import { Spinner } from 'cozy-ui/transpiled/react/Spinner';
9
9
  import { GroupAvatar } from './Avatar/GroupAvatar';
10
10
  import ContactSuggestion from './ContactSuggestion';
11
+ import { isContactToBeCreated } from '../helpers/contacts';
11
12
  import { extractEmails, validateEmail } from '../helpers/email';
12
13
  import { getDisplayName, getInitials, Contact, Group } from '../models';
13
14
  var styles = {
@@ -22,15 +23,16 @@ var styles = {
22
23
  "recipientChip": "autosuggest__recipientChip___d5I3R",
23
24
  "endAdornment": "autosuggest__endAdornment___2EwTN"
24
25
  };
25
- import { cozyUrlMatch, emailMatch, groupNameMatch, fullnameMatch } from '../suggestionMatchers';
26
+ import { contactOrGroupMatch } from '../suggestionMatchers';
26
27
 
27
- var ShareAutocomplete = function ShareAutocomplete(_ref) {
28
+ var ShareAutosuggest = function ShareAutosuggest(_ref) {
28
29
  var loading = _ref.loading,
29
30
  disabled = _ref.disabled,
30
31
  contactsAndGroups = _ref.contactsAndGroups,
31
32
  recipients = _ref.recipients,
32
33
  onPick = _ref.onPick,
33
34
  onRemove = _ref.onRemove,
35
+ enableCreateContact = _ref.enableCreateContact,
34
36
  placeholder = _ref.placeholder,
35
37
  endAdornment = _ref.endAdornment;
36
38
 
@@ -60,18 +62,24 @@ var ShareAutocomplete = function ShareAutocomplete(_ref) {
60
62
  var computeSuggestions = function computeSuggestions(value) {
61
63
  var inputValue = value.trim().toLowerCase();
62
64
  return inputValue.length === 0 ? [] : contactsAndGroups.filter(function (contactOrGroup) {
63
- return groupNameMatch(inputValue, contactOrGroup) || fullnameMatch(inputValue, contactOrGroup) || emailMatch(inputValue, contactOrGroup) || cozyUrlMatch(inputValue, contactOrGroup);
65
+ return contactOrGroupMatch(inputValue, contactOrGroup);
64
66
  });
65
67
  };
66
68
 
67
69
  var onSuggestionsFetchRequested = function onSuggestionsFetchRequested(_ref2) {
68
70
  var value = _ref2.value;
69
71
  var computedSuggestions = computeSuggestions(value);
70
- var newSuggestions = computedSuggestions.length === 0 && validateEmail(value) ? [{
71
- email: value,
72
- _type: Contact.doctype
73
- }] : computedSuggestions;
74
- setSuggestions(newSuggestions);
72
+
73
+ if (computedSuggestions.length > 0) {
74
+ setSuggestions(computedSuggestions);
75
+ } else if (enableCreateContact && validateEmail(value)) {
76
+ setSuggestions([{
77
+ email: value,
78
+ _type: Contact.doctype
79
+ }]);
80
+ } else {
81
+ setSuggestions([]);
82
+ }
75
83
  };
76
84
 
77
85
  var onSuggestionsClearRequested = function onSuggestionsClearRequested() {
@@ -142,6 +150,7 @@ var ShareAutocomplete = function ShareAutocomplete(_ref) {
142
150
  var onAutosuggestPick = function onAutosuggestPick(value) {
143
151
  var _autosuggestRef$curre;
144
152
 
153
+ if (isContactToBeCreated(value) && !enableCreateContact) return;
145
154
  onPick(value);
146
155
  setInputValue('');
147
156
  autosuggestRef === null || autosuggestRef === void 0 || (_autosuggestRef$curre = autosuggestRef.current) === null || _autosuggestRef$curre === void 0 || (_autosuggestRef$curre = _autosuggestRef$curre.input) === null || _autosuggestRef$curre === void 0 || _autosuggestRef$curre.focus();
@@ -234,14 +243,15 @@ var ShareAutocomplete = function ShareAutocomplete(_ref) {
234
243
  }));
235
244
  };
236
245
 
237
- ShareAutocomplete.propTypes = {
246
+ ShareAutosuggest.propTypes = {
238
247
  loading: PropTypes.bool,
239
248
  disabled: PropTypes.bool,
240
249
  contactsAndGroups: PropTypes.array,
241
250
  recipients: PropTypes.array.isRequired,
242
251
  onPick: PropTypes.func.isRequired,
243
252
  onRemove: PropTypes.func.isRequired,
253
+ enableCreateContact: PropTypes.bool,
244
254
  placeholder: PropTypes.string,
245
255
  endAdornment: PropTypes.node
246
256
  };
247
- export default ShareAutocomplete;
257
+ export default ShareAutosuggest;
@@ -14,14 +14,17 @@ describe('ShareAutosuggest', function () {
14
14
  var onPick = _ref.onPick,
15
15
  onRemove = _ref.onRemove,
16
16
  _ref$loading = _ref.loading,
17
- loading = _ref$loading === void 0 ? false : _ref$loading;
17
+ loading = _ref$loading === void 0 ? false : _ref$loading,
18
+ _ref$enableCreateCont = _ref.enableCreateContact,
19
+ enableCreateContact = _ref$enableCreateCont === void 0 ? true : _ref$enableCreateCont;
18
20
  render( /*#__PURE__*/React.createElement(AppLike, null, /*#__PURE__*/React.createElement(ShareAutosuggest, {
19
21
  loading: loading,
20
22
  contactsAndGroups: [],
21
23
  placeholder: "myPlaceHolder",
22
24
  onPick: onPick,
23
25
  recipients: [],
24
- onRemove: onRemove
26
+ onRemove: onRemove,
27
+ enableCreateContact: enableCreateContact
25
28
  })));
26
29
  };
27
30
 
@@ -78,6 +81,27 @@ describe('ShareAutosuggest', function () {
78
81
  email: 'quentin.valmori@cozycloud.cc'
79
82
  });
80
83
  });
84
+ it('should not call onPick for new contacts when enableCreateContact is false', function () {
85
+ var onPick = jest.fn();
86
+ var onRemove = jest.fn();
87
+ setup({
88
+ onPick: onPick,
89
+ onRemove: onRemove,
90
+ enableCreateContact: false
91
+ });
92
+ var inputNode = screen.getByPlaceholderText('myPlaceHolder');
93
+ fireEvent.change(inputNode, {
94
+ target: {
95
+ value: 'new@cozycloud.cc'
96
+ }
97
+ });
98
+ fireEvent.keyPress(inputNode, {
99
+ key: 'Enter',
100
+ keyCode: 13,
101
+ charCode: 13
102
+ });
103
+ expect(onPick).not.toHaveBeenCalled();
104
+ });
81
105
  it('should show loading indicator when loading prop is true (autoFocus triggers onFocus on mount)', function () {
82
106
  var onPick = jest.fn();
83
107
  var onRemove = jest.fn();
@@ -33,6 +33,7 @@ export var ShareByEmail = function ShareByEmail(_ref) {
33
33
  onPendingRecipientsChange = _ref.onPendingRecipientsChange,
34
34
  selectedOption = _ref.selectedOption,
35
35
  onSelectedOptionChange = _ref.onSelectedOptionChange,
36
+ enableCreateContact = _ref.enableCreateContact,
36
37
  sharing = _ref.sharing;
37
38
 
38
39
  var _useI18n = useI18n(),
@@ -71,6 +72,7 @@ export var ShareByEmail = function ShareByEmail(_ref) {
71
72
  placeholder: pendingRecipients.length === 0 ? t("".concat(documentType, ".share.shareByEmail.emailPlaceholder")) : '',
72
73
  onPick: onRecipientPick,
73
74
  onRemove: onRecipientRemove,
75
+ enableCreateContact: enableCreateContact,
74
76
  currentRecipients: currentRecipients,
75
77
  recipients: pendingRecipients,
76
78
  endAdornment: /*#__PURE__*/React.createElement(ShareTypeSelect, {
@@ -88,6 +90,7 @@ ShareByEmail.propTypes = {
88
90
  onPendingRecipientsChange: PropTypes.func.isRequired,
89
91
  selectedOption: PropTypes.oneOf(['readWrite', 'readOnly']).isRequired,
90
92
  onSelectedOptionChange: PropTypes.func.isRequired,
93
+ enableCreateContact: PropTypes.bool,
91
94
  sharing: PropTypes.object
92
95
  };
93
96
  export default ShareByEmail;
@@ -25,7 +25,8 @@ describe('ShareByEmail (controlled)', function () {
25
25
  pendingRecipients: [],
26
26
  onPendingRecipientsChange: onPendingRecipientsChange,
27
27
  selectedOption: 'readWrite',
28
- onSelectedOptionChange: onSelectedOptionChange
28
+ onSelectedOptionChange: onSelectedOptionChange,
29
+ enableCreateContact: true
29
30
  }, overrides);
30
31
 
31
32
  var utils = render( /*#__PURE__*/React.createElement(AppLike, null, /*#__PURE__*/React.createElement(ShareByEmail, props)));
@@ -91,7 +91,8 @@ var SharingContent = function SharingContent(_ref) {
91
91
  pendingRecipients: pendingRecipients,
92
92
  onPendingRecipientsChange: onPendingRecipientsChange,
93
93
  selectedOption: selectedOption,
94
- onSelectedOptionChange: onSelectedOptionChange
94
+ onSelectedOptionChange: onSelectedOptionChange,
95
+ enableCreateContact: true
95
96
  })), showWhoHasAccess && /*#__PURE__*/React.createElement(WhoHasAccess, {
96
97
  document: document,
97
98
  documentType: documentType,
@@ -39,11 +39,11 @@ jest.mock('../hooks/useSharingContext', function () {
39
39
  };
40
40
  });
41
41
  jest.mock('../helpers/contacts', function () {
42
- return {
42
+ return _objectSpread(_objectSpread({}, jest.requireActual('../helpers/contacts')), {}, {
43
43
  getOrCreateFromArray: jest.fn(function (client, recipients) {
44
44
  return Promise.resolve(recipients);
45
45
  })
46
- };
46
+ });
47
47
  });
48
48
  describe('ShareDialogCozyToCozy', function () {
49
49
  var client = createMockClient({});
@@ -8,10 +8,12 @@ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { va
8
8
  import PropTypes from 'prop-types';
9
9
  import React from 'react';
10
10
  import { useQueryAll, isQueryLoading } from 'cozy-client';
11
+ import { models } from 'cozy-client';
11
12
  import flag from 'cozy-flags';
12
13
  import ShareAutosuggest from './ShareAutosuggest';
13
14
  import { getContactsFromGroupId } from '../helpers/contacts';
14
15
  import { buildReachableContactsQuery, buildContactGroupsQuery, buildUnreachableContactsWithGroupsQuery } from '../queries/queries';
16
+ var ContactModel = models.contact;
15
17
  /**
16
18
  * ShareRecipientsInput is responsible for fetching contacts and groups.
17
19
  * We retrieve all the contacts that are reachable and the groups they belong to.
@@ -28,6 +30,7 @@ var ShareRecipientsInput = function ShareRecipientsInput(_ref) {
28
30
  placeholder = _ref.placeholder,
29
31
  onPick = _ref.onPick,
30
32
  onRemove = _ref.onRemove,
33
+ enableCreateContact = _ref.enableCreateContact,
31
34
  disabled = _ref.disabled,
32
35
  endAdornment = _ref.endAdornment;
33
36
  var reachableContactsQuery = buildReachableContactsQuery();
@@ -40,7 +43,13 @@ var ShareRecipientsInput = function ShareRecipientsInput(_ref) {
40
43
  var contactGroupsResult = useQueryAll(contactGroupsQuery.definition, contactGroupsQuery.options);
41
44
  var isLoading = isQueryLoading(reachableContactsResult) || reachableContactsResult.hasMore || isQueryLoading(contactGroupsResult) || contactGroupsResult.hasMore || flag('sharing.show-recipient-groups') && (isQueryLoading(unreachableContactsWithGroupsResult) || unreachableContactsWithGroupsResult.hasMore);
42
45
  var reachableContacts = (reachableContactsResult.data || []).filter(function (contact) {
43
- return !isCurrentUser(contact);
46
+ return !isCurrentUser(contact) && // We do not want contact already in the sharing
47
+ !currentRecipients.find(function (r) {
48
+ return r.email === ContactModel.getPrimaryEmail(contact);
49
+ }) && // We do not want contact currently in pending contacts (in chips)
50
+ !recipients.find(function (r) {
51
+ return r._id === contact._id;
52
+ });
44
53
  });
45
54
  var unreachableContactsWithGroups = (unreachableContactsWithGroupsResult.data || []).filter(function (contact) {
46
55
  return !isCurrentUser(contact);
@@ -82,6 +91,7 @@ var ShareRecipientsInput = function ShareRecipientsInput(_ref) {
82
91
  recipients: recipients,
83
92
  onPick: onPick,
84
93
  onRemove: onRemove,
94
+ enableCreateContact: enableCreateContact,
85
95
  placeholder: placeholder,
86
96
  endAdornment: endAdornment
87
97
  });
@@ -93,6 +103,7 @@ ShareRecipientsInput.propTypes = {
93
103
  placeholder: PropTypes.string,
94
104
  onPick: PropTypes.func.isRequired,
95
105
  onRemove: PropTypes.func.isRequired,
106
+ enableCreateContact: PropTypes.bool,
96
107
  disabled: PropTypes.bool,
97
108
  endAdornment: PropTypes.node
98
109
  };
@@ -38,6 +38,11 @@ describe('ShareRecipientsInput component', function () {
38
38
  var contacts = _ref2.contacts,
39
39
  contactGroups = _ref2.contactGroups,
40
40
  unreachableContact = _ref2.unreachableContact;
41
+
42
+ var _ref3 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
43
+ currentRecipients = _ref3.currentRecipients,
44
+ recipients = _ref3.recipients;
45
+
41
46
  var mockClient = createMockClient({
42
47
  queries: {
43
48
  'io.cozy.contacts/reachable': {
@@ -59,7 +64,9 @@ describe('ShareRecipientsInput component', function () {
59
64
  }, /*#__PURE__*/React.createElement(ShareRecipientsInput, {
60
65
  onPick: function onPick() {},
61
66
  onRemove: function onRemove() {},
62
- placeholder: "Enter recipients here"
67
+ placeholder: "Enter recipients here",
68
+ currentRecipients: currentRecipients,
69
+ recipients: recipients
63
70
  })));
64
71
  };
65
72
 
@@ -172,10 +179,66 @@ describe('ShareRecipientsInput component', function () {
172
179
  }
173
180
  }, _callee2);
174
181
  })));
175
- it('should include unreachable members when flag sharing.show-recipient-groups is activated', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3() {
176
- var contacts, contactGroups, unreachableContact;
182
+ it('should exclude already added recipients from suggestions', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3() {
183
+ var contacts, contactGroups, currentRecipients, recipients;
177
184
  return _regeneratorRuntime.wrap(function _callee3$(_context3) {
178
185
  while (1) switch (_context3.prev = _context3.next) {
186
+ case 0:
187
+ contacts = [{
188
+ id: 'df563cc4-6440',
189
+ _id: 'df563cc4-6440',
190
+ _type: 'io.cozy.contacts',
191
+ name: {
192
+ givenName: 'Michale',
193
+ familyName: 'Russel'
194
+ },
195
+ email: [{
196
+ address: 'michale@example.com',
197
+ primary: true
198
+ }]
199
+ }, {
200
+ id: '5a3b4ccf-c257',
201
+ _id: '5a3b4ccf-c257',
202
+ _type: 'io.cozy.contacts',
203
+ name: {
204
+ givenName: 'Teagan',
205
+ familyName: 'Wolf'
206
+ },
207
+ email: [{
208
+ address: 'teagan@example.com',
209
+ primary: true
210
+ }]
211
+ }];
212
+ contactGroups = [];
213
+ currentRecipients = [{
214
+ id: 'recipient-1',
215
+ email: 'michale@example.com'
216
+ }];
217
+ recipients = [{
218
+ _id: '5a3b4ccf-c257'
219
+ }];
220
+ setup({
221
+ contacts: contacts,
222
+ contactGroups: contactGroups
223
+ }, {
224
+ currentRecipients: currentRecipients,
225
+ recipients: recipients
226
+ }); // Michale is in currentRecipients by email, should be filtered out
227
+
228
+ expect(screen.queryByText('Michale Russel')).not.toBeInTheDocument(); // Teagan is in recipients by _id, should be filtered out
229
+
230
+ expect(screen.queryByText('Teagan Wolf')).not.toBeInTheDocument();
231
+
232
+ case 7:
233
+ case "end":
234
+ return _context3.stop();
235
+ }
236
+ }, _callee3);
237
+ })));
238
+ it('should include unreachable members when flag sharing.show-recipient-groups is activated', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee4() {
239
+ var contacts, contactGroups, unreachableContact;
240
+ return _regeneratorRuntime.wrap(function _callee4$(_context4) {
241
+ while (1) switch (_context4.prev = _context4.next) {
179
242
  case 0:
180
243
  flag.mockReturnValue(true);
181
244
  contacts = [{
@@ -240,8 +303,8 @@ describe('ShareRecipientsInput component', function () {
240
303
 
241
304
  case 8:
242
305
  case "end":
243
- return _context3.stop();
306
+ return _context4.stop();
244
307
  }
245
- }, _callee3);
308
+ }, _callee4);
246
309
  })));
247
310
  });
@@ -102,4 +102,16 @@ export var getContactsFromGroupId = function getContactsFromGroupId() {
102
102
  return group._id === groupId;
103
103
  });
104
104
  });
105
+ };
106
+ /**
107
+ * Returns true when a contact does not exist. A contact without
108
+ * an `_id` is always a placeholder that must be created before sharing.
109
+ *
110
+ * Used to decide whether the autosuggest input should allow creating
111
+ * a new contact on the fly or should block the pick (depending on the
112
+ * `enableCreateContact` flag).
113
+ */
114
+
115
+ export var isContactToBeCreated = function isContactToBeCreated(contact) {
116
+ return !contact._id;
105
117
  };
@@ -40,4 +40,8 @@ var fullnameMatch = function fullnameMatch(input, contact) {
40
40
  return fullnameInput.test(contact.fullname);
41
41
  };
42
42
 
43
- export { emailMatch, cozyUrlMatch, groupNameMatch, fullnameMatch };
43
+ var contactOrGroupMatch = function contactOrGroupMatch(input, contact) {
44
+ return groupNameMatch(input, contact) || fullnameMatch(input, contact) || emailMatch(input, contact) || cozyUrlMatch(input, contact);
45
+ };
46
+
47
+ export { emailMatch, cozyUrlMatch, groupNameMatch, fullnameMatch, contactOrGroupMatch };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-sharing",
3
- "version": "33.0.0",
3
+ "version": "33.0.2",
4
4
  "description": "Provides sharing login for React applications.",
5
5
  "main": "dist/index.js",
6
6
  "author": "Cozy",
@@ -83,5 +83,5 @@
83
83
  "sideEffects": [
84
84
  "*.css"
85
85
  ],
86
- "gitHead": "6276bf5263e77e71944feb505a0aad085e770166"
86
+ "gitHead": "0a14cfca6a608f4a40d2301c370b176ecbaeee4c"
87
87
  }
@@ -206,6 +206,7 @@ const FederatedFolderModalContent = ({
206
206
  onPendingRecipientsChange={setPendingRecipients}
207
207
  selectedOption={selectedOption}
208
208
  onSelectedOptionChange={setSelectedOption}
209
+ enableCreateContact
209
210
  />
210
211
  </div>
211
212
  <WhoHasAccess
@@ -8,23 +8,20 @@ import { Spinner } from 'cozy-ui/transpiled/react/Spinner'
8
8
 
9
9
  import { GroupAvatar } from './Avatar/GroupAvatar'
10
10
  import ContactSuggestion from './ContactSuggestion'
11
+ import { isContactToBeCreated } from '../helpers/contacts'
11
12
  import { extractEmails, validateEmail } from '../helpers/email'
12
13
  import { getDisplayName, getInitials, Contact, Group } from '../models'
13
14
  import styles from '../styles/autosuggest.styl'
14
- import {
15
- cozyUrlMatch,
16
- emailMatch,
17
- groupNameMatch,
18
- fullnameMatch
19
- } from '../suggestionMatchers'
20
-
21
- const ShareAutocomplete = ({
15
+ import { contactOrGroupMatch } from '../suggestionMatchers'
16
+
17
+ const ShareAutosuggest = ({
22
18
  loading,
23
19
  disabled,
24
20
  contactsAndGroups,
25
21
  recipients,
26
22
  onPick,
27
23
  onRemove,
24
+ enableCreateContact,
28
25
  placeholder,
29
26
  endAdornment
30
27
  }) => {
@@ -41,29 +38,26 @@ const ShareAutocomplete = ({
41
38
 
42
39
  return inputValue.length === 0
43
40
  ? []
44
- : contactsAndGroups.filter(
45
- contactOrGroup =>
46
- groupNameMatch(inputValue, contactOrGroup) ||
47
- fullnameMatch(inputValue, contactOrGroup) ||
48
- emailMatch(inputValue, contactOrGroup) ||
49
- cozyUrlMatch(inputValue, contactOrGroup)
41
+ : contactsAndGroups.filter(contactOrGroup =>
42
+ contactOrGroupMatch(inputValue, contactOrGroup)
50
43
  )
51
44
  }
52
45
 
53
46
  const onSuggestionsFetchRequested = ({ value }) => {
54
47
  const computedSuggestions = computeSuggestions(value)
55
48
 
56
- const newSuggestions =
57
- computedSuggestions.length === 0 && validateEmail(value)
58
- ? [
59
- {
60
- email: value,
61
- _type: Contact.doctype
62
- }
63
- ]
64
- : computedSuggestions
65
-
66
- setSuggestions(newSuggestions)
49
+ if (computedSuggestions.length > 0) {
50
+ setSuggestions(computedSuggestions)
51
+ } else if (enableCreateContact && validateEmail(value)) {
52
+ setSuggestions([
53
+ {
54
+ email: value,
55
+ _type: Contact.doctype
56
+ }
57
+ ])
58
+ } else {
59
+ setSuggestions([])
60
+ }
67
61
  }
68
62
 
69
63
  const onSuggestionsClearRequested = () => {
@@ -130,6 +124,8 @@ const ShareAutocomplete = ({
130
124
  }
131
125
 
132
126
  const onAutosuggestPick = value => {
127
+ if (isContactToBeCreated(value) && !enableCreateContact) return
128
+
133
129
  onPick(value)
134
130
  setInputValue('')
135
131
  autosuggestRef?.current?.input?.focus()
@@ -221,15 +217,16 @@ const ShareAutocomplete = ({
221
217
  )
222
218
  }
223
219
 
224
- ShareAutocomplete.propTypes = {
220
+ ShareAutosuggest.propTypes = {
225
221
  loading: PropTypes.bool,
226
222
  disabled: PropTypes.bool,
227
223
  contactsAndGroups: PropTypes.array,
228
224
  recipients: PropTypes.array.isRequired,
229
225
  onPick: PropTypes.func.isRequired,
230
226
  onRemove: PropTypes.func.isRequired,
227
+ enableCreateContact: PropTypes.bool,
231
228
  placeholder: PropTypes.string,
232
229
  endAdornment: PropTypes.node
233
230
  }
234
231
 
235
- export default ShareAutocomplete
232
+ export default ShareAutosuggest
@@ -9,7 +9,12 @@ jest.mock('cozy-ui/transpiled/react/Spinner', () => ({
9
9
  }))
10
10
 
11
11
  describe('ShareAutosuggest', () => {
12
- const setup = ({ onPick, onRemove, loading = false }) => {
12
+ const setup = ({
13
+ onPick,
14
+ onRemove,
15
+ loading = false,
16
+ enableCreateContact = true
17
+ }) => {
13
18
  render(
14
19
  <AppLike>
15
20
  <ShareAutosuggest
@@ -19,6 +24,7 @@ describe('ShareAutosuggest', () => {
19
24
  onPick={onPick}
20
25
  recipients={[]}
21
26
  onRemove={onRemove}
27
+ enableCreateContact={enableCreateContact}
22
28
  />
23
29
  </AppLike>
24
30
  )
@@ -51,6 +57,19 @@ describe('ShareAutosuggest', () => {
51
57
  })
52
58
  })
53
59
 
60
+ it('should not call onPick for new contacts when enableCreateContact is false', () => {
61
+ const onPick = jest.fn()
62
+ const onRemove = jest.fn()
63
+
64
+ setup({ onPick, onRemove, enableCreateContact: false })
65
+
66
+ const inputNode = screen.getByPlaceholderText('myPlaceHolder')
67
+
68
+ fireEvent.change(inputNode, { target: { value: 'new@cozycloud.cc' } })
69
+ fireEvent.keyPress(inputNode, { key: 'Enter', keyCode: 13, charCode: 13 })
70
+ expect(onPick).not.toHaveBeenCalled()
71
+ })
72
+
54
73
  it('should show loading indicator when loading prop is true (autoFocus triggers onFocus on mount)', () => {
55
74
  const onPick = jest.fn()
56
75
  const onRemove = jest.fn()
@@ -21,6 +21,7 @@ export const ShareByEmail = ({
21
21
  onPendingRecipientsChange,
22
22
  selectedOption,
23
23
  onSelectedOptionChange,
24
+ enableCreateContact,
24
25
  sharing
25
26
  }) => {
26
27
  const { t } = useI18n()
@@ -66,6 +67,7 @@ export const ShareByEmail = ({
66
67
  }
67
68
  onPick={onRecipientPick}
68
69
  onRemove={onRecipientRemove}
70
+ enableCreateContact={enableCreateContact}
69
71
  currentRecipients={currentRecipients}
70
72
  recipients={pendingRecipients}
71
73
  endAdornment={
@@ -89,6 +91,7 @@ ShareByEmail.propTypes = {
89
91
  onPendingRecipientsChange: PropTypes.func.isRequired,
90
92
  selectedOption: PropTypes.oneOf(['readWrite', 'readOnly']).isRequired,
91
93
  onSelectedOptionChange: PropTypes.func.isRequired,
94
+ enableCreateContact: PropTypes.bool,
92
95
  sharing: PropTypes.object
93
96
  }
94
97
 
@@ -17,6 +17,7 @@ describe('ShareByEmail (controlled)', () => {
17
17
  onPendingRecipientsChange,
18
18
  selectedOption: 'readWrite',
19
19
  onSelectedOptionChange,
20
+ enableCreateContact: true,
20
21
  ...overrides
21
22
  }
22
23
  const utils = render(
@@ -79,6 +79,7 @@ const SharingContent = ({
79
79
  onPendingRecipientsChange={onPendingRecipientsChange}
80
80
  selectedOption={selectedOption}
81
81
  onSelectedOptionChange={onSelectedOptionChange}
82
+ enableCreateContact
82
83
  />
83
84
  )}
84
85
  </div>
@@ -21,6 +21,7 @@ jest.mock('../hooks/useSharingContext', () => ({
21
21
  }))
22
22
 
23
23
  jest.mock('../helpers/contacts', () => ({
24
+ ...jest.requireActual('../helpers/contacts'),
24
25
  getOrCreateFromArray: jest.fn((client, recipients) =>
25
26
  Promise.resolve(recipients)
26
27
  )
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
2
2
  import React from 'react'
3
3
 
4
4
  import { 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'
@@ -12,6 +13,8 @@ import {
12
13
  buildUnreachableContactsWithGroupsQuery
13
14
  } from '../queries/queries'
14
15
 
16
+ const ContactModel = models.contact
17
+
15
18
  /**
16
19
  * ShareRecipientsInput is responsible for fetching contacts and groups.
17
20
  * We retrieve all the contacts that are reachable and the groups they belong to.
@@ -25,6 +28,7 @@ const ShareRecipientsInput = ({
25
28
  placeholder,
26
29
  onPick,
27
30
  onRemove,
31
+ enableCreateContact,
28
32
  disabled,
29
33
  endAdornment
30
34
  }) => {
@@ -60,8 +64,16 @@ const ShareRecipientsInput = ({
60
64
  unreachableContactsWithGroupsResult.hasMore))
61
65
 
62
66
  const reachableContacts = (reachableContactsResult.data || []).filter(
63
- contact => !isCurrentUser(contact)
67
+ contact =>
68
+ !isCurrentUser(contact) &&
69
+ // We do not want contact already in the sharing
70
+ !currentRecipients.find(
71
+ r => r.email === ContactModel.getPrimaryEmail(contact)
72
+ ) &&
73
+ // We do not want contact currently in pending contacts (in chips)
74
+ !recipients.find(r => r._id === contact._id)
64
75
  )
76
+
65
77
  const unreachableContactsWithGroups = (
66
78
  unreachableContactsWithGroupsResult.data || []
67
79
  ).filter(contact => !isCurrentUser(contact))
@@ -114,6 +126,7 @@ const ShareRecipientsInput = ({
114
126
  recipients={recipients}
115
127
  onPick={onPick}
116
128
  onRemove={onRemove}
129
+ enableCreateContact={enableCreateContact}
117
130
  placeholder={placeholder}
118
131
  endAdornment={endAdornment}
119
132
  />
@@ -126,6 +139,7 @@ ShareRecipientsInput.propTypes = {
126
139
  placeholder: PropTypes.string,
127
140
  onPick: PropTypes.func.isRequired,
128
141
  onRemove: PropTypes.func.isRequired,
142
+ enableCreateContact: PropTypes.bool,
129
143
  disabled: PropTypes.bool,
130
144
  endAdornment: PropTypes.node
131
145
  }
@@ -38,7 +38,10 @@ jest.mock('./ShareAutosuggest', () => ({
38
38
  }))
39
39
 
40
40
  describe('ShareRecipientsInput component', () => {
41
- const setup = ({ contacts, contactGroups, unreachableContact }) => {
41
+ const setup = (
42
+ { contacts, contactGroups, unreachableContact },
43
+ { currentRecipients, recipients } = {}
44
+ ) => {
42
45
  const mockClient = createMockClient({
43
46
  queries: {
44
47
  'io.cozy.contacts/reachable': {
@@ -61,6 +64,8 @@ describe('ShareRecipientsInput component', () => {
61
64
  onPick={() => {}}
62
65
  onRemove={() => {}}
63
66
  placeholder="Enter recipients here"
67
+ currentRecipients={currentRecipients}
68
+ recipients={recipients}
64
69
  />
65
70
  </AppLike>
66
71
  )
@@ -175,6 +180,44 @@ describe('ShareRecipientsInput component', () => {
175
180
  expect(screen.getByText('Friends - 1 members')).toBeInTheDocument()
176
181
  })
177
182
 
183
+ it('should exclude already added recipients from suggestions', async () => {
184
+ const contacts = [
185
+ {
186
+ id: 'df563cc4-6440',
187
+ _id: 'df563cc4-6440',
188
+ _type: 'io.cozy.contacts',
189
+ name: {
190
+ givenName: 'Michale',
191
+ familyName: 'Russel'
192
+ },
193
+ email: [{ address: 'michale@example.com', primary: true }]
194
+ },
195
+ {
196
+ id: '5a3b4ccf-c257',
197
+ _id: '5a3b4ccf-c257',
198
+ _type: 'io.cozy.contacts',
199
+ name: {
200
+ givenName: 'Teagan',
201
+ familyName: 'Wolf'
202
+ },
203
+ email: [{ address: 'teagan@example.com', primary: true }]
204
+ }
205
+ ]
206
+
207
+ const contactGroups = []
208
+ const currentRecipients = [
209
+ { id: 'recipient-1', email: 'michale@example.com' }
210
+ ]
211
+ const recipients = [{ _id: '5a3b4ccf-c257' }]
212
+
213
+ setup({ contacts, contactGroups }, { currentRecipients, recipients })
214
+
215
+ // Michale is in currentRecipients by email, should be filtered out
216
+ expect(screen.queryByText('Michale Russel')).not.toBeInTheDocument()
217
+ // Teagan is in recipients by _id, should be filtered out
218
+ expect(screen.queryByText('Teagan Wolf')).not.toBeInTheDocument()
219
+ })
220
+
178
221
  it('should include unreachable members when flag sharing.show-recipient-groups is activated', async () => {
179
222
  flag.mockReturnValue(true)
180
223
 
@@ -47,3 +47,15 @@ export const getContactsFromGroupId = (contacts = [], groupId) => {
47
47
  )
48
48
  })
49
49
  }
50
+
51
+ /**
52
+ * Returns true when a contact does not exist. A contact without
53
+ * an `_id` is always a placeholder that must be created before sharing.
54
+ *
55
+ * Used to decide whether the autosuggest input should allow creating
56
+ * a new contact on the fly or should block the pick (depending on the
57
+ * `enableCreateContact` flag).
58
+ */
59
+ export const isContactToBeCreated = contact => {
60
+ return !contact._id
61
+ }
@@ -34,4 +34,19 @@ const fullnameMatch = (input, contact) => {
34
34
  return fullnameInput.test(contact.fullname)
35
35
  }
36
36
 
37
- export { emailMatch, cozyUrlMatch, groupNameMatch, fullnameMatch }
37
+ const contactOrGroupMatch = (input, contact) => {
38
+ return (
39
+ groupNameMatch(input, contact) ||
40
+ fullnameMatch(input, contact) ||
41
+ emailMatch(input, contact) ||
42
+ cozyUrlMatch(input, contact)
43
+ )
44
+ }
45
+
46
+ export {
47
+ emailMatch,
48
+ cozyUrlMatch,
49
+ groupNameMatch,
50
+ fullnameMatch,
51
+ contactOrGroupMatch
52
+ }