cozy-sharing 40.1.9 → 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.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -0
- package/dist/SharingProvider.js +3 -1
- package/dist/components/Recipient/PermissionTypeMenuWrapper.spec.js +3 -1
- package/dist/components/ShareAutosuggest.js +87 -14
- package/dist/components/ShareAutosuggest.spec.js +123 -3
- package/dist/components/ShareAutosuggestWrapper.js +61 -5
- package/dist/components/ShareAutosuggestWrapper.spec.js +113 -11
- package/dist/helpers/contacts.js +75 -16
- package/dist/helpers/contacts.spec.js +150 -1
- package/dist/hooks/useResolveRecipients.js +6 -2
- package/dist/hooks/useResolveRecipients.spec.js +11 -2
- package/dist/queries/queries.js +16 -3
- package/package.json +3 -3
- package/src/SharingProvider.jsx +6 -1
- package/src/components/Recipient/PermissionTypeMenuWrapper.spec.jsx +3 -1
- package/src/components/ShareAutosuggest.jsx +45 -7
- package/src/components/ShareAutosuggest.spec.jsx +59 -2
- package/src/components/ShareAutosuggestWrapper.jsx +34 -4
- package/src/components/ShareAutosuggestWrapper.spec.jsx +78 -5
- package/src/helpers/contacts.js +31 -1
- package/src/helpers/contacts.spec.js +102 -1
- package/src/hooks/useResolveRecipients.js +5 -2
- package/src/hooks/useResolveRecipients.spec.js +8 -2
- package/src/queries/queries.js +6 -3
package/dist/helpers/contacts.js
CHANGED
|
@@ -10,16 +10,35 @@ import _regeneratorRuntime from "@babel/runtime/regenerator";
|
|
|
10
10
|
import { models } from 'cozy-client';
|
|
11
11
|
import flag from 'cozy-flags';
|
|
12
12
|
import { validateEmail } from '../helpers/email';
|
|
13
|
+
import { DOCTYPE_CONTACTS, DOCTYPE_GROUPS } from '../models';
|
|
13
14
|
var ContactModel = models.contact; // Return the contact object if present
|
|
14
15
|
// Else check if a contact exist with this email address, if yes return it
|
|
15
16
|
// if not create it
|
|
17
|
+
// With contactsSuggestEnabled, people are sent by email and the
|
|
18
|
+
// stack finds or creates the contact: an org member's id is not on this instance
|
|
16
19
|
|
|
17
20
|
export var getOrCreateFromArray = /*#__PURE__*/function () {
|
|
18
|
-
var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(client, contacts, createContact) {
|
|
21
|
+
var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(client, contacts, createContact, contactsSuggestEnabled) {
|
|
19
22
|
return _regeneratorRuntime.wrap(function _callee2$(_context2) {
|
|
20
23
|
while (1) switch (_context2.prev = _context2.next) {
|
|
21
24
|
case 0:
|
|
22
|
-
|
|
25
|
+
if (!contactsSuggestEnabled) {
|
|
26
|
+
_context2.next = 2;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return _context2.abrupt("return", contacts.map(function (contact) {
|
|
31
|
+
if (contact._type === DOCTYPE_GROUPS) return contact;
|
|
32
|
+
var email = ContactModel.getPrimaryEmail(contact);
|
|
33
|
+
if (validateEmail(email)) return {
|
|
34
|
+
_type: DOCTYPE_CONTACTS,
|
|
35
|
+
email: email
|
|
36
|
+
};
|
|
37
|
+
return contact._id ? contact : undefined;
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
case 2:
|
|
41
|
+
_context2.next = 4;
|
|
23
42
|
return Promise.all(contacts.map( /*#__PURE__*/function () {
|
|
24
43
|
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(contact) {
|
|
25
44
|
var matchedContact, resp;
|
|
@@ -83,24 +102,64 @@ export var getOrCreateFromArray = /*#__PURE__*/function () {
|
|
|
83
102
|
}, _callee);
|
|
84
103
|
}));
|
|
85
104
|
|
|
86
|
-
return function (
|
|
105
|
+
return function (_x5) {
|
|
87
106
|
return _ref2.apply(this, arguments);
|
|
88
107
|
};
|
|
89
108
|
}()));
|
|
90
109
|
|
|
91
|
-
case
|
|
110
|
+
case 4:
|
|
92
111
|
return _context2.abrupt("return", _context2.sent);
|
|
93
112
|
|
|
94
|
-
case
|
|
113
|
+
case 5:
|
|
95
114
|
case "end":
|
|
96
115
|
return _context2.stop();
|
|
97
116
|
}
|
|
98
117
|
}, _callee2);
|
|
99
118
|
}));
|
|
100
119
|
|
|
101
|
-
return function getOrCreateFromArray(_x, _x2, _x3) {
|
|
120
|
+
return function getOrCreateFromArray(_x, _x2, _x3, _x4) {
|
|
102
121
|
return _ref.apply(this, arguments);
|
|
103
122
|
};
|
|
123
|
+
}(); // A suggestion without id has no contact document, and only a fullname,
|
|
124
|
+
// which getDisplayName does not read
|
|
125
|
+
|
|
126
|
+
export var suggestContacts = /*#__PURE__*/function () {
|
|
127
|
+
var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(client, q) {
|
|
128
|
+
var _yield$client$getStac, data;
|
|
129
|
+
|
|
130
|
+
return _regeneratorRuntime.wrap(function _callee3$(_context3) {
|
|
131
|
+
while (1) switch (_context3.prev = _context3.next) {
|
|
132
|
+
case 0:
|
|
133
|
+
_context3.next = 2;
|
|
134
|
+
return client.getStackClient().fetchJSON('GET', "/contacts/suggest?q=".concat(encodeURIComponent(q)));
|
|
135
|
+
|
|
136
|
+
case 2:
|
|
137
|
+
_yield$client$getStac = _context3.sent;
|
|
138
|
+
data = _yield$client$getStac.data;
|
|
139
|
+
return _context3.abrupt("return", data.map(function (_ref4) {
|
|
140
|
+
var id = _ref4.id,
|
|
141
|
+
type = _ref4.type,
|
|
142
|
+
attributes = _ref4.attributes;
|
|
143
|
+
return _objectSpread(_objectSpread(_objectSpread({}, attributes), id ? {
|
|
144
|
+
_id: id,
|
|
145
|
+
id: id
|
|
146
|
+
} : {
|
|
147
|
+
displayName: attributes.fullname
|
|
148
|
+
}), {}, {
|
|
149
|
+
_type: type
|
|
150
|
+
});
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
case 5:
|
|
154
|
+
case "end":
|
|
155
|
+
return _context3.stop();
|
|
156
|
+
}
|
|
157
|
+
}, _callee3);
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
return function suggestContacts(_x6, _x7) {
|
|
161
|
+
return _ref3.apply(this, arguments);
|
|
162
|
+
};
|
|
104
163
|
}();
|
|
105
164
|
export var getContactsFromGroupId = function getContactsFromGroupId() {
|
|
106
165
|
var contacts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
@@ -144,12 +203,12 @@ var isCurrentUser = function isCurrentUser(contact) {
|
|
|
144
203
|
*/
|
|
145
204
|
|
|
146
205
|
|
|
147
|
-
export var computeContactsAndGroups = function computeContactsAndGroups(
|
|
148
|
-
var reachableContacts =
|
|
149
|
-
unreachableContactsWithGroups =
|
|
150
|
-
contactGroups =
|
|
151
|
-
currentRecipients =
|
|
152
|
-
pendingRecipients =
|
|
206
|
+
export var computeContactsAndGroups = function computeContactsAndGroups(_ref5) {
|
|
207
|
+
var reachableContacts = _ref5.reachableContacts,
|
|
208
|
+
unreachableContactsWithGroups = _ref5.unreachableContactsWithGroups,
|
|
209
|
+
contactGroups = _ref5.contactGroups,
|
|
210
|
+
currentRecipients = _ref5.currentRecipients,
|
|
211
|
+
pendingRecipients = _ref5.pendingRecipients;
|
|
153
212
|
var suggestedContacts = reachableContacts.filter(function (contact) {
|
|
154
213
|
return !isCurrentUser(contact) && // We do not want contact already in the sharing
|
|
155
214
|
!currentRecipients.find(function (r) {
|
|
@@ -162,12 +221,12 @@ export var computeContactsAndGroups = function computeContactsAndGroups(_ref3) {
|
|
|
162
221
|
var unreachableContacts = unreachableContactsWithGroups.filter(function (contact) {
|
|
163
222
|
return !isCurrentUser(contact);
|
|
164
223
|
});
|
|
165
|
-
var currentRecipientGroups = currentRecipients.map(function (
|
|
166
|
-
var id =
|
|
224
|
+
var currentRecipientGroups = currentRecipients.map(function (_ref6) {
|
|
225
|
+
var id = _ref6.id;
|
|
167
226
|
return id;
|
|
168
227
|
}).filter(Boolean);
|
|
169
|
-
var suggestedGroups = contactGroups.filter(function (
|
|
170
|
-
var _id =
|
|
228
|
+
var suggestedGroups = contactGroups.filter(function (_ref7) {
|
|
229
|
+
var _id = _ref7._id;
|
|
171
230
|
return !flag('sharing.show-recipient-groups') || !currentRecipientGroups.includes(_id);
|
|
172
231
|
}).map(function (contactGroup) {
|
|
173
232
|
var reachableMembers = getContactsFromGroupId(suggestedContacts, contactGroup.id).map(function (contact) {
|
|
@@ -8,7 +8,7 @@ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { va
|
|
|
8
8
|
|
|
9
9
|
import _regeneratorRuntime from "@babel/runtime/regenerator";
|
|
10
10
|
import flag from 'cozy-flags';
|
|
11
|
-
import { computeContactsAndGroups, getOrCreateFromArray } from './contacts';
|
|
11
|
+
import { computeContactsAndGroups, getOrCreateFromArray, suggestContacts } from './contacts';
|
|
12
12
|
jest.mock('cozy-flags', function () {
|
|
13
13
|
return {
|
|
14
14
|
__esModule: true,
|
|
@@ -184,4 +184,153 @@ describe('computeContactsAndGroups', function () {
|
|
|
184
184
|
return c._id;
|
|
185
185
|
})).toEqual(['alice', 'bob']);
|
|
186
186
|
});
|
|
187
|
+
});
|
|
188
|
+
describe('getOrCreateFromArray with contactsSuggestEnabled', function () {
|
|
189
|
+
it('should send people by email and keep groups, without creating contacts', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2() {
|
|
190
|
+
var createContact, group, recipients;
|
|
191
|
+
return _regeneratorRuntime.wrap(function _callee2$(_context2) {
|
|
192
|
+
while (1) switch (_context2.prev = _context2.next) {
|
|
193
|
+
case 0:
|
|
194
|
+
createContact = jest.fn();
|
|
195
|
+
group = {
|
|
196
|
+
_id: 'group-1',
|
|
197
|
+
_type: 'io.cozy.contacts.groups'
|
|
198
|
+
};
|
|
199
|
+
_context2.next = 4;
|
|
200
|
+
return getOrCreateFromArray(mockClient, [{
|
|
201
|
+
_id: 'org-member-1',
|
|
202
|
+
_type: 'io.cozy.contacts',
|
|
203
|
+
email: [{
|
|
204
|
+
address: 'member@org.example',
|
|
205
|
+
primary: true
|
|
206
|
+
}],
|
|
207
|
+
meta: {
|
|
208
|
+
kind: 'member'
|
|
209
|
+
}
|
|
210
|
+
}, {
|
|
211
|
+
email: 'unknown@friends.example'
|
|
212
|
+
}, group], createContact, true);
|
|
213
|
+
|
|
214
|
+
case 4:
|
|
215
|
+
recipients = _context2.sent;
|
|
216
|
+
expect(recipients).toEqual([{
|
|
217
|
+
_type: 'io.cozy.contacts',
|
|
218
|
+
email: 'member@org.example'
|
|
219
|
+
}, {
|
|
220
|
+
_type: 'io.cozy.contacts',
|
|
221
|
+
email: 'unknown@friends.example'
|
|
222
|
+
}, group]);
|
|
223
|
+
expect(createContact).not.toHaveBeenCalled();
|
|
224
|
+
|
|
225
|
+
case 7:
|
|
226
|
+
case "end":
|
|
227
|
+
return _context2.stop();
|
|
228
|
+
}
|
|
229
|
+
}, _callee2);
|
|
230
|
+
})));
|
|
231
|
+
it('should keep a contact without email by id, and drop a recipient with neither', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3() {
|
|
232
|
+
var cozyOnlyContact, recipients;
|
|
233
|
+
return _regeneratorRuntime.wrap(function _callee3$(_context3) {
|
|
234
|
+
while (1) switch (_context3.prev = _context3.next) {
|
|
235
|
+
case 0:
|
|
236
|
+
cozyOnlyContact = {
|
|
237
|
+
_id: 'contact-1',
|
|
238
|
+
_type: 'io.cozy.contacts',
|
|
239
|
+
cozy: [{
|
|
240
|
+
url: 'https://bob.example'
|
|
241
|
+
}]
|
|
242
|
+
};
|
|
243
|
+
_context3.next = 3;
|
|
244
|
+
return getOrCreateFromArray(mockClient, [cozyOnlyContact, {
|
|
245
|
+
instance: 'https://alice.example'
|
|
246
|
+
}], jest.fn(), true);
|
|
247
|
+
|
|
248
|
+
case 3:
|
|
249
|
+
recipients = _context3.sent;
|
|
250
|
+
expect(recipients).toEqual([cozyOnlyContact, undefined]);
|
|
251
|
+
|
|
252
|
+
case 5:
|
|
253
|
+
case "end":
|
|
254
|
+
return _context3.stop();
|
|
255
|
+
}
|
|
256
|
+
}, _callee3);
|
|
257
|
+
})));
|
|
258
|
+
});
|
|
259
|
+
describe('suggestContacts', function () {
|
|
260
|
+
it('should map the suggestions of the stack to contacts', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee4() {
|
|
261
|
+
var fetchJSON, client, contacts;
|
|
262
|
+
return _regeneratorRuntime.wrap(function _callee4$(_context4) {
|
|
263
|
+
while (1) switch (_context4.prev = _context4.next) {
|
|
264
|
+
case 0:
|
|
265
|
+
fetchJSON = jest.fn().mockResolvedValue({
|
|
266
|
+
data: [{
|
|
267
|
+
type: 'io.cozy.contacts',
|
|
268
|
+
id: 'org-member-1',
|
|
269
|
+
attributes: {
|
|
270
|
+
fullname: 'Jean Dupont',
|
|
271
|
+
name: {
|
|
272
|
+
givenName: 'Jean',
|
|
273
|
+
familyName: 'Dupont'
|
|
274
|
+
},
|
|
275
|
+
email: [{
|
|
276
|
+
address: 'jean@org.example',
|
|
277
|
+
primary: true
|
|
278
|
+
}]
|
|
279
|
+
},
|
|
280
|
+
meta: {
|
|
281
|
+
kind: 'member'
|
|
282
|
+
}
|
|
283
|
+
}, {
|
|
284
|
+
type: 'io.cozy.contacts',
|
|
285
|
+
attributes: {
|
|
286
|
+
fullname: 'Paul Dupuis',
|
|
287
|
+
email: [{
|
|
288
|
+
address: 'paul@friends.example',
|
|
289
|
+
primary: true
|
|
290
|
+
}]
|
|
291
|
+
}
|
|
292
|
+
}]
|
|
293
|
+
});
|
|
294
|
+
client = {
|
|
295
|
+
getStackClient: function getStackClient() {
|
|
296
|
+
return {
|
|
297
|
+
fetchJSON: fetchJSON
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
_context4.next = 4;
|
|
302
|
+
return suggestContacts(client, 'dup & co');
|
|
303
|
+
|
|
304
|
+
case 4:
|
|
305
|
+
contacts = _context4.sent;
|
|
306
|
+
expect(fetchJSON).toHaveBeenCalledWith('GET', '/contacts/suggest?q=dup%20%26%20co');
|
|
307
|
+
expect(contacts).toEqual([{
|
|
308
|
+
_id: 'org-member-1',
|
|
309
|
+
id: 'org-member-1',
|
|
310
|
+
_type: 'io.cozy.contacts',
|
|
311
|
+
fullname: 'Jean Dupont',
|
|
312
|
+
name: {
|
|
313
|
+
givenName: 'Jean',
|
|
314
|
+
familyName: 'Dupont'
|
|
315
|
+
},
|
|
316
|
+
email: [{
|
|
317
|
+
address: 'jean@org.example',
|
|
318
|
+
primary: true
|
|
319
|
+
}]
|
|
320
|
+
}, {
|
|
321
|
+
_type: 'io.cozy.contacts',
|
|
322
|
+
fullname: 'Paul Dupuis',
|
|
323
|
+
displayName: 'Paul Dupuis',
|
|
324
|
+
email: [{
|
|
325
|
+
address: 'paul@friends.example',
|
|
326
|
+
primary: true
|
|
327
|
+
}]
|
|
328
|
+
}]);
|
|
329
|
+
|
|
330
|
+
case 7:
|
|
331
|
+
case "end":
|
|
332
|
+
return _context4.stop();
|
|
333
|
+
}
|
|
334
|
+
}, _callee4);
|
|
335
|
+
})));
|
|
187
336
|
});
|
|
@@ -2,6 +2,7 @@ import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
|
|
|
2
2
|
import _regeneratorRuntime from "@babel/runtime/regenerator";
|
|
3
3
|
import { useCallback } from 'react';
|
|
4
4
|
import { useClient } from 'cozy-client';
|
|
5
|
+
import { useSharingContext } from './useSharingContext';
|
|
5
6
|
import { getOrCreateFromArray } from '../helpers/contacts';
|
|
6
7
|
import { DOCTYPE_CONTACTS } from '../models';
|
|
7
8
|
/**
|
|
@@ -13,7 +14,10 @@ import { DOCTYPE_CONTACTS } from '../models';
|
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
export var useResolveRecipients = function useResolveRecipients() {
|
|
17
|
+
var _useSharingContext;
|
|
18
|
+
|
|
16
19
|
var client = useClient();
|
|
20
|
+
var contactsSuggestEnabled = (_useSharingContext = useSharingContext()) === null || _useSharingContext === void 0 ? void 0 : _useSharingContext.contactsSuggestEnabled;
|
|
17
21
|
return useCallback( /*#__PURE__*/function () {
|
|
18
22
|
var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(pendingRecipients) {
|
|
19
23
|
var selectedOption,
|
|
@@ -29,7 +33,7 @@ export var useResolveRecipients = function useResolveRecipients() {
|
|
|
29
33
|
return client.create(DOCTYPE_CONTACTS, contact);
|
|
30
34
|
};
|
|
31
35
|
_context.next = 4;
|
|
32
|
-
return getOrCreateFromArray(client, pendingRecipients, createContact);
|
|
36
|
+
return getOrCreateFromArray(client, pendingRecipients, createContact, contactsSuggestEnabled);
|
|
33
37
|
|
|
34
38
|
case 4:
|
|
35
39
|
contacts = _context.sent;
|
|
@@ -49,5 +53,5 @@ export var useResolveRecipients = function useResolveRecipients() {
|
|
|
49
53
|
return function (_x) {
|
|
50
54
|
return _ref.apply(this, arguments);
|
|
51
55
|
};
|
|
52
|
-
}(), [client]);
|
|
56
|
+
}(), [client, contactsSuggestEnabled]);
|
|
53
57
|
};
|
|
@@ -18,6 +18,15 @@ jest.mock('../helpers/contacts', function () {
|
|
|
18
18
|
getOrCreateFromArray: jest.fn()
|
|
19
19
|
};
|
|
20
20
|
});
|
|
21
|
+
jest.mock('./useSharingContext', function () {
|
|
22
|
+
return {
|
|
23
|
+
useSharingContext: function useSharingContext() {
|
|
24
|
+
return {
|
|
25
|
+
contactsSuggestEnabled: true
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
});
|
|
21
30
|
var contacts = [{
|
|
22
31
|
_id: 'alice'
|
|
23
32
|
}, {
|
|
@@ -102,7 +111,7 @@ describe('useResolveRecipients', function () {
|
|
|
102
111
|
address: 'alice@cozy.cc'
|
|
103
112
|
}]
|
|
104
113
|
});
|
|
105
|
-
expect(getOrCreateFromArray).toHaveBeenCalledWith(mockClient, pendingRecipients, expect.any(Function));
|
|
114
|
+
expect(getOrCreateFromArray).toHaveBeenCalledWith(mockClient, pendingRecipients, expect.any(Function), true);
|
|
106
115
|
expect(mockClient.create).toHaveBeenCalledWith('io.cozy.contacts', {
|
|
107
116
|
email: [{
|
|
108
117
|
address: 'alice@cozy.cc'
|
|
@@ -129,7 +138,7 @@ describe('useResolveRecipients', function () {
|
|
|
129
138
|
return result.current([], 'readWrite', createContact);
|
|
130
139
|
|
|
131
140
|
case 4:
|
|
132
|
-
expect(getOrCreateFromArray).toHaveBeenCalledWith(mockClient, [], createContact);
|
|
141
|
+
expect(getOrCreateFromArray).toHaveBeenCalledWith(mockClient, [], createContact, true);
|
|
133
142
|
|
|
134
143
|
case 5:
|
|
135
144
|
case "end":
|
package/dist/queries/queries.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
|
|
3
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
4
|
+
|
|
5
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
|
6
|
+
|
|
1
7
|
import { Q, fetchPolicies } from 'cozy-client';
|
|
2
8
|
import { DOCTYPE_CONTACTS, DOCTYPE_GROUPS } from '../models';
|
|
3
9
|
var DEFAULT_CACHE_TIMEOUT_QUERIES = 9 * 60 * 1000;
|
|
@@ -12,12 +18,15 @@ export var buildSharingsByIdQuery = function buildSharingsByIdQuery(sharingId) {
|
|
|
12
18
|
};
|
|
13
19
|
};
|
|
14
20
|
export var buildReachableContactsQuery = function buildReachableContactsQuery() {
|
|
21
|
+
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
|
|
22
|
+
inGroups = _ref.inGroups;
|
|
23
|
+
|
|
15
24
|
return {
|
|
16
25
|
definition: Q(DOCTYPE_CONTACTS).where({
|
|
17
26
|
_id: {
|
|
18
27
|
$gt: null
|
|
19
28
|
}
|
|
20
|
-
}).partialIndex({
|
|
29
|
+
}).partialIndex(_objectSpread({
|
|
21
30
|
trashed: {
|
|
22
31
|
$or: [{
|
|
23
32
|
$eq: false
|
|
@@ -38,9 +47,13 @@ export var buildReachableContactsQuery = function buildReachableContactsQuery()
|
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
49
|
}]
|
|
41
|
-
}
|
|
50
|
+
}, inGroups && {
|
|
51
|
+
'relationships.groups.data': {
|
|
52
|
+
$exists: true
|
|
53
|
+
}
|
|
54
|
+
})).indexFields(['_id']).limitBy(1000),
|
|
42
55
|
options: {
|
|
43
|
-
as: 'io.cozy.contacts/reachable'
|
|
56
|
+
as: inGroups ? 'io.cozy.contacts/reachable-with-groups' : 'io.cozy.contacts/reachable'
|
|
44
57
|
}
|
|
45
58
|
};
|
|
46
59
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozy-sharing",
|
|
3
|
-
"version": "40.
|
|
3
|
+
"version": "40.2.0",
|
|
4
4
|
"description": "Provides sharing login for React applications.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"author": "Cozy",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"cozy-intent": "^2.31.1",
|
|
59
59
|
"cozy-minilog": "^3.10.1",
|
|
60
60
|
"cozy-ui": "^140.5.0",
|
|
61
|
-
"cozy-ui-plus": "^14.1.
|
|
61
|
+
"cozy-ui-plus": "^14.1.5",
|
|
62
62
|
"jest": "30.3.0",
|
|
63
63
|
"jest-environment-jsdom": "30.3.0",
|
|
64
64
|
"react": "16.12.0",
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"sideEffects": [
|
|
85
85
|
"*.css"
|
|
86
86
|
],
|
|
87
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "5641a18cdc0008e4a73d0d00ffc7b7e58b124185"
|
|
88
88
|
}
|
package/src/SharingProvider.jsx
CHANGED
|
@@ -745,7 +745,12 @@ export class SharingProvider extends Component {
|
|
|
745
745
|
// but if we don't, consumers don't rerender when the state changes after loading the sharings,
|
|
746
746
|
// probably because the state object remains the same...
|
|
747
747
|
return (
|
|
748
|
-
<SharingContext.Provider
|
|
748
|
+
<SharingContext.Provider
|
|
749
|
+
value={{
|
|
750
|
+
...this.state,
|
|
751
|
+
contactsSuggestEnabled: this.props.contactsSuggestEnabled
|
|
752
|
+
}}
|
|
753
|
+
>
|
|
749
754
|
{this.props.children}
|
|
750
755
|
</SharingContext.Provider>
|
|
751
756
|
)
|
|
@@ -160,7 +160,9 @@ describe('PermissionTypeMenu component', () => {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
beforeEach(() => {
|
|
163
|
-
flag.
|
|
163
|
+
flag.mockImplementation(
|
|
164
|
+
name => name === 'drive.federated-shared-folder.enabled'
|
|
165
|
+
)
|
|
164
166
|
mockHasSharedParent.mockReturnValue(true)
|
|
165
167
|
mockGetSharingById.mockReturnValue({
|
|
166
168
|
id: 'sharing-123',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import PropTypes from 'prop-types'
|
|
2
|
-
import React, { useState, useRef } from 'react'
|
|
2
|
+
import React, { useEffect, useState, useRef } from 'react'
|
|
3
3
|
import Autosuggest from 'react-autosuggest'
|
|
4
4
|
|
|
5
5
|
import Chip from 'cozy-ui/transpiled/react/Chips'
|
|
@@ -10,10 +10,14 @@ import { MemberAvatar } from './Avatar/MemberAvatar'
|
|
|
10
10
|
import ContactSuggestion from './ContactSuggestion'
|
|
11
11
|
import { isContactToBeCreated } from '../helpers/contacts'
|
|
12
12
|
import { extractEmails, validateEmail } from '../helpers/email'
|
|
13
|
+
import logger from '../logger'
|
|
13
14
|
import { getDisplayName, DOCTYPE_CONTACTS, DOCTYPE_GROUPS } from '../models'
|
|
14
15
|
import styles from '../styles/autosuggest.styl'
|
|
15
16
|
import { contactOrGroupMatch } from '../suggestionMatchers'
|
|
16
17
|
|
|
18
|
+
const FETCH_CONTACTS_DEBOUNCE = 300
|
|
19
|
+
const FETCH_CONTACTS_MIN_LENGTH = 3
|
|
20
|
+
|
|
17
21
|
const ShareAutosuggest = ({
|
|
18
22
|
loading,
|
|
19
23
|
disabled,
|
|
@@ -24,15 +28,21 @@ const ShareAutosuggest = ({
|
|
|
24
28
|
enableCreateContact,
|
|
25
29
|
placeholder,
|
|
26
30
|
autoFocus,
|
|
27
|
-
endAdornment
|
|
31
|
+
endAdornment,
|
|
32
|
+
fetchContacts
|
|
28
33
|
}) => {
|
|
29
34
|
const [inputValue, setInputValue] = useState('')
|
|
30
35
|
const [suggestions, setSuggestions] = useState([])
|
|
31
36
|
const [isPasted, setIsPasted] = useState(false)
|
|
32
37
|
const [isLoadingDisplayed, setLoadingDisplayed] = useState(false)
|
|
38
|
+
const [isFetching, setIsFetching] = useState(false)
|
|
33
39
|
|
|
34
40
|
const autosuggestRef = useRef(null)
|
|
35
41
|
const containerRef = useRef(null)
|
|
42
|
+
const fetchTimeoutRef = useRef(null)
|
|
43
|
+
const fetchedValueRef = useRef(null)
|
|
44
|
+
|
|
45
|
+
useEffect(() => () => clearTimeout(fetchTimeoutRef.current), [])
|
|
36
46
|
|
|
37
47
|
const computeSuggestions = value => {
|
|
38
48
|
const inputValue = value.trim().toLowerCase()
|
|
@@ -44,9 +54,7 @@ const ShareAutosuggest = ({
|
|
|
44
54
|
)
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
const
|
|
48
|
-
const computedSuggestions = computeSuggestions(value)
|
|
49
|
-
|
|
57
|
+
const showSuggestions = (computedSuggestions, value) => {
|
|
50
58
|
if (computedSuggestions.length > 0) {
|
|
51
59
|
setSuggestions(computedSuggestions)
|
|
52
60
|
} else if (enableCreateContact && validateEmail(value)) {
|
|
@@ -61,7 +69,36 @@ const ShareAutosuggest = ({
|
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
const onSuggestionsFetchRequested = ({ value }) => {
|
|
73
|
+
const computedSuggestions = computeSuggestions(value)
|
|
74
|
+
clearTimeout(fetchTimeoutRef.current)
|
|
75
|
+
fetchedValueRef.current = value
|
|
76
|
+
|
|
77
|
+
if (!fetchContacts || value.trim().length < FETCH_CONTACTS_MIN_LENGTH) {
|
|
78
|
+
setIsFetching(false)
|
|
79
|
+
showSuggestions(computedSuggestions, value)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setIsFetching(true)
|
|
84
|
+
fetchTimeoutRef.current = setTimeout(async () => {
|
|
85
|
+
let contacts = []
|
|
86
|
+
try {
|
|
87
|
+
contacts = await fetchContacts(value.trim())
|
|
88
|
+
} catch (err) {
|
|
89
|
+
logger.error('Failed to fetch contact suggestions', err)
|
|
90
|
+
}
|
|
91
|
+
// The user kept typing or left the input while the stack was answering
|
|
92
|
+
if (fetchedValueRef.current !== value) return
|
|
93
|
+
setIsFetching(false)
|
|
94
|
+
showSuggestions([...contacts, ...computedSuggestions], value)
|
|
95
|
+
}, FETCH_CONTACTS_DEBOUNCE)
|
|
96
|
+
}
|
|
97
|
+
|
|
64
98
|
const onSuggestionsClearRequested = () => {
|
|
99
|
+
clearTimeout(fetchTimeoutRef.current)
|
|
100
|
+
fetchedValueRef.current = null
|
|
101
|
+
setIsFetching(false)
|
|
65
102
|
setSuggestions([])
|
|
66
103
|
}
|
|
67
104
|
|
|
@@ -174,7 +211,7 @@ const ShareAutosuggest = ({
|
|
|
174
211
|
)
|
|
175
212
|
})}
|
|
176
213
|
<input {...inputProps} onKeyPress={onKeyPress} onKeyUp={onKeyUp} />
|
|
177
|
-
{loading && isLoadingDisplayed ? (
|
|
214
|
+
{(loading || isFetching) && isLoadingDisplayed ? (
|
|
178
215
|
<Spinner color="var(--primaryColor)" />
|
|
179
216
|
) : null}
|
|
180
217
|
{endAdornment ? (
|
|
@@ -231,7 +268,8 @@ ShareAutosuggest.propTypes = {
|
|
|
231
268
|
enableCreateContact: PropTypes.bool,
|
|
232
269
|
placeholder: PropTypes.string,
|
|
233
270
|
autoFocus: PropTypes.bool,
|
|
234
|
-
endAdornment: PropTypes.node
|
|
271
|
+
endAdornment: PropTypes.node,
|
|
272
|
+
fetchContacts: PropTypes.func
|
|
235
273
|
}
|
|
236
274
|
|
|
237
275
|
export default ShareAutosuggest
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, fireEvent, screen } from '@testing-library/react'
|
|
1
|
+
import { act, render, fireEvent, screen } from '@testing-library/react'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
4
|
import ShareAutosuggest from './ShareAutosuggest'
|
|
@@ -13,7 +13,8 @@ describe('ShareAutosuggest', () => {
|
|
|
13
13
|
onPick,
|
|
14
14
|
onRemove,
|
|
15
15
|
loading = false,
|
|
16
|
-
enableCreateContact = true
|
|
16
|
+
enableCreateContact = true,
|
|
17
|
+
fetchContacts
|
|
17
18
|
}) => {
|
|
18
19
|
render(
|
|
19
20
|
<AppLike>
|
|
@@ -25,6 +26,7 @@ describe('ShareAutosuggest', () => {
|
|
|
25
26
|
recipients={[]}
|
|
26
27
|
onRemove={onRemove}
|
|
27
28
|
enableCreateContact={enableCreateContact}
|
|
29
|
+
fetchContacts={fetchContacts}
|
|
28
30
|
/>
|
|
29
31
|
</AppLike>
|
|
30
32
|
)
|
|
@@ -88,4 +90,59 @@ describe('ShareAutosuggest', () => {
|
|
|
88
90
|
|
|
89
91
|
expect(screen.queryByText('loading')).toBeNull()
|
|
90
92
|
})
|
|
93
|
+
|
|
94
|
+
describe('with fetchContacts', () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
jest.useFakeTimers()
|
|
97
|
+
// Not implemented by jsdom, called when a suggestion is highlighted
|
|
98
|
+
Element.prototype.scrollIntoView = jest.fn()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
jest.useRealTimers()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should suggest the fetched contacts, debounced and from 3 characters', async () => {
|
|
106
|
+
const fetchContacts = jest.fn().mockResolvedValue([
|
|
107
|
+
{
|
|
108
|
+
_id: 'org-member-1',
|
|
109
|
+
_type: 'io.cozy.contacts',
|
|
110
|
+
displayName: 'Jean Dupont',
|
|
111
|
+
email: [{ address: 'jean@org.example', primary: true }]
|
|
112
|
+
}
|
|
113
|
+
])
|
|
114
|
+
|
|
115
|
+
setup({ onPick: jest.fn(), onRemove: jest.fn(), fetchContacts })
|
|
116
|
+
|
|
117
|
+
const inputNode = screen.getByPlaceholderText('myPlaceHolder')
|
|
118
|
+
fireEvent.change(inputNode, { target: { value: 'du' } })
|
|
119
|
+
act(() => jest.runAllTimers())
|
|
120
|
+
expect(fetchContacts).not.toHaveBeenCalled()
|
|
121
|
+
|
|
122
|
+
fireEvent.change(inputNode, { target: { value: 'dup' } })
|
|
123
|
+
fireEvent.change(inputNode, { target: { value: 'dupo' } })
|
|
124
|
+
act(() => jest.runAllTimers())
|
|
125
|
+
expect(fetchContacts).toHaveBeenCalledTimes(1)
|
|
126
|
+
expect(fetchContacts).toHaveBeenCalledWith('dupo')
|
|
127
|
+
|
|
128
|
+
expect(await screen.findByText('Jean Dupont')).toBeInTheDocument()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should show the loading indicator until the contacts are fetched', async () => {
|
|
132
|
+
let resolveFetch
|
|
133
|
+
const fetchContacts = jest.fn(
|
|
134
|
+
() => new Promise(resolve => (resolveFetch = resolve))
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
setup({ onPick: jest.fn(), onRemove: jest.fn(), fetchContacts })
|
|
138
|
+
|
|
139
|
+
const inputNode = screen.getByPlaceholderText('myPlaceHolder')
|
|
140
|
+
fireEvent.change(inputNode, { target: { value: 'dup' } })
|
|
141
|
+
expect(screen.getByText('loading')).toBeInTheDocument()
|
|
142
|
+
|
|
143
|
+
act(() => jest.runAllTimers())
|
|
144
|
+
await act(async () => resolveFetch([]))
|
|
145
|
+
expect(screen.queryByText('loading')).toBeNull()
|
|
146
|
+
})
|
|
147
|
+
})
|
|
91
148
|
})
|