cozy-sharing 33.3.1 → 33.3.3

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.3.3](https://github.com/cozy/cozy-libs/compare/cozy-sharing@33.3.2...cozy-sharing@33.3.3) (2026-06-11)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **cozy-sharing:** Scroll members list in federated share dialogs ([a3b8a64](https://github.com/cozy/cozy-libs/commit/a3b8a6460efa0b6228cdbf825eca7b950300b2f9))
11
+
12
+ ## [33.3.2](https://github.com/cozy/cozy-libs/compare/cozy-sharing@33.3.1...cozy-sharing@33.3.2) (2026-06-11)
13
+
14
+ ### Bug Fixes
15
+
16
+ - **cozy-sharing:** Use readonly endpoint in updateSharingMemberType ([f4914b6](https://github.com/cozy/cozy-libs/commit/f4914b61daaf5c20b17c4b9f1ddda511860a56ac))
17
+
6
18
  ## [33.3.1](https://github.com/cozy/cozy-libs/compare/cozy-sharing@33.3.0...cozy-sharing@33.3.1) (2026-06-10)
7
19
 
8
20
  ### Bug Fixes
@@ -471,7 +471,7 @@ export var SharingProvider = /*#__PURE__*/function (_Component) {
471
471
  */
472
472
  _defineProperty(_this2, "updateSharingMemberType", /*#__PURE__*/function () {
473
473
  var _ref12 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee10(sharingId, memberIndex, newType) {
474
- var sharing, member, matchedContacts, contact, readOnly;
474
+ var sharing, member, makeReadOnly;
475
475
  return _regeneratorRuntime.wrap(function _callee10$(_context10) {
476
476
  while (1) switch (_context10.prev = _context10.next) {
477
477
  case 0:
@@ -495,62 +495,49 @@ export var SharingProvider = /*#__PURE__*/function (_Component) {
495
495
  throw new Error('Member not found');
496
496
 
497
497
  case 6:
498
- _context10.next = 8;
499
- return _this2.props.client.collection('io.cozy.contacts').find({
500
- email: {
501
- $elemMatch: {
502
- address: member.email
503
- }
504
- },
505
- _id: {
506
- $gt: null
507
- }
508
- }, {
509
- indexedFields: ['_id']
510
- });
498
+ makeReadOnly = newType === 'one-way';
511
499
 
512
- case 8:
513
- matchedContacts = _context10.sent;
514
-
515
- if (matchedContacts.data.length) {
516
- _context10.next = 11;
500
+ if (!(newType !== 'one-way' && newType !== 'two-way')) {
501
+ _context10.next = 9;
517
502
  break;
518
503
  }
519
504
 
520
- throw new Error("Contact not found for member ".concat(member.email));
505
+ throw new Error('Unsupported sharing type: ' + newType);
521
506
 
522
- case 11:
523
- contact = matchedContacts.data[0]; // Revoke (API call only, no state dispatch to avoid UI flickering)
507
+ case 9:
508
+ _context10.prev = 9;
524
509
 
525
- _context10.next = 14;
526
- return _this2.sharingCol.revokeRecipient(sharing, memberIndex);
510
+ if (!makeReadOnly) {
511
+ _context10.next = 15;
512
+ break;
513
+ }
527
514
 
528
- case 14:
529
- // Re-add with the new permission type
530
- readOnly = newType === 'one-way';
531
- _context10.prev = 15;
532
- _context10.next = 18;
533
- return _this2.addRecipients({
534
- document: sharing,
535
- recipients: readOnly ? [] : [contact],
536
- readOnlyRecipients: readOnly ? [contact] : []
537
- });
515
+ _context10.next = 13;
516
+ return _this2.sharingCol.setReadOnly(sharing, memberIndex);
538
517
 
539
- case 18:
540
- _context10.next = 24;
518
+ case 13:
519
+ _context10.next = 17;
541
520
  break;
542
521
 
543
- case 20:
544
- _context10.prev = 20;
545
- _context10.t0 = _context10["catch"](15);
546
- log.error("Failed to re-add member ".concat(member.email, " after revocation. The member has been revoked but could not be re-added with the new permission type."), _context10.t0);
522
+ case 15:
523
+ _context10.next = 17;
524
+ return _this2.sharingCol.setReadWrite(sharing, memberIndex);
525
+
526
+ case 17:
527
+ _context10.next = 23;
528
+ break;
529
+
530
+ case 19:
531
+ _context10.prev = 19;
532
+ _context10.t0 = _context10["catch"](9);
533
+ log.error("Failed to change member ".concat(member.email, " permission type to ").concat(newType), _context10.t0);
547
534
  throw _context10.t0;
548
535
 
549
- case 24:
536
+ case 23:
550
537
  case "end":
551
538
  return _context10.stop();
552
539
  }
553
- }, _callee10, null, [[15, 20]]);
540
+ }, _callee10, null, [[9, 19]]);
554
541
  }));
555
542
 
556
543
  return function (_x16, _x17, _x18) {
@@ -412,12 +412,6 @@ describe('updateDocumentPermissions', function () {
412
412
  })));
413
413
  });
414
414
  describe('updateSharingMemberType', function () {
415
- var mockContact = {
416
- _id: 'contact-1',
417
- email: [{
418
- address: 'bob@bob.cozy'
419
- }]
420
- };
421
415
  var mockSharing = {
422
416
  id: 'sharing-123',
423
417
  type: 'io.cozy.sharings',
@@ -432,20 +426,14 @@ describe('updateSharingMemberType', function () {
432
426
  }
433
427
  };
434
428
  var instance;
435
- var mockContactsCollection;
436
429
  beforeEach(function () {
437
- mockContactsCollection = {
438
- find: jest.fn().mockResolvedValue({
439
- data: [mockContact]
440
- })
441
- };
442
430
  var mockClient = {
443
431
  getStackClient: function getStackClient() {
444
432
  return {
445
433
  uri: 'http://cozy.local'
446
434
  };
447
435
  },
448
- collection: jest.fn().mockReturnValue(mockContactsCollection)
436
+ collection: jest.fn().mockReturnValue({})
449
437
  };
450
438
  instance = new SharingProvider({
451
439
  client: mockClient
@@ -454,9 +442,10 @@ describe('updateSharingMemberType', function () {
454
442
  sharings: [mockSharing]
455
443
  });
456
444
  instance.sharingCol = {
457
- revokeRecipient: jest.fn().mockResolvedValue({})
445
+ setReadOnly: jest.fn().mockResolvedValue({}),
446
+ setReadWrite: jest.fn().mockResolvedValue({})
458
447
  };
459
- jest.spyOn(instance, 'addRecipients').mockResolvedValue({});
448
+ jest.spyOn(instance, 'setState');
460
449
  });
461
450
  it('should throw when sharing is not found', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee8() {
462
451
  return _regeneratorRuntime.wrap(function _callee8$(_context8) {
@@ -484,36 +473,33 @@ describe('updateSharingMemberType', function () {
484
473
  }
485
474
  }, _callee9);
486
475
  })));
487
- it('should throw when contact is not found', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee10() {
476
+ it('should call setReadOnly when switching to one-way', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee10() {
488
477
  return _regeneratorRuntime.wrap(function _callee10$(_context10) {
489
478
  while (1) switch (_context10.prev = _context10.next) {
490
479
  case 0:
491
- mockContactsCollection.find.mockResolvedValue({
492
- data: []
493
- });
494
- _context10.next = 3;
495
- return expect(instance.updateSharingMemberType('sharing-123', 1, 'one-way')).rejects.toThrow('Contact not found');
480
+ _context10.next = 2;
481
+ return instance.updateSharingMemberType('sharing-123', 1, 'one-way');
496
482
 
497
- case 3:
483
+ case 2:
484
+ expect(instance.sharingCol.setReadOnly).toHaveBeenCalledWith(mockSharing, 1);
485
+ expect(instance.sharingCol.setReadWrite).not.toHaveBeenCalled();
486
+
487
+ case 4:
498
488
  case "end":
499
489
  return _context10.stop();
500
490
  }
501
491
  }, _callee10);
502
492
  })));
503
- it('should revoke and re-add with readOnly when switching to one-way', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee11() {
493
+ it('should call setReadWrite when switching to two-way', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee11() {
504
494
  return _regeneratorRuntime.wrap(function _callee11$(_context11) {
505
495
  while (1) switch (_context11.prev = _context11.next) {
506
496
  case 0:
507
497
  _context11.next = 2;
508
- return instance.updateSharingMemberType('sharing-123', 1, 'one-way');
498
+ return instance.updateSharingMemberType('sharing-123', 1, 'two-way');
509
499
 
510
500
  case 2:
511
- expect(instance.sharingCol.revokeRecipient).toHaveBeenCalledWith(mockSharing, 1);
512
- expect(instance.addRecipients).toHaveBeenCalledWith({
513
- document: mockSharing,
514
- recipients: [],
515
- readOnlyRecipients: [mockContact]
516
- });
501
+ expect(instance.sharingCol.setReadWrite).toHaveBeenCalledWith(mockSharing, 1);
502
+ expect(instance.sharingCol.setReadOnly).not.toHaveBeenCalled();
517
503
 
518
504
  case 4:
519
505
  case "end":
@@ -521,34 +507,31 @@ describe('updateSharingMemberType', function () {
521
507
  }
522
508
  }, _callee11);
523
509
  })));
524
- it('should revoke and re-add as read-write when switching to two-way', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee12() {
510
+ it('should not dispatch a state update realtime refreshes the store', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee12() {
525
511
  return _regeneratorRuntime.wrap(function _callee12$(_context12) {
526
512
  while (1) switch (_context12.prev = _context12.next) {
527
513
  case 0:
528
514
  _context12.next = 2;
529
- return instance.updateSharingMemberType('sharing-123', 1, 'two-way');
515
+ return instance.updateSharingMemberType('sharing-123', 1, 'one-way');
530
516
 
531
517
  case 2:
532
- expect(instance.sharingCol.revokeRecipient).toHaveBeenCalledWith(mockSharing, 1);
533
- expect(instance.addRecipients).toHaveBeenCalledWith({
534
- document: mockSharing,
535
- recipients: [mockContact],
536
- readOnlyRecipients: []
537
- });
518
+ expect(instance.setState).not.toHaveBeenCalled();
538
519
 
539
- case 4:
520
+ case 3:
540
521
  case "end":
541
522
  return _context12.stop();
542
523
  }
543
524
  }, _callee12);
544
525
  })));
545
- it('should rethrow error if addRecipients fails after revocation', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee13() {
526
+ it('should not dispatch a state update for two-way — realtime refreshes the store', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee13() {
546
527
  return _regeneratorRuntime.wrap(function _callee13$(_context13) {
547
528
  while (1) switch (_context13.prev = _context13.next) {
548
529
  case 0:
549
- instance.addRecipients.mockRejectedValue(new Error('Network error'));
550
- _context13.next = 3;
551
- return expect(instance.updateSharingMemberType('sharing-123', 1, 'one-way')).rejects.toThrow('Network error');
530
+ _context13.next = 2;
531
+ return instance.updateSharingMemberType('sharing-123', 1, 'two-way');
532
+
533
+ case 2:
534
+ expect(instance.setState).not.toHaveBeenCalled();
552
535
 
553
536
  case 3:
554
537
  case "end":
@@ -556,6 +539,51 @@ describe('updateSharingMemberType', function () {
556
539
  }
557
540
  }, _callee13);
558
541
  })));
542
+ it('should rethrow error if setReadOnly fails', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee14() {
543
+ return _regeneratorRuntime.wrap(function _callee14$(_context14) {
544
+ while (1) switch (_context14.prev = _context14.next) {
545
+ case 0:
546
+ instance.sharingCol.setReadOnly.mockRejectedValue(new Error('Network error'));
547
+ _context14.next = 3;
548
+ return expect(instance.updateSharingMemberType('sharing-123', 1, 'one-way')).rejects.toThrow('Network error');
549
+
550
+ case 3:
551
+ case "end":
552
+ return _context14.stop();
553
+ }
554
+ }, _callee14);
555
+ })));
556
+ it('should rethrow error if setReadWrite fails', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee15() {
557
+ return _regeneratorRuntime.wrap(function _callee15$(_context15) {
558
+ while (1) switch (_context15.prev = _context15.next) {
559
+ case 0:
560
+ instance.sharingCol.setReadWrite.mockRejectedValue(new Error('Network error'));
561
+ _context15.next = 3;
562
+ return expect(instance.updateSharingMemberType('sharing-123', 1, 'two-way')).rejects.toThrow('Network error');
563
+
564
+ case 3:
565
+ case "end":
566
+ return _context15.stop();
567
+ }
568
+ }, _callee15);
569
+ })));
570
+ it('should not dispatch state update if API call fails', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee16() {
571
+ return _regeneratorRuntime.wrap(function _callee16$(_context16) {
572
+ while (1) switch (_context16.prev = _context16.next) {
573
+ case 0:
574
+ instance.sharingCol.setReadOnly.mockRejectedValue(new Error('Network error'));
575
+ _context16.next = 3;
576
+ return expect(instance.updateSharingMemberType('sharing-123', 1, 'one-way')).rejects.toThrow('Network error');
577
+
578
+ case 3:
579
+ expect(instance.setState).not.toHaveBeenCalled();
580
+
581
+ case 4:
582
+ case "end":
583
+ return _context16.stop();
584
+ }
585
+ }, _callee16);
586
+ })));
559
587
  }); // TODO Convert with react-testing-library
560
588
  // describe('hasWriteAccess', () => {
561
589
  // it('tells if a doc is writtable', () => {
@@ -26,6 +26,9 @@ var styles = {
26
26
  "coz-form-controls--full": "share__coz-form-controls--full___1oQXF",
27
27
  "coz-form-controls--dispatch": "share__coz-form-controls--dispatch___1zIxt",
28
28
  "error": "share__error___pwqPO",
29
+ "share-dialog-scrollable-content": "share__share-dialog-scrollable-content___2Xyoq",
30
+ "share-dialog-scrollable-body": "share__share-dialog-scrollable-body___2O5UT",
31
+ "share-dialog-members": "share__share-dialog-members___CNHKJ",
29
32
  "share-byemail-onlybylink": "share__share-byemail-onlybylink___29W-t",
30
33
  "aligned-dropdown-button": "share__aligned-dropdown-button___3crgG",
31
34
  "share-details-created": "share__share-details-created___z3nvO",
@@ -221,10 +224,12 @@ var FederatedFolderModalContent = function FederatedFolderModalContent(_ref) {
221
224
  },
222
225
  componentsProps: {
223
226
  dialogContent: {
224
- className: 'u-ov-visible'
227
+ className: styles['share-dialog-scrollable-content']
225
228
  }
226
229
  },
227
- content: /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
230
+ content: /*#__PURE__*/React.createElement("div", {
231
+ className: styles['share-dialog-scrollable-body']
232
+ }, /*#__PURE__*/React.createElement("div", {
228
233
  className: "u-ph-2"
229
234
  }, /*#__PURE__*/React.createElement(AntivirusAlert, {
230
235
  document: isSending ? frozenDoc : existingDocument
@@ -250,7 +255,7 @@ var FederatedFolderModalContent = function FederatedFolderModalContent(_ref) {
250
255
  recipients: isSending ? frozenRecipients : existingRecipients,
251
256
  document: isSending ? frozenDoc : existingDocument,
252
257
  documentType: "Files",
253
- className: "u-w-100",
258
+ className: styles['share-dialog-members'],
254
259
  onRevoke: revoke,
255
260
  link: displayedLink
256
261
  })),
@@ -14,6 +14,9 @@ var modalStyles = {
14
14
  "coz-form-controls--full": "share__coz-form-controls--full___1oQXF",
15
15
  "coz-form-controls--dispatch": "share__coz-form-controls--dispatch___1zIxt",
16
16
  "error": "share__error___pwqPO",
17
+ "share-dialog-scrollable-content": "share__share-dialog-scrollable-content___2Xyoq",
18
+ "share-dialog-scrollable-body": "share__share-dialog-scrollable-body___2O5UT",
19
+ "share-dialog-members": "share__share-dialog-members___CNHKJ",
17
20
  "share-byemail-onlybylink": "share__share-byemail-onlybylink___29W-t",
18
21
  "aligned-dropdown-button": "share__aligned-dropdown-button___3crgG",
19
22
  "share-details-created": "share__share-details-created___z3nvO",
@@ -19,6 +19,9 @@ var styles = {
19
19
  "coz-form-controls--full": "share__coz-form-controls--full___1oQXF",
20
20
  "coz-form-controls--dispatch": "share__coz-form-controls--dispatch___1zIxt",
21
21
  "error": "share__error___pwqPO",
22
+ "share-dialog-scrollable-content": "share__share-dialog-scrollable-content___2Xyoq",
23
+ "share-dialog-scrollable-body": "share__share-dialog-scrollable-body___2O5UT",
24
+ "share-dialog-members": "share__share-dialog-members___CNHKJ",
22
25
  "share-byemail-onlybylink": "share__share-byemail-onlybylink___29W-t",
23
26
  "aligned-dropdown-button": "share__aligned-dropdown-button___3crgG",
24
27
  "share-details-created": "share__share-details-created___z3nvO",
@@ -37,6 +37,9 @@ var styles = {
37
37
  "coz-form-controls--full": "share__coz-form-controls--full___1oQXF",
38
38
  "coz-form-controls--dispatch": "share__coz-form-controls--dispatch___1zIxt",
39
39
  "error": "share__error___pwqPO",
40
+ "share-dialog-scrollable-content": "share__share-dialog-scrollable-content___2Xyoq",
41
+ "share-dialog-scrollable-body": "share__share-dialog-scrollable-body___2O5UT",
42
+ "share-dialog-members": "share__share-dialog-members___CNHKJ",
40
43
  "share-byemail-onlybylink": "share__share-byemail-onlybylink___29W-t",
41
44
  "aligned-dropdown-button": "share__aligned-dropdown-button___3crgG",
42
45
  "share-details-created": "share__share-details-created___z3nvO",
@@ -18,6 +18,9 @@ var styles = {
18
18
  "coz-form-controls--full": "share__coz-form-controls--full___1oQXF",
19
19
  "coz-form-controls--dispatch": "share__coz-form-controls--dispatch___1zIxt",
20
20
  "error": "share__error___pwqPO",
21
+ "share-dialog-scrollable-content": "share__share-dialog-scrollable-content___2Xyoq",
22
+ "share-dialog-scrollable-body": "share__share-dialog-scrollable-body___2O5UT",
23
+ "share-dialog-members": "share__share-dialog-members___CNHKJ",
21
24
  "share-byemail-onlybylink": "share__share-byemail-onlybylink___29W-t",
22
25
  "aligned-dropdown-button": "share__aligned-dropdown-button___3crgG",
23
26
  "share-details-created": "share__share-details-created___z3nvO",
@@ -21,6 +21,27 @@ import { getOrCreateFromArray } from '../../helpers/contacts';
21
21
  import withLocales from '../../hoc/withLocales';
22
22
  import { usePendingRecipients } from '../../hooks/usePendingRecipients';
23
23
  import { useSharingContext } from '../../hooks/useSharingContext';
24
+ var styles = {
25
+ "share-modal-content": "share__share-modal-content___1iqEq",
26
+ "coz-form": "share__coz-form___1ICST",
27
+ "coz-form-group": "share__coz-form-group___VsQp7",
28
+ "coz-form-desc": "share__coz-form-desc___2mYN4",
29
+ "coz-form-label": "share__coz-form-label___bogBw",
30
+ "coz-form-label--error": "share__coz-form-label--error___3YPkf",
31
+ "coz-form-errors": "share__coz-form-errors___2xYRz",
32
+ "coz-form-controls": "share__coz-form-controls___H8kGc",
33
+ "coz-form-controls--full": "share__coz-form-controls--full___1oQXF",
34
+ "coz-form-controls--dispatch": "share__coz-form-controls--dispatch___1zIxt",
35
+ "error": "share__error___pwqPO",
36
+ "share-dialog-scrollable-content": "share__share-dialog-scrollable-content___2Xyoq",
37
+ "share-dialog-scrollable-body": "share__share-dialog-scrollable-body___2O5UT",
38
+ "share-dialog-members": "share__share-dialog-members___CNHKJ",
39
+ "share-byemail-onlybylink": "share__share-byemail-onlybylink___29W-t",
40
+ "aligned-dropdown-button": "share__aligned-dropdown-button___3crgG",
41
+ "share-details-created": "share__share-details-created___z3nvO",
42
+ "share-details-perm": "share__share-details-perm___3HjxK",
43
+ "share-details-perm-desc": "share__share-details-perm-desc___3HyHu"
44
+ };
24
45
  import { default as DumbShareByEmail } from '../ShareByEmail';
25
46
  import WhoHasAccess from '../WhoHasAccess';
26
47
  export var EditSharedDriveModal = withLocales(function (_ref) {
@@ -109,10 +130,12 @@ export var EditSharedDriveModal = withLocales(function (_ref) {
109
130
  },
110
131
  componentsProps: {
111
132
  dialogContent: {
112
- className: 'u-ov-visible'
133
+ className: styles['share-dialog-scrollable-content']
113
134
  }
114
135
  },
115
- content: /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
136
+ content: /*#__PURE__*/React.createElement("div", {
137
+ className: styles['share-dialog-scrollable-body']
138
+ }, /*#__PURE__*/React.createElement("div", {
116
139
  className: "u-ph-2 u-pt-2"
117
140
  }, /*#__PURE__*/React.createElement(SharedDriveHeader, {
118
141
  title: t('SharedDrive.editSharedDriveModal.title')
@@ -143,7 +166,7 @@ export var EditSharedDriveModal = withLocales(function (_ref) {
143
166
  recipients: existingRecipients,
144
167
  document: document,
145
168
  documentType: "Files",
146
- className: "u-w-100",
169
+ className: styles['share-dialog-members'],
147
170
  onRevoke: revoke
148
171
  }), /*#__PURE__*/React.createElement("div", {
149
172
  className: "u-flex u-ph-2 u-pv-1"
@@ -2,6 +2,39 @@
2
2
  @require 'components/popover.styl'
3
3
  @require 'settings/breakpoints.styl'
4
4
 
5
+ // The federated share dialogs keep overflow visible so the contact
6
+ // suggestions dropdown can pop over the dialog, which disables the dialog's
7
+ // native scrolling. These classes pass the height constraint down through
8
+ // the flex chain so only the members list scrolls.
9
+ .share-dialog-scrollable-content
10
+ display flex
11
+ flex-direction column
12
+ min-height 0
13
+ // keep the contact suggestions dropdown visible over the dialog;
14
+ // !important to win over MUI's DialogContent overflow-y auto
15
+ overflow visible !important // @stylint ignore
16
+
17
+ // cozy-ui dialogs expose no componentsProps hook for their
18
+ // .dialogContentInner wrapper, but the classname is a stable contract:
19
+ // cozy-ui's own MUI theme overrides style it by name
20
+ > :global(.dialogContentInner) // @stylint ignore Naming imposed by cozy-ui
21
+ display flex
22
+ flex-direction column
23
+ flex 1 1 auto
24
+ min-height 0
25
+
26
+ .share-dialog-scrollable-body
27
+ display flex
28
+ flex-direction column
29
+ flex 1 1 auto
30
+ min-height 0
31
+
32
+ .share-dialog-members
33
+ flex 1 1 auto
34
+ min-height 0
35
+ overflow-y auto
36
+ width 100%
37
+
5
38
  .share-byemail-onlybylink
6
39
  background var(--defaultBackgroundColor)
7
40
  padding 1rem 2rem
@@ -277,6 +277,30 @@
277
277
  }
278
278
  @media (max-width: 48rem) {
279
279
  }
280
+ .share__share-dialog-scrollable-content___2Xyoq {
281
+ display: flex;
282
+ flex-direction: column;
283
+ min-height: 0;
284
+ overflow: visible !important;
285
+ }
286
+ .share__share-dialog-scrollable-content___2Xyoq > .dialogContentInner {
287
+ display: flex;
288
+ flex-direction: column;
289
+ flex: 1 1 auto;
290
+ min-height: 0;
291
+ }
292
+ .share__share-dialog-scrollable-body___2O5UT {
293
+ display: flex;
294
+ flex-direction: column;
295
+ flex: 1 1 auto;
296
+ min-height: 0;
297
+ }
298
+ .share__share-dialog-members___CNHKJ {
299
+ flex: 1 1 auto;
300
+ min-height: 0;
301
+ overflow-y: auto;
302
+ width: 100%;
303
+ }
280
304
  .share__share-byemail-onlybylink___29W-t {
281
305
  background: var(--defaultBackgroundColor);
282
306
  padding: 1rem 2rem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-sharing",
3
- "version": "33.3.1",
3
+ "version": "33.3.3",
4
4
  "description": "Provides sharing login for React applications.",
5
5
  "main": "dist/index.js",
6
6
  "author": "Cozy",
@@ -53,7 +53,7 @@
53
53
  "babel-jest": "30.3.0",
54
54
  "babel-plugin-css-modules-transform": "1.6.2",
55
55
  "babel-plugin-inline-react-svg": "1.1.2",
56
- "cozy-client": "^60.24.0",
56
+ "cozy-client": "^60.27.0",
57
57
  "cozy-device-helper": "^4.0.0",
58
58
  "cozy-intent": "^2.31.1",
59
59
  "cozy-minilog": "^3.10.1",
@@ -68,7 +68,7 @@
68
68
  "twake-i18n": "^0.4.1"
69
69
  },
70
70
  "peerDependencies": {
71
- "cozy-client": ">=60.24.0",
71
+ "cozy-client": ">=60.27.0",
72
72
  "cozy-device-helper": ">=4.0.0",
73
73
  "cozy-flags": ">=4.8.1",
74
74
  "cozy-intent": ">=2.29.1",
@@ -83,5 +83,5 @@
83
83
  "sideEffects": [
84
84
  "*.css"
85
85
  ],
86
- "gitHead": "80c72e3af992596597b9319ee436ec8d238c9967"
86
+ "gitHead": "2f0b77b413399e9bc3daa40a0829a08891d677c1"
87
87
  }
@@ -143,7 +143,7 @@ export class SharingProvider extends Component {
143
143
  this.fetchAllSharings()
144
144
  if (!client.plugins.realtime) {
145
145
  // eslint-disable-next-line
146
- console.warn(
146
+ console.warn(
147
147
  `You should register the realtime plugin to your CozyClient instance see https://docs.cozy.io/en/cozy-realtime/#example`
148
148
  )
149
149
  } else {
@@ -353,36 +353,19 @@ export class SharingProvider extends Component {
353
353
  const member = sharing.attributes.members[memberIndex]
354
354
  if (!member) throw new Error('Member not found')
355
355
 
356
- // Find the contact by email (same pattern as helpers/contacts.js)
357
- const matchedContacts = await this.props.client
358
- .collection('io.cozy.contacts')
359
- .find(
360
- {
361
- email: { $elemMatch: { address: member.email } },
362
- _id: { $gt: null }
363
- },
364
- { indexedFields: ['_id'] }
365
- )
366
-
367
- if (!matchedContacts.data.length) {
368
- throw new Error(`Contact not found for member ${member.email}`)
356
+ const makeReadOnly = newType === 'one-way'
357
+ if (newType !== 'one-way' && newType !== 'two-way') {
358
+ throw new Error('Unsupported sharing type: ' + newType)
369
359
  }
370
- const contact = matchedContacts.data[0]
371
-
372
- // Revoke (API call only, no state dispatch to avoid UI flickering)
373
- await this.sharingCol.revokeRecipient(sharing, memberIndex)
374
-
375
- // Re-add with the new permission type
376
- const readOnly = newType === 'one-way'
377
360
  try {
378
- await this.addRecipients({
379
- document: sharing,
380
- recipients: readOnly ? [] : [contact],
381
- readOnlyRecipients: readOnly ? [contact] : []
382
- })
361
+ if (makeReadOnly) {
362
+ await this.sharingCol.setReadOnly(sharing, memberIndex)
363
+ } else {
364
+ await this.sharingCol.setReadWrite(sharing, memberIndex)
365
+ }
383
366
  } catch (error) {
384
367
  log.error(
385
- `Failed to re-add member ${member.email} after revocation. The member has been revoked but could not be re-added with the new permission type.`,
368
+ `Failed to change member ${member.email} permission type to ${newType}`,
386
369
  error
387
370
  )
388
371
  throw error
@@ -292,7 +292,6 @@ describe('updateDocumentPermissions', () => {
292
292
  })
293
293
 
294
294
  describe('updateSharingMemberType', () => {
295
- const mockContact = { _id: 'contact-1', email: [{ address: 'bob@bob.cozy' }] }
296
295
  const mockSharing = {
297
296
  id: 'sharing-123',
298
297
  type: 'io.cozy.sharings',
@@ -305,16 +304,11 @@ describe('updateSharingMemberType', () => {
305
304
  }
306
305
 
307
306
  let instance
308
- let mockContactsCollection
309
307
 
310
308
  beforeEach(() => {
311
- mockContactsCollection = {
312
- find: jest.fn().mockResolvedValue({ data: [mockContact] })
313
- }
314
-
315
309
  const mockClient = {
316
310
  getStackClient: () => ({ uri: 'http://cozy.local' }),
317
- collection: jest.fn().mockReturnValue(mockContactsCollection)
311
+ collection: jest.fn().mockReturnValue({})
318
312
  }
319
313
 
320
314
  instance = new SharingProvider({ client: mockClient })
@@ -323,9 +317,10 @@ describe('updateSharingMemberType', () => {
323
317
  sharings: [mockSharing]
324
318
  }
325
319
  instance.sharingCol = {
326
- revokeRecipient: jest.fn().mockResolvedValue({})
320
+ setReadOnly: jest.fn().mockResolvedValue({}),
321
+ setReadWrite: jest.fn().mockResolvedValue({})
327
322
  }
328
- jest.spyOn(instance, 'addRecipients').mockResolvedValue({})
323
+ jest.spyOn(instance, 'setState')
329
324
  })
330
325
 
331
326
  it('should throw when sharing is not found', async () => {
@@ -340,48 +335,65 @@ describe('updateSharingMemberType', () => {
340
335
  ).rejects.toThrow('Member not found')
341
336
  })
342
337
 
343
- it('should throw when contact is not found', async () => {
344
- mockContactsCollection.find.mockResolvedValue({ data: [] })
338
+ it('should call setReadOnly when switching to one-way', async () => {
339
+ await instance.updateSharingMemberType('sharing-123', 1, 'one-way')
345
340
 
346
- await expect(
347
- instance.updateSharingMemberType('sharing-123', 1, 'one-way')
348
- ).rejects.toThrow('Contact not found')
341
+ expect(instance.sharingCol.setReadOnly).toHaveBeenCalledWith(mockSharing, 1)
342
+ expect(instance.sharingCol.setReadWrite).not.toHaveBeenCalled()
349
343
  })
350
344
 
351
- it('should revoke and re-add with readOnly when switching to one-way', async () => {
352
- await instance.updateSharingMemberType('sharing-123', 1, 'one-way')
345
+ it('should call setReadWrite when switching to two-way', async () => {
346
+ await instance.updateSharingMemberType('sharing-123', 1, 'two-way')
353
347
 
354
- expect(instance.sharingCol.revokeRecipient).toHaveBeenCalledWith(
348
+ expect(instance.sharingCol.setReadWrite).toHaveBeenCalledWith(
355
349
  mockSharing,
356
350
  1
357
351
  )
358
- expect(instance.addRecipients).toHaveBeenCalledWith({
359
- document: mockSharing,
360
- recipients: [],
361
- readOnlyRecipients: [mockContact]
362
- })
352
+ expect(instance.sharingCol.setReadOnly).not.toHaveBeenCalled()
363
353
  })
364
354
 
365
- it('should revoke and re-add as read-write when switching to two-way', async () => {
355
+ it('should not dispatch a state update realtime refreshes the store', async () => {
356
+ await instance.updateSharingMemberType('sharing-123', 1, 'one-way')
357
+
358
+ expect(instance.setState).not.toHaveBeenCalled()
359
+ })
360
+
361
+ it('should not dispatch a state update for two-way — realtime refreshes the store', async () => {
366
362
  await instance.updateSharingMemberType('sharing-123', 1, 'two-way')
367
363
 
368
- expect(instance.sharingCol.revokeRecipient).toHaveBeenCalledWith(
369
- mockSharing,
370
- 1
364
+ expect(instance.setState).not.toHaveBeenCalled()
365
+ })
366
+
367
+ it('should rethrow error if setReadOnly fails', async () => {
368
+ instance.sharingCol.setReadOnly.mockRejectedValue(
369
+ new Error('Network error')
371
370
  )
372
- expect(instance.addRecipients).toHaveBeenCalledWith({
373
- document: mockSharing,
374
- recipients: [mockContact],
375
- readOnlyRecipients: []
376
- })
371
+
372
+ await expect(
373
+ instance.updateSharingMemberType('sharing-123', 1, 'one-way')
374
+ ).rejects.toThrow('Network error')
375
+ })
376
+
377
+ it('should rethrow error if setReadWrite fails', async () => {
378
+ instance.sharingCol.setReadWrite.mockRejectedValue(
379
+ new Error('Network error')
380
+ )
381
+
382
+ await expect(
383
+ instance.updateSharingMemberType('sharing-123', 1, 'two-way')
384
+ ).rejects.toThrow('Network error')
377
385
  })
378
386
 
379
- it('should rethrow error if addRecipients fails after revocation', async () => {
380
- instance.addRecipients.mockRejectedValue(new Error('Network error'))
387
+ it('should not dispatch state update if API call fails', async () => {
388
+ instance.sharingCol.setReadOnly.mockRejectedValue(
389
+ new Error('Network error')
390
+ )
381
391
 
382
392
  await expect(
383
393
  instance.updateSharingMemberType('sharing-123', 1, 'one-way')
384
394
  ).rejects.toThrow('Network error')
395
+
396
+ expect(instance.setState).not.toHaveBeenCalled()
385
397
  })
386
398
  })
387
399
 
@@ -186,10 +186,12 @@ const FederatedFolderModalContent = ({
186
186
  title={modalTitle}
187
187
  classes={{ paper: 'u-ov-visible' }}
188
188
  componentsProps={{
189
- dialogContent: { className: 'u-ov-visible' }
189
+ dialogContent: {
190
+ className: styles['share-dialog-scrollable-content']
191
+ }
190
192
  }}
191
193
  content={
192
- <div>
194
+ <div className={styles['share-dialog-scrollable-body']}>
193
195
  <div className="u-ph-2">
194
196
  <AntivirusAlert
195
197
  document={isSending ? frozenDoc : existingDocument}
@@ -240,7 +242,7 @@ const FederatedFolderModalContent = ({
240
242
  recipients={isSending ? frozenRecipients : existingRecipients}
241
243
  document={isSending ? frozenDoc : existingDocument}
242
244
  documentType="Files"
243
- className="u-w-100"
245
+ className={styles['share-dialog-members']}
244
246
  onRevoke={revoke}
245
247
  link={displayedLink}
246
248
  />
@@ -14,6 +14,7 @@ import { getOrCreateFromArray } from '../../helpers/contacts'
14
14
  import withLocales from '../../hoc/withLocales'
15
15
  import { usePendingRecipients } from '../../hooks/usePendingRecipients'
16
16
  import { useSharingContext } from '../../hooks/useSharingContext'
17
+ import styles from '../../styles/share.styl'
17
18
  import { default as DumbShareByEmail } from '../ShareByEmail'
18
19
  import WhoHasAccess from '../WhoHasAccess'
19
20
 
@@ -68,10 +69,12 @@ export const EditSharedDriveModal = withLocales(({ onClose, document }) => {
68
69
  onClose={onClose}
69
70
  classes={{ paper: 'u-ov-visible' }}
70
71
  componentsProps={{
71
- dialogContent: { className: 'u-ov-visible' }
72
+ dialogContent: {
73
+ className: styles['share-dialog-scrollable-content']
74
+ }
72
75
  }}
73
76
  content={
74
- <div>
77
+ <div className={styles['share-dialog-scrollable-body']}>
75
78
  <div className="u-ph-2 u-pt-2">
76
79
  <SharedDriveHeader
77
80
  title={t('SharedDrive.editSharedDriveModal.title')}
@@ -104,7 +107,7 @@ export const EditSharedDriveModal = withLocales(({ onClose, document }) => {
104
107
  recipients={existingRecipients}
105
108
  document={document}
106
109
  documentType="Files"
107
- className="u-w-100"
110
+ className={styles['share-dialog-members']}
108
111
  onRevoke={revoke}
109
112
  />
110
113
  <div className="u-flex u-ph-2 u-pv-1">
@@ -2,6 +2,39 @@
2
2
  @require 'components/popover.styl'
3
3
  @require 'settings/breakpoints.styl'
4
4
 
5
+ // The federated share dialogs keep overflow visible so the contact
6
+ // suggestions dropdown can pop over the dialog, which disables the dialog's
7
+ // native scrolling. These classes pass the height constraint down through
8
+ // the flex chain so only the members list scrolls.
9
+ .share-dialog-scrollable-content
10
+ display flex
11
+ flex-direction column
12
+ min-height 0
13
+ // keep the contact suggestions dropdown visible over the dialog;
14
+ // !important to win over MUI's DialogContent overflow-y auto
15
+ overflow visible !important // @stylint ignore
16
+
17
+ // cozy-ui dialogs expose no componentsProps hook for their
18
+ // .dialogContentInner wrapper, but the classname is a stable contract:
19
+ // cozy-ui's own MUI theme overrides style it by name
20
+ > :global(.dialogContentInner) // @stylint ignore Naming imposed by cozy-ui
21
+ display flex
22
+ flex-direction column
23
+ flex 1 1 auto
24
+ min-height 0
25
+
26
+ .share-dialog-scrollable-body
27
+ display flex
28
+ flex-direction column
29
+ flex 1 1 auto
30
+ min-height 0
31
+
32
+ .share-dialog-members
33
+ flex 1 1 auto
34
+ min-height 0
35
+ overflow-y auto
36
+ width 100%
37
+
5
38
  .share-byemail-onlybylink
6
39
  background var(--defaultBackgroundColor)
7
40
  padding 1rem 2rem