cozy-sharing 33.1.1 → 33.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 CHANGED
@@ -3,6 +3,19 @@
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.2.0](https://github.com/cozy/cozy-libs/compare/cozy-sharing@33.1.2...cozy-sharing@33.2.0) (2026-06-09)
7
+
8
+ ### Features
9
+
10
+ - **cozy-sharing:** Show progress icon on done button during share ([885a715](https://github.com/cozy/cozy-libs/commit/885a71568a96354e3d962296a205efb05881a5d1))
11
+
12
+ ## [33.1.2](https://github.com/cozy/cozy-libs/compare/cozy-sharing@33.1.1...cozy-sharing@33.1.2) (2026-06-08)
13
+
14
+ ### Bug Fixes
15
+
16
+ - **cozy-sharing:** Avoid regex parsing in input matchers ([ecab525](https://github.com/cozy/cozy-libs/commit/ecab525b7559710d693787fb50c6c5fe8abd55ac))
17
+ - **cozy-sharing:** Make matching diacritic-insensitive ([e421a5a](https://github.com/cozy/cozy-libs/commit/e421a5ac5f97f64ef27340a3b87f9f15b3efebd9))
18
+
6
19
  ## [33.1.1](https://github.com/cozy/cozy-libs/compare/cozy-sharing@33.1.0...cozy-sharing@33.1.1) (2026-06-04)
7
20
 
8
21
  ### Bug Fixes
@@ -307,7 +307,7 @@ var FederatedFolderModalContent = function FederatedFolderModalContent(_ref) {
307
307
  }), canShareByEmail && /*#__PURE__*/React.createElement(Button, {
308
308
  variant: "primary",
309
309
  label: t('FederatedFolder.share'),
310
- disabled: isSending,
310
+ busy: isSending,
311
311
  onClick: onSend
312
312
  }))
313
313
  });
@@ -7,7 +7,7 @@ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (O
7
7
  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; }
8
8
 
9
9
  import _regeneratorRuntime from "@babel/runtime/regenerator";
10
- import { fireEvent, render, waitFor } from '@testing-library/react';
10
+ import { act, fireEvent, render, waitFor } from '@testing-library/react';
11
11
  import React from 'react';
12
12
  import { createMockClient } from 'cozy-client';
13
13
  import { FederatedFolderModal } from './FederatedFolderModal';
@@ -384,36 +384,88 @@ describe('FederatedFolderModal', function () {
384
384
  }
385
385
  }, _callee9);
386
386
  })));
387
+ it('should show progress icon on Done button while share is in progress', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee11() {
388
+ var resolveShare, _setup10, findByText, getByRole, queryByRole, sendButton;
389
+
390
+ return _regeneratorRuntime.wrap(function _callee11$(_context11) {
391
+ while (1) switch (_context11.prev = _context11.next) {
392
+ case 0:
393
+ usePendingRecipients.mockReturnValue({
394
+ pendingRecipients: [pendingRecipient],
395
+ setPendingRecipients: jest.fn(),
396
+ selectedOption: 'readWrite',
397
+ setSelectedOption: jest.fn()
398
+ });
399
+ getOrCreateFromArray.mockResolvedValue([pendingRecipient]);
400
+ mockShare.mockReturnValueOnce(new Promise(function (resolve) {
401
+ resolveShare = resolve;
402
+ }));
403
+ _setup10 = setup(), findByText = _setup10.findByText, getByRole = _setup10.getByRole, queryByRole = _setup10.queryByRole;
404
+ _context11.next = 6;
405
+ return findByText('Done');
406
+
407
+ case 6:
408
+ sendButton = _context11.sent;
409
+ fireEvent.click(sendButton);
410
+ _context11.next = 10;
411
+ return waitFor(function () {
412
+ expect(getByRole('button', {
413
+ name: 'Done'
414
+ })).toBeDisabled();
415
+ expect(queryByRole('progressbar')).not.toBeNull();
416
+ });
417
+
418
+ case 10:
419
+ _context11.next = 12;
420
+ return act( /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee10() {
421
+ return _regeneratorRuntime.wrap(function _callee10$(_context10) {
422
+ while (1) switch (_context10.prev = _context10.next) {
423
+ case 0:
424
+ resolveShare({});
425
+
426
+ case 1:
427
+ case "end":
428
+ return _context10.stop();
429
+ }
430
+ }, _callee10);
431
+ })));
432
+
433
+ case 12:
434
+ case "end":
435
+ return _context11.stop();
436
+ }
437
+ }, _callee11);
438
+ })));
387
439
  });
388
440
  describe('onClose callback', function () {
389
- it('should call onClose when close button is clicked', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee10() {
390
- var _setup10, findByRole, closeButton;
441
+ it('should call onClose when close button is clicked', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee12() {
442
+ var _setup11, findByRole, closeButton;
391
443
 
392
- return _regeneratorRuntime.wrap(function _callee10$(_context10) {
393
- while (1) switch (_context10.prev = _context10.next) {
444
+ return _regeneratorRuntime.wrap(function _callee12$(_context12) {
445
+ while (1) switch (_context12.prev = _context12.next) {
394
446
  case 0:
395
- _setup10 = setup(), findByRole = _setup10.findByRole;
396
- _context10.next = 3;
447
+ _setup11 = setup(), findByRole = _setup11.findByRole;
448
+ _context12.next = 3;
397
449
  return findByRole('button', {
398
450
  name: /close/i
399
451
  });
400
452
 
401
453
  case 3:
402
- closeButton = _context10.sent;
454
+ closeButton = _context12.sent;
403
455
  fireEvent.click(closeButton);
404
456
  expect(mockOnClose).toHaveBeenCalled();
405
457
 
406
458
  case 6:
407
459
  case "end":
408
- return _context10.stop();
460
+ return _context12.stop();
409
461
  }
410
- }, _callee10);
462
+ }, _callee12);
411
463
  })));
412
- it('should ask confirmation when close button is clicked with pending recipients', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee11() {
413
- var _setup11, getByRole, findByRole, getByText, closeButton;
464
+ it('should ask confirmation when close button is clicked with pending recipients', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee13() {
465
+ var _setup12, getByRole, findByRole, getByText, closeButton;
414
466
 
415
- return _regeneratorRuntime.wrap(function _callee11$(_context11) {
416
- while (1) switch (_context11.prev = _context11.next) {
467
+ return _regeneratorRuntime.wrap(function _callee13$(_context13) {
468
+ while (1) switch (_context13.prev = _context13.next) {
417
469
  case 0:
418
470
  usePendingRecipients.mockReturnValue({
419
471
  pendingRecipients: [{
@@ -424,14 +476,14 @@ describe('FederatedFolderModal', function () {
424
476
  selectedOption: 'readWrite',
425
477
  setSelectedOption: jest.fn()
426
478
  });
427
- _setup11 = setup(), getByRole = _setup11.getByRole, findByRole = _setup11.findByRole, getByText = _setup11.getByText;
428
- _context11.next = 4;
479
+ _setup12 = setup(), getByRole = _setup12.getByRole, findByRole = _setup12.findByRole, getByText = _setup12.getByText;
480
+ _context13.next = 4;
429
481
  return findByRole('button', {
430
482
  name: /close/i
431
483
  });
432
484
 
433
485
  case 4:
434
- closeButton = _context11.sent;
486
+ closeButton = _context13.sent;
435
487
  fireEvent.click(closeButton);
436
488
  expect(getByText("Discard the changes you haven't saved?")).toBeTruthy();
437
489
  expect(mockOnClose).not.toHaveBeenCalled();
@@ -442,15 +494,15 @@ describe('FederatedFolderModal', function () {
442
494
 
443
495
  case 10:
444
496
  case "end":
445
- return _context11.stop();
497
+ return _context13.stop();
446
498
  }
447
- }, _callee11);
499
+ }, _callee13);
448
500
  })));
449
- it('should close after discard confirmation', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee12() {
450
- var _setup12, getByRole, findByRole, getByText, closeButton;
501
+ it('should close after discard confirmation', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee14() {
502
+ var _setup13, getByRole, findByRole, getByText, closeButton;
451
503
 
452
- return _regeneratorRuntime.wrap(function _callee12$(_context12) {
453
- while (1) switch (_context12.prev = _context12.next) {
504
+ return _regeneratorRuntime.wrap(function _callee14$(_context14) {
505
+ while (1) switch (_context14.prev = _context14.next) {
454
506
  case 0:
455
507
  usePendingRecipients.mockReturnValue({
456
508
  pendingRecipients: [{
@@ -461,14 +513,14 @@ describe('FederatedFolderModal', function () {
461
513
  selectedOption: 'readWrite',
462
514
  setSelectedOption: jest.fn()
463
515
  });
464
- _setup12 = setup(), getByRole = _setup12.getByRole, findByRole = _setup12.findByRole, getByText = _setup12.getByText;
465
- _context12.next = 4;
516
+ _setup13 = setup(), getByRole = _setup13.getByRole, findByRole = _setup13.findByRole, getByText = _setup13.getByText;
517
+ _context14.next = 4;
466
518
  return findByRole('button', {
467
519
  name: /close/i
468
520
  });
469
521
 
470
522
  case 4:
471
- closeButton = _context12.sent;
523
+ closeButton = _context14.sent;
472
524
  fireEvent.click(closeButton);
473
525
  expect(getByText("Discard the changes you haven't saved?")).toBeTruthy();
474
526
  fireEvent.click(getByRole('button', {
@@ -478,9 +530,9 @@ describe('FederatedFolderModal', function () {
478
530
 
479
531
  case 9:
480
532
  case "end":
481
- return _context12.stop();
533
+ return _context14.stop();
482
534
  }
483
- }, _callee12);
535
+ }, _callee14);
484
536
  })));
485
537
  });
486
538
  });
@@ -1,43 +1,64 @@
1
- import escapeRegExp from 'lodash/escapeRegExp';
2
- import { Group } from 'cozy-doctypes'; // TODO: sadly we have different versions of contacts' doctype to handle...
1
+ import { Group } from 'cozy-doctypes';
2
+ var removeDiacritics;
3
+
4
+ try {
5
+ new RegExp('\\p{Diacritic}', 'gu');
6
+
7
+ removeDiacritics = function removeDiacritics(str) {
8
+ return str.replace(/(?:[\^`\xA8\xAF\xB4\xB7\xB8\u02B0-\u034E\u0350-\u0357\u035D-\u0362\u0374\u0375\u037A\u0384\u0385\u0483-\u0487\u0559\u0591-\u05A1\u05A3-\u05BD\u05BF\u05C1\u05C2\u05C4\u064B-\u0652\u0657\u0658\u06DF\u06E0\u06E5\u06E6\u06EA-\u06EC\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F5\u0818\u0819\u0898-\u089F\u08C9-\u08D2\u08E3-\u08FE\u093C\u094D\u0951-\u0954\u0971\u09BC\u09CD\u0A3C\u0A4D\u0ABC\u0ACD\u0AFD-\u0AFF\u0B3C\u0B4D\u0B55\u0BCD\u0C3C\u0C4D\u0CBC\u0CCD\u0D3B\u0D3C\u0D4D\u0DCA\u0E3A\u0E47-\u0E4C\u0E4E\u0EBA\u0EC8-\u0ECC\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F82-\u0F84\u0F86\u0F87\u0FC6\u1037\u1039\u103A\u1063\u1064\u1069-\u106D\u1087-\u108D\u108F\u109A\u109B\u135D-\u135F\u1714\u1715\u1734\u17C9-\u17D3\u17DD\u1939-\u193B\u1A60\u1A75-\u1A7C\u1A7F\u1AB0-\u1ABE\u1AC1-\u1ACB\u1B34\u1B44\u1B6B-\u1B73\u1BAA\u1BAB\u1BE6\u1BF2\u1BF3\u1C36\u1C37\u1C78-\u1C7D\u1CD0-\u1CE8\u1CED\u1CF4\u1CF7-\u1CF9\u1D2C-\u1D6A\u1DC4-\u1DCF\u1DF5-\u1DFF\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2CEF-\u2CF1\u2E2F\u302A-\u302F\u3099-\u309C\u30FC\uA66F\uA67C\uA67D\uA67F\uA69C\uA69D\uA6F0\uA6F1\uA700-\uA721\uA788-\uA78A\uA7F8\uA7F9\uA806\uA82C\uA8C4\uA8E0-\uA8F1\uA92B-\uA92E\uA953\uA9B3\uA9C0\uA9E5\uAA7B-\uAA7D\uAABF-\uAAC2\uAAF6\uAB5B-\uAB5F\uAB69-\uAB6B\uABEC\uABED\uFB1E\uFE20-\uFE2F\uFF3E\uFF40\uFF70\uFF9E\uFF9F\uFFE3]|\uD800\uDEE0|\uD801[\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDE38-\uDE3A\uDE3F\uDEE5\uDEE6]|\uD803[\uDD22-\uDD27\uDD4E\uDD69-\uDD6D\uDEFD-\uDEFF\uDF46-\uDF50\uDF82-\uDF85]|\uD804[\uDC46\uDC70\uDCB9\uDCBA\uDD33\uDD34\uDD73\uDDC0\uDDCA-\uDDCC\uDE35\uDE36\uDEE9\uDEEA\uDF3B\uDF3C\uDF4D\uDF66-\uDF6C\uDF70-\uDF74\uDFCE-\uDFD0\uDFD2\uDFD3\uDFE1\uDFE2]|\uD805[\uDC42\uDC46\uDCC2\uDCC3\uDDBF\uDDC0\uDE3F\uDEB6\uDEB7\uDF2B]|\uD806[\uDC39\uDC3A\uDD3D\uDD3E\uDD43\uDDE0\uDE34\uDE47\uDE99]|\uD807[\uDC3F\uDD42\uDD44\uDD45\uDD97\uDF41\uDF42\uDF5A]|\uD80D[\uDC47-\uDC55]|\uD818\uDD2F|\uD81A[\uDEF0-\uDEF4\uDF30-\uDF36]|\uD81B[\uDD6B\uDD6C\uDF8F-\uDF9F\uDFF0\uDFF1]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD833[\uDF00-\uDF2D\uDF30-\uDF46]|\uD834[\uDD67-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD]|\uD838[\uDC30-\uDC6D\uDD30-\uDD36\uDEAE\uDEEC-\uDEEF]|\uD839[\uDDEE\uDDEF]|\uD83A[\uDCD0-\uDCD6\uDD44-\uDD46\uDD48-\uDD4A])/g, '');
9
+ };
10
+ } catch (_unused) {
11
+ removeDiacritics = function removeDiacritics(str) {
12
+ return str.replace(/[̀-ͯ]/g, '');
13
+ };
14
+ }
15
+
16
+ var normalize = function normalize(str) {
17
+ return removeDiacritics(str.normalize('NFD'));
18
+ };
19
+
20
+ var normalizeLowercase = function normalizeLowercase(str) {
21
+ return normalize(str).toLowerCase();
22
+ }; // TODO: sadly we have different versions of contacts' doctype to handle...
3
23
  // A migration tool on the stack side is needed here
4
24
 
25
+
5
26
  var emailMatch = function emailMatch(input, contact) {
6
27
  if (!contact.email) return false;
7
- var emailInput = new RegExp(escapeRegExp(input), 'i');
28
+ var normalizedInput = normalizeLowercase(input);
8
29
 
9
30
  if (Array.isArray(contact.email)) {
10
31
  return contact.email.some(function (email) {
11
- return emailInput.test(email.address);
32
+ return normalizeLowercase(email.address).includes(normalizedInput);
12
33
  });
13
34
  }
14
35
 
15
- return emailInput.test(contact.email);
36
+ return normalizeLowercase(contact.email).includes(normalizedInput);
16
37
  };
17
38
 
18
39
  var cozyUrlMatch = function cozyUrlMatch(input, contact) {
19
40
  if (!contact.cozy && !contact.url) return false;
20
- var urlInput = new RegExp(escapeRegExp(input), 'i');
41
+ var normalizedInput = normalizeLowercase(input);
21
42
 
22
43
  if (contact.cozy && Array.isArray(contact.cozy)) {
23
44
  return contact.cozy.some(function (cozy) {
24
- return urlInput.test(cozy.url);
45
+ return normalizeLowercase(cozy.url).includes(normalizedInput);
25
46
  });
26
47
  }
27
48
 
28
- return urlInput.test(contact.url);
49
+ return normalizeLowercase(contact.url).includes(normalizedInput);
29
50
  };
30
51
 
31
52
  var groupNameMatch = function groupNameMatch(input, contactOrGroup) {
32
53
  if (contactOrGroup._type !== Group.doctype) return false;
33
- var nameInput = new RegExp(escapeRegExp(input), 'i');
34
- return nameInput.test(contactOrGroup.name);
54
+ var normalizedInput = normalizeLowercase(input);
55
+ return normalizeLowercase(contactOrGroup.name).includes(normalizedInput);
35
56
  };
36
57
 
37
58
  var fullnameMatch = function fullnameMatch(input, contact) {
38
59
  if (!contact.fullname) return false;
39
- var fullnameInput = new RegExp(escapeRegExp(input), 'i');
40
- return fullnameInput.test(contact.fullname);
60
+ var normalizedInput = normalizeLowercase(input);
61
+ return normalizeLowercase(contact.fullname).includes(normalizedInput);
41
62
  };
42
63
 
43
64
  var contactOrGroupMatch = function contactOrGroupMatch(input, contact) {
@@ -127,5 +127,108 @@ describe('Suggestion matchers', function () {
127
127
  expect(matchers.fullnameMatch('snow', contact)).toBeTruthy();
128
128
  expect(matchers.fullnameMatch('MARG', contact)).toBeTruthy();
129
129
  });
130
+ it('should match accented fullname with unaccented input', function () {
131
+ expect(matchers.fullnameMatch('zoe', {
132
+ fullname: 'Zoé Dupont'
133
+ })).toBeTruthy();
134
+ expect(matchers.fullnameMatch('elodie', {
135
+ fullname: 'Élodie Martin'
136
+ })).toBeTruthy();
137
+ expect(matchers.fullnameMatch('celine', {
138
+ fullname: 'Céline'
139
+ })).toBeTruthy();
140
+ });
141
+ it('should match unaccented fullname with accented input', function () {
142
+ expect(matchers.fullnameMatch('zoé', {
143
+ fullname: 'Zoe Dupont'
144
+ })).toBeTruthy();
145
+ });
146
+ it('should treat regex metacharacters as plain text', function () {
147
+ expect(function () {
148
+ return matchers.fullnameMatch('(', {
149
+ fullname: 'Jon Snow'
150
+ });
151
+ }).not.toThrow();
152
+ expect(matchers.fullnameMatch('(', {
153
+ fullname: 'Jon Snow'
154
+ })).toBeFalsy();
155
+ expect(matchers.fullnameMatch('.', {
156
+ fullname: 'Jon Snow'
157
+ })).toBeFalsy();
158
+ expect(matchers.fullnameMatch('.', {
159
+ fullname: 'Dr. House'
160
+ })).toBeTruthy();
161
+ });
162
+ });
163
+ describe('groupNameMatch', function () {
164
+ it('should match accented group name with unaccented input', function () {
165
+ var group = {
166
+ _type: 'io.cozy.contacts.groups',
167
+ name: 'Équipe'
168
+ };
169
+ expect(matchers.groupNameMatch('equipe', group)).toBeTruthy();
170
+ });
171
+ });
172
+ describe('Vietnamese diacritics support', function () {
173
+ var vietnameseContacts = [{
174
+ fullname: 'Nguyễn Văn Minh',
175
+ email: 'minh.nguyen@email.com'
176
+ }, {
177
+ fullname: 'Trần Thị Hương',
178
+ email: 'huong.tran@email.com'
179
+ }, {
180
+ fullname: 'Lê Hoàng Nam',
181
+ email: 'nam.hoang@email.com'
182
+ }, {
183
+ fullname: 'Phạm Đức Anh',
184
+ email: 'anh.pham@email.com',
185
+ cozy: [{
186
+ url: 'https://anh.mycozy.cloud'
187
+ }]
188
+ }];
189
+ it('should match Vietnamese accented name with unaccented input', function () {
190
+ expect(matchers.fullnameMatch('nguyen', vietnameseContacts[0])).toBeTruthy();
191
+ expect(matchers.fullnameMatch('van', vietnameseContacts[0])).toBeTruthy();
192
+ expect(matchers.fullnameMatch('minh', vietnameseContacts[0])).toBeTruthy();
193
+ expect(matchers.fullnameMatch('tran', vietnameseContacts[1])).toBeTruthy();
194
+ expect(matchers.fullnameMatch('huong', vietnameseContacts[1])).toBeTruthy();
195
+ expect(matchers.fullnameMatch('le', vietnameseContacts[2])).toBeTruthy();
196
+ expect(matchers.fullnameMatch('nam', vietnameseContacts[2])).toBeTruthy();
197
+ expect(matchers.fullnameMatch('pham', vietnameseContacts[3])).toBeTruthy();
198
+ expect(matchers.fullnameMatch('anh', vietnameseContacts[3])).toBeTruthy();
199
+ });
200
+ it('should match unaccented input with Vietnamese accented name', function () {
201
+ expect(matchers.fullnameMatch('nguyễn', vietnameseContacts[0])).toBeTruthy();
202
+ expect(matchers.fullnameMatch('văn', vietnameseContacts[0])).toBeTruthy();
203
+ expect(matchers.fullnameMatch('trần', vietnameseContacts[1])).toBeTruthy();
204
+ expect(matchers.fullnameMatch('hương', vietnameseContacts[1])).toBeTruthy();
205
+ expect(matchers.fullnameMatch('lê', vietnameseContacts[2])).toBeTruthy();
206
+ expect(matchers.fullnameMatch('phạm', vietnameseContacts[3])).toBeTruthy();
207
+ });
208
+ it('should match Vietnamese email with partial unaccented input', function () {
209
+ expect(matchers.emailMatch('minh', vietnameseContacts[0])).toBeTruthy();
210
+ expect(matchers.emailMatch('nguyen', vietnameseContacts[0])).toBeTruthy();
211
+ expect(matchers.emailMatch('huong', vietnameseContacts[1])).toBeTruthy();
212
+ expect(matchers.emailMatch('tran', vietnameseContacts[1])).toBeTruthy();
213
+ });
214
+ it('should match Vietnamese cozy URL with unaccented input', function () {
215
+ var contact = {
216
+ fullname: 'Đặng Minh Đức',
217
+ cozy: [{
218
+ url: 'https://duc.mycozy.cloud'
219
+ }]
220
+ };
221
+ expect(matchers.cozyUrlMatch('duc', contact)).toBeTruthy();
222
+ expect(matchers.cozyUrlMatch('mycozy', contact)).toBeTruthy();
223
+ });
224
+ it('should match Vietnamese group names with diacritics', function () {
225
+ var vietGroup = {
226
+ _type: 'io.cozy.contacts.groups',
227
+ name: 'Nhóm Việt'
228
+ };
229
+ expect(matchers.groupNameMatch('nhom', vietGroup)).toBeTruthy();
230
+ expect(matchers.groupNameMatch('viet', vietGroup)).toBeTruthy();
231
+ expect(matchers.groupNameMatch('nhóm', vietGroup)).toBeTruthy();
232
+ });
130
233
  });
131
234
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-sharing",
3
- "version": "33.1.1",
3
+ "version": "33.2.0",
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": "a6e1d756c4f41496383878fd9ce59afbb443f5b7"
86
+ "gitHead": "aeaf0cb5bb862ce8908d78862ca79e8e749982f2"
87
87
  }
@@ -269,7 +269,7 @@ const FederatedFolderModalContent = ({
269
269
  <Button
270
270
  variant="primary"
271
271
  label={t('FederatedFolder.share')}
272
- disabled={isSending}
272
+ busy={isSending}
273
273
  onClick={onSend}
274
274
  />
275
275
  )}
@@ -1,4 +1,4 @@
1
- import { fireEvent, render, waitFor } from '@testing-library/react'
1
+ import { act, fireEvent, render, waitFor } from '@testing-library/react'
2
2
  import React from 'react'
3
3
 
4
4
  import { createMockClient } from 'cozy-client'
@@ -257,6 +257,36 @@ describe('FederatedFolderModal', () => {
257
257
  expect(mockOnClose).not.toHaveBeenCalled()
258
258
  })
259
259
  })
260
+
261
+ it('should show progress icon on Done button while share is in progress', async () => {
262
+ usePendingRecipients.mockReturnValue({
263
+ pendingRecipients: [pendingRecipient],
264
+ setPendingRecipients: jest.fn(),
265
+ selectedOption: 'readWrite',
266
+ setSelectedOption: jest.fn()
267
+ })
268
+ getOrCreateFromArray.mockResolvedValue([pendingRecipient])
269
+ let resolveShare
270
+ mockShare.mockReturnValueOnce(
271
+ new Promise(resolve => {
272
+ resolveShare = resolve
273
+ })
274
+ )
275
+ const { findByText, getByRole, queryByRole } = setup()
276
+
277
+ const sendButton = await findByText('Done')
278
+
279
+ fireEvent.click(sendButton)
280
+
281
+ await waitFor(() => {
282
+ expect(getByRole('button', { name: 'Done' })).toBeDisabled()
283
+ expect(queryByRole('progressbar')).not.toBeNull()
284
+ })
285
+
286
+ await act(async () => {
287
+ resolveShare({})
288
+ })
289
+ })
260
290
  })
261
291
 
262
292
  describe('onClose callback', () => {
@@ -1,37 +1,50 @@
1
- import escapeRegExp from 'lodash/escapeRegExp'
2
-
3
1
  import { Group } from 'cozy-doctypes'
4
2
 
3
+ let removeDiacritics
4
+ try {
5
+ new RegExp('\\p{Diacritic}', 'gu')
6
+ removeDiacritics = str => str.replace(/\p{Diacritic}/gu, '')
7
+ } catch {
8
+ removeDiacritics = str => str.replace(/[̀-ͯ]/g, '')
9
+ }
10
+
11
+ const normalize = str => removeDiacritics(str.normalize('NFD'))
12
+ const normalizeLowercase = str => normalize(str).toLowerCase()
13
+
5
14
  // TODO: sadly we have different versions of contacts' doctype to handle...
6
15
  // A migration tool on the stack side is needed here
7
16
  const emailMatch = (input, contact) => {
8
17
  if (!contact.email) return false
9
- const emailInput = new RegExp(escapeRegExp(input), 'i')
18
+ const normalizedInput = normalizeLowercase(input)
10
19
  if (Array.isArray(contact.email)) {
11
- return contact.email.some(email => emailInput.test(email.address))
20
+ return contact.email.some(email =>
21
+ normalizeLowercase(email.address).includes(normalizedInput)
22
+ )
12
23
  }
13
- return emailInput.test(contact.email)
24
+ return normalizeLowercase(contact.email).includes(normalizedInput)
14
25
  }
15
26
 
16
27
  const cozyUrlMatch = (input, contact) => {
17
28
  if (!contact.cozy && !contact.url) return false
18
- const urlInput = new RegExp(escapeRegExp(input), 'i')
29
+ const normalizedInput = normalizeLowercase(input)
19
30
  if (contact.cozy && Array.isArray(contact.cozy)) {
20
- return contact.cozy.some(cozy => urlInput.test(cozy.url))
31
+ return contact.cozy.some(cozy =>
32
+ normalizeLowercase(cozy.url).includes(normalizedInput)
33
+ )
21
34
  }
22
- return urlInput.test(contact.url)
35
+ return normalizeLowercase(contact.url).includes(normalizedInput)
23
36
  }
24
37
 
25
38
  const groupNameMatch = (input, contactOrGroup) => {
26
39
  if (contactOrGroup._type !== Group.doctype) return false
27
- const nameInput = new RegExp(escapeRegExp(input), 'i')
28
- return nameInput.test(contactOrGroup.name)
40
+ const normalizedInput = normalizeLowercase(input)
41
+ return normalizeLowercase(contactOrGroup.name).includes(normalizedInput)
29
42
  }
30
43
 
31
44
  const fullnameMatch = (input, contact) => {
32
45
  if (!contact.fullname) return false
33
- const fullnameInput = new RegExp(escapeRegExp(input), 'i')
34
- return fullnameInput.test(contact.fullname)
46
+ const normalizedInput = normalizeLowercase(input)
47
+ return normalizeLowercase(contact.fullname).includes(normalizedInput)
35
48
  }
36
49
 
37
50
  const contactOrGroupMatch = (input, contact) => {
@@ -146,5 +146,112 @@ describe('Suggestion matchers', () => {
146
146
  expect(matchers.fullnameMatch('snow', contact)).toBeTruthy()
147
147
  expect(matchers.fullnameMatch('MARG', contact)).toBeTruthy()
148
148
  })
149
+
150
+ it('should match accented fullname with unaccented input', () => {
151
+ expect(
152
+ matchers.fullnameMatch('zoe', { fullname: 'Zoé Dupont' })
153
+ ).toBeTruthy()
154
+ expect(
155
+ matchers.fullnameMatch('elodie', { fullname: 'Élodie Martin' })
156
+ ).toBeTruthy()
157
+ expect(
158
+ matchers.fullnameMatch('celine', { fullname: 'Céline' })
159
+ ).toBeTruthy()
160
+ })
161
+
162
+ it('should match unaccented fullname with accented input', () => {
163
+ expect(
164
+ matchers.fullnameMatch('zoé', { fullname: 'Zoe Dupont' })
165
+ ).toBeTruthy()
166
+ })
167
+
168
+ it('should treat regex metacharacters as plain text', () => {
169
+ expect(() =>
170
+ matchers.fullnameMatch('(', { fullname: 'Jon Snow' })
171
+ ).not.toThrow()
172
+ expect(matchers.fullnameMatch('(', { fullname: 'Jon Snow' })).toBeFalsy()
173
+ expect(matchers.fullnameMatch('.', { fullname: 'Jon Snow' })).toBeFalsy()
174
+ expect(
175
+ matchers.fullnameMatch('.', { fullname: 'Dr. House' })
176
+ ).toBeTruthy()
177
+ })
178
+ })
179
+
180
+ describe('groupNameMatch', () => {
181
+ it('should match accented group name with unaccented input', () => {
182
+ const group = {
183
+ _type: 'io.cozy.contacts.groups',
184
+ name: 'Équipe'
185
+ }
186
+ expect(matchers.groupNameMatch('equipe', group)).toBeTruthy()
187
+ })
188
+ })
189
+
190
+ describe('Vietnamese diacritics support', () => {
191
+ const vietnameseContacts = [
192
+ { fullname: 'Nguyễn Văn Minh', email: 'minh.nguyen@email.com' },
193
+ { fullname: 'Trần Thị Hương', email: 'huong.tran@email.com' },
194
+ { fullname: 'Lê Hoàng Nam', email: 'nam.hoang@email.com' },
195
+ {
196
+ fullname: 'Phạm Đức Anh',
197
+ email: 'anh.pham@email.com',
198
+ cozy: [{ url: 'https://anh.mycozy.cloud' }]
199
+ }
200
+ ]
201
+
202
+ it('should match Vietnamese accented name with unaccented input', () => {
203
+ expect(
204
+ matchers.fullnameMatch('nguyen', vietnameseContacts[0])
205
+ ).toBeTruthy()
206
+ expect(matchers.fullnameMatch('van', vietnameseContacts[0])).toBeTruthy()
207
+ expect(matchers.fullnameMatch('minh', vietnameseContacts[0])).toBeTruthy()
208
+ expect(matchers.fullnameMatch('tran', vietnameseContacts[1])).toBeTruthy()
209
+ expect(
210
+ matchers.fullnameMatch('huong', vietnameseContacts[1])
211
+ ).toBeTruthy()
212
+ expect(matchers.fullnameMatch('le', vietnameseContacts[2])).toBeTruthy()
213
+ expect(matchers.fullnameMatch('nam', vietnameseContacts[2])).toBeTruthy()
214
+ expect(matchers.fullnameMatch('pham', vietnameseContacts[3])).toBeTruthy()
215
+ expect(matchers.fullnameMatch('anh', vietnameseContacts[3])).toBeTruthy()
216
+ })
217
+
218
+ it('should match unaccented input with Vietnamese accented name', () => {
219
+ expect(
220
+ matchers.fullnameMatch('nguyễn', vietnameseContacts[0])
221
+ ).toBeTruthy()
222
+ expect(matchers.fullnameMatch('văn', vietnameseContacts[0])).toBeTruthy()
223
+ expect(matchers.fullnameMatch('trần', vietnameseContacts[1])).toBeTruthy()
224
+ expect(
225
+ matchers.fullnameMatch('hương', vietnameseContacts[1])
226
+ ).toBeTruthy()
227
+ expect(matchers.fullnameMatch('lê', vietnameseContacts[2])).toBeTruthy()
228
+ expect(matchers.fullnameMatch('phạm', vietnameseContacts[3])).toBeTruthy()
229
+ })
230
+
231
+ it('should match Vietnamese email with partial unaccented input', () => {
232
+ expect(matchers.emailMatch('minh', vietnameseContacts[0])).toBeTruthy()
233
+ expect(matchers.emailMatch('nguyen', vietnameseContacts[0])).toBeTruthy()
234
+ expect(matchers.emailMatch('huong', vietnameseContacts[1])).toBeTruthy()
235
+ expect(matchers.emailMatch('tran', vietnameseContacts[1])).toBeTruthy()
236
+ })
237
+
238
+ it('should match Vietnamese cozy URL with unaccented input', () => {
239
+ const contact = {
240
+ fullname: 'Đặng Minh Đức',
241
+ cozy: [{ url: 'https://duc.mycozy.cloud' }]
242
+ }
243
+ expect(matchers.cozyUrlMatch('duc', contact)).toBeTruthy()
244
+ expect(matchers.cozyUrlMatch('mycozy', contact)).toBeTruthy()
245
+ })
246
+
247
+ it('should match Vietnamese group names with diacritics', () => {
248
+ const vietGroup = {
249
+ _type: 'io.cozy.contacts.groups',
250
+ name: 'Nhóm Việt'
251
+ }
252
+ expect(matchers.groupNameMatch('nhom', vietGroup)).toBeTruthy()
253
+ expect(matchers.groupNameMatch('viet', vietGroup)).toBeTruthy()
254
+ expect(matchers.groupNameMatch('nhóm', vietGroup)).toBeTruthy()
255
+ })
149
256
  })
150
257
  })