cozy-sharing 28.6.0 → 28.8.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
+ # [28.8.0](https://github.com/cozy/cozy-libs/compare/cozy-sharing@28.7.0...cozy-sharing@28.8.0) (2026-03-12)
7
+
8
+ ### Features
9
+
10
+ - **cozy-sharing:** Use FederatedFolderModal on existing sharings ([541de13](https://github.com/cozy/cozy-libs/commit/541de1310631170b5567694ff1366bc997fb3ea3))
11
+
12
+ # [28.7.0](https://github.com/cozy/cozy-libs/compare/cozy-sharing@28.6.0...cozy-sharing@28.7.0) (2026-03-12)
13
+
14
+ ### Features
15
+
16
+ - Allow to change link editing rights directly from the share modal ([57c3e61](https://github.com/cozy/cozy-libs/commit/57c3e6144649d3102c37961b4953219bdfe553a3))
17
+ - Display the Share restriction modal on click on the sharing link ([a875c36](https://github.com/cozy/cozy-libs/commit/a875c36e7b6ea592b25cbc8386a305b1c1dac516))
18
+
6
19
  # [28.6.0](https://github.com/cozy/cozy-libs/compare/cozy-sharing@28.5.0...cozy-sharing@28.6.0) (2026-03-11)
7
20
 
8
21
  ### Features
@@ -755,9 +755,12 @@ export var SharingProvider = /*#__PURE__*/function (_Component) {
755
755
 
756
756
  case 11:
757
757
  resp = _context14.t0;
758
+
759
+ _this2.dispatch(updateSharingLink(resp.data));
760
+
758
761
  return _context14.abrupt("return", resp);
759
762
 
760
- case 13:
763
+ case 14:
761
764
  case "end":
762
765
  return _context14.stop();
763
766
  }
@@ -1,3 +1,4 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
1
2
  import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
2
3
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
4
  import _regeneratorRuntime from "@babel/runtime/regenerator";
@@ -24,7 +25,9 @@ export var FederatedFolderModal = withLocales(function (_ref) {
24
25
  getFederatedShareLink = _useSharingContext.getFederatedShareLink,
25
26
  getDocumentPermissions = _useSharingContext.getDocumentPermissions,
26
27
  getOwner = _useSharingContext.getOwner,
27
- getSharingById = _useSharingContext.getSharingById;
28
+ getSharingById = _useSharingContext.getSharingById,
29
+ getRecipients = _useSharingContext.getRecipients,
30
+ revoke = _useSharingContext.revoke;
28
31
 
29
32
  var _useAlert = useAlert(),
30
33
  showAlert = _useAlert.showAlert;
@@ -162,21 +165,50 @@ export var FederatedFolderModal = withLocales(function (_ref) {
162
165
  }
163
166
  };
164
167
 
165
- var onRevoke = function onRevoke(index) {
166
- var _id = index.split(RECIPIENT_INDEX_PREFIX)[1];
167
- setFederatedRecipients(function (prev) {
168
- return {
169
- recipients: prev.recipients.filter(function (r) {
170
- return r._id !== _id;
171
- }),
172
- readOnlyRecipients: prev.readOnlyRecipients.filter(function (r) {
173
- return r._id !== _id;
174
- })
175
- };
176
- });
177
- };
168
+ var onRevoke = /*#__PURE__*/function () {
169
+ var _ref4 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(documentOrIndex, sharingId, memberIndex) {
170
+ var _id;
171
+
172
+ return _regeneratorRuntime.wrap(function _callee3$(_context3) {
173
+ while (1) switch (_context3.prev = _context3.next) {
174
+ case 0:
175
+ if (!(typeof documentOrIndex === 'string' && documentOrIndex.startsWith(RECIPIENT_INDEX_PREFIX))) {
176
+ _context3.next = 5;
177
+ break;
178
+ }
179
+
180
+ _id = documentOrIndex.split(RECIPIENT_INDEX_PREFIX)[1];
181
+ setFederatedRecipients(function (prev) {
182
+ return {
183
+ recipients: prev.recipients.filter(function (r) {
184
+ return r._id !== _id;
185
+ }),
186
+ readOnlyRecipients: prev.readOnlyRecipients.filter(function (r) {
187
+ return r._id !== _id;
188
+ })
189
+ };
190
+ });
191
+ _context3.next = 7;
192
+ break;
193
+
194
+ case 5:
195
+ _context3.next = 7;
196
+ return revoke(documentOrIndex, sharingId, memberIndex);
197
+
198
+ case 7:
199
+ case "end":
200
+ return _context3.stop();
201
+ }
202
+ }, _callee3);
203
+ }));
204
+
205
+ return function onRevoke(_x, _x2, _x3) {
206
+ return _ref4.apply(this, arguments);
207
+ };
208
+ }();
178
209
 
179
- var recipients = formatRecipients(federatedRecipients);
210
+ var existingRecipients = existingDocument ? getRecipients(existingDocument._id) : [];
211
+ var recipients = [].concat(_toConsumableArray(existingRecipients), _toConsumableArray(formatRecipients(federatedRecipients)));
180
212
  var modalTitle = t('FederatedFolder.shareTitle', {
181
213
  name: folderName
182
214
  });
@@ -189,7 +221,7 @@ export var FederatedFolderModal = withLocales(function (_ref) {
189
221
  },
190
222
  recipients: recipients,
191
223
  readOnlyRecipients: federatedRecipients.readOnlyRecipients,
192
- currentRecipients: [],
224
+ currentRecipients: existingRecipients,
193
225
  onRevoke: onRevoke,
194
226
  onSetType: onSetType,
195
227
  onSend: onSend,
@@ -7,7 +7,9 @@ import { createMockClient } from 'cozy-client';
7
7
  import { FederatedFolderModal } from './FederatedFolderModal';
8
8
  import AppLike from '../SharingBanner/test/AppLike';
9
9
  var mockShare = jest.fn();
10
+ var mockRevoke = jest.fn();
10
11
  var mockGetSharingLink = jest.fn();
12
+ var mockGetRecipients = jest.fn().mockReturnValue([]);
11
13
  var mockShowAlert = jest.fn();
12
14
  var mockOnClose = jest.fn();
13
15
  jest.mock('../../hooks/useSharingContext', function () {
@@ -15,8 +17,10 @@ jest.mock('../../hooks/useSharingContext', function () {
15
17
  useSharingContext: function useSharingContext() {
16
18
  return {
17
19
  share: mockShare,
20
+ revoke: mockRevoke,
18
21
  getSharingLink: mockGetSharingLink,
19
- getDocumentPermissions: jest.fn().mockReturnValue([])
22
+ getDocumentPermissions: jest.fn().mockReturnValue([]),
23
+ getRecipients: mockGetRecipients
20
24
  };
21
25
  }
22
26
  };
@@ -91,6 +95,13 @@ jest.mock('./DumbFederatedFolderModal', function () {
91
95
  return onRevoke('virtual-shared-drive-sharing-r1');
92
96
  }
93
97
  }, "Revoke"), /*#__PURE__*/React.createElement("button", {
98
+ "data-testid": "btn-revoke-existing",
99
+ onClick: function onClick() {
100
+ return onRevoke({
101
+ _id: 'folder-123'
102
+ }, 'abc', 1);
103
+ }
104
+ }, "Revoke Existing"), /*#__PURE__*/React.createElement("button", {
94
105
  "data-testid": "btn-close",
95
106
  onClick: onClose
96
107
  }, "Close"), /*#__PURE__*/React.createElement("button", {
@@ -150,6 +161,7 @@ describe('FederatedFolderModal', function () {
150
161
  beforeEach(function () {
151
162
  jest.clearAllMocks();
152
163
  mockGetSharingLink.mockReturnValue('https://example.com/share/abc123');
164
+ mockGetRecipients.mockReturnValue([]);
153
165
  client = createTestClient();
154
166
  sharingContextValue = createSharingContextValue();
155
167
  });
@@ -511,22 +523,109 @@ describe('FederatedFolderModal', function () {
511
523
  }, _callee12);
512
524
  })));
513
525
  });
514
- describe('createContact callback', function () {
515
- it('should call client.create when creating a contact', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee13() {
526
+ describe('existing recipients', function () {
527
+ var existingRecipients = [{
528
+ index: 'sharing-abc-member-0',
529
+ name: 'Charlie',
530
+ email: 'charlie@example.com',
531
+ status: 'owner',
532
+ type: 'two-way',
533
+ sharingId: 'abc',
534
+ memberIndex: 0
535
+ }, {
536
+ index: 'sharing-abc-member-1',
537
+ name: 'Diana',
538
+ email: 'diana@example.com',
539
+ status: 'ready',
540
+ type: 'one-way',
541
+ sharingId: 'abc',
542
+ memberIndex: 1
543
+ }];
544
+ it('should display existing recipients from getRecipients', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee13() {
516
545
  var _setup13, getByTestId;
517
546
 
518
547
  return _regeneratorRuntime.wrap(function _callee13$(_context13) {
519
548
  while (1) switch (_context13.prev = _context13.next) {
520
549
  case 0:
550
+ mockGetRecipients.mockReturnValue(existingRecipients);
521
551
  _setup13 = setup(), getByTestId = _setup13.getByTestId;
522
- _context13.next = 3;
552
+ _context13.next = 4;
553
+ return waitFor(function () {
554
+ expect(getByTestId('recipients-count').textContent).toBe('2');
555
+ });
556
+
557
+ case 4:
558
+ case "end":
559
+ return _context13.stop();
560
+ }
561
+ }, _callee13);
562
+ })));
563
+ it('should pass existing recipients as currentRecipients', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee14() {
564
+ var _setup14, getByTestId;
565
+
566
+ return _regeneratorRuntime.wrap(function _callee14$(_context14) {
567
+ while (1) switch (_context14.prev = _context14.next) {
568
+ case 0:
569
+ mockGetRecipients.mockReturnValue(existingRecipients);
570
+ _setup14 = setup(), getByTestId = _setup14.getByTestId;
571
+ _context14.next = 4;
572
+ return waitFor(function () {
573
+ expect(getByTestId('recipients-count').textContent).toBe('2');
574
+ });
575
+
576
+ case 4:
577
+ case "end":
578
+ return _context14.stop();
579
+ }
580
+ }, _callee14);
581
+ })));
582
+ it('should call revoke for existing member', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee15() {
583
+ var _setup15, getByTestId;
584
+
585
+ return _regeneratorRuntime.wrap(function _callee15$(_context15) {
586
+ while (1) switch (_context15.prev = _context15.next) {
587
+ case 0:
588
+ mockGetRecipients.mockReturnValue(existingRecipients);
589
+ mockRevoke.mockResolvedValueOnce({});
590
+ _setup15 = setup(), getByTestId = _setup15.getByTestId;
591
+ _context15.next = 5;
592
+ return waitFor(function () {
593
+ expect(getByTestId('dumb-modal')).toBeTruthy();
594
+ });
595
+
596
+ case 5:
597
+ // Simulate revoke of an existing member (non-virtual-shared-drive index)
598
+ fireEvent.click(getByTestId('btn-revoke-existing'));
599
+ _context15.next = 8;
600
+ return waitFor(function () {
601
+ expect(mockRevoke).toHaveBeenCalledWith({
602
+ _id: 'folder-123'
603
+ }, 'abc', 1);
604
+ });
605
+
606
+ case 8:
607
+ case "end":
608
+ return _context15.stop();
609
+ }
610
+ }, _callee15);
611
+ })));
612
+ });
613
+ describe('createContact callback', function () {
614
+ it('should call client.create when creating a contact', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee16() {
615
+ var _setup16, getByTestId;
616
+
617
+ return _regeneratorRuntime.wrap(function _callee16$(_context16) {
618
+ while (1) switch (_context16.prev = _context16.next) {
619
+ case 0:
620
+ _setup16 = setup(), getByTestId = _setup16.getByTestId;
621
+ _context16.next = 3;
523
622
  return waitFor(function () {
524
623
  expect(getByTestId('btn-create-contact')).toBeTruthy();
525
624
  });
526
625
 
527
626
  case 3:
528
627
  fireEvent.click(getByTestId('btn-create-contact'));
529
- _context13.next = 6;
628
+ _context16.next = 6;
530
629
  return waitFor(function () {
531
630
  expect(client.create).toHaveBeenCalledWith('io.cozy.contacts', {
532
631
  email: 'test@example.com'
@@ -535,9 +634,9 @@ describe('FederatedFolderModal', function () {
535
634
 
536
635
  case 6:
537
636
  case "end":
538
- return _context13.stop();
637
+ return _context16.stop();
539
638
  }
540
- }, _callee13);
639
+ }, _callee16);
541
640
  })));
542
641
  });
543
642
  });
@@ -1,21 +1,23 @@
1
1
  import _extends from "@babel/runtime/helpers/extends";
2
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
3
  import PropTypes from 'prop-types';
3
- import React from 'react';
4
+ import React, { useState } from 'react';
4
5
  import { useI18n } from 'twake-i18n';
5
6
  import Avatar from 'cozy-ui/transpiled/react/Avatar';
7
+ import DropdownButton from 'cozy-ui/transpiled/react/DropdownButton';
6
8
  import Fade from 'cozy-ui/transpiled/react/Fade';
7
9
  import Icon from 'cozy-ui/transpiled/react/Icon';
8
10
  import LinkIcon from 'cozy-ui/transpiled/react/Icons/Link';
9
11
  import ListItem from 'cozy-ui/transpiled/react/ListItem';
10
12
  import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon';
11
13
  import ListItemText from 'cozy-ui/transpiled/react/ListItemText';
12
- import Typography from 'cozy-ui/transpiled/react/Typography';
13
14
  import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints';
14
15
  import LinkRecipientPermissions from './LinkRecipientPermissions';
15
16
  import RecipientConfirm from './RecipientConfirm';
16
17
  import { checkIsPermissionHasPassword, getPermissionExpiresDate } from '../../helpers/permissions';
17
18
  import { FADE_IN_DURATION } from '../../helpers/recipients';
18
19
  import { useSharingContext } from '../../hooks/useSharingContext';
20
+ import { ShareRestrictionModal } from '../ShareRestrictionModal/ShareRestrictionModal';
19
21
 
20
22
  var LinkRecipient = function LinkRecipient(props) {
21
23
  var _useI18n = useI18n(),
@@ -33,6 +35,12 @@ var LinkRecipient = function LinkRecipient(props) {
33
35
  verifyRecipient = props.verifyRecipient,
34
36
  fadeIn = props.fadeIn,
35
37
  document = props.document;
38
+
39
+ var _useState = useState(false),
40
+ _useState2 = _slicedToArray(_useState, 2),
41
+ openShareRestrictionModal = _useState2[0],
42
+ setOpenShareRestrictionModal = _useState2[1];
43
+
36
44
  var permissions = getDocumentPermissions(document === null || document === void 0 ? void 0 : document._id);
37
45
  var hasPassword = checkIsPermissionHasPassword(permissions);
38
46
  var expiresDate = getPermissionExpiresDate(permissions);
@@ -61,11 +69,19 @@ var LinkRecipient = function LinkRecipient(props) {
61
69
  }, /*#__PURE__*/React.createElement(Icon, {
62
70
  icon: LinkIcon
63
71
  }))), /*#__PURE__*/React.createElement(ListItemText, {
64
- primary: /*#__PURE__*/React.createElement(Typography, {
65
- variant: "body1"
72
+ primary: /*#__PURE__*/React.createElement(DropdownButton, {
73
+ textVariant: "body1",
74
+ onClick: function onClick() {
75
+ return setOpenShareRestrictionModal(true);
76
+ }
66
77
  }, textPrimary),
67
78
  secondary: textSecondary
68
- }), RightPart));
79
+ }), RightPart, openShareRestrictionModal && /*#__PURE__*/React.createElement(ShareRestrictionModal, {
80
+ file: document,
81
+ onClose: function onClose() {
82
+ return setOpenShareRestrictionModal(false);
83
+ }
84
+ })));
69
85
  };
70
86
 
71
87
  LinkRecipient.propTypes = {
@@ -2,14 +2,20 @@ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
2
  import PropTypes from 'prop-types';
3
3
  import React, { useState, useRef } from 'react';
4
4
  import { useI18n } from 'twake-i18n';
5
+ import ActionsMenu from 'cozy-ui/transpiled/react/ActionsMenu';
6
+ import { makeActions, divider } from 'cozy-ui/transpiled/react/ActionsMenu/Actions';
5
7
  import DropdownButton from 'cozy-ui/transpiled/react/DropdownButton';
8
+ import { revokeLink } from './actions/revokeLink';
9
+ import { setReadOnlySharedPermission } from './actions/setReadOnlySharedPermission';
10
+ import { setReadWriteSharedPermission } from './actions/setReadWriteSharedPermission';
11
+ import { isOnlyReadOnlyLinkAllowed } from '../../helpers/link';
6
12
  import { checkIsReadOnlyPermissions } from '../../helpers/permissions';
7
- import { ShareRestrictionModal } from '../ShareRestrictionModal/ShareRestrictionModal';
13
+ import { useSharingContext } from '../../hooks/useSharingContext';
14
+ import { WRITE_PERMS, READ_ONLY_PERMS } from '../ShareRestrictionModal/helpers';
8
15
 
9
16
  var LinkRecipientPermissions = function LinkRecipientPermissions(_ref) {
10
17
  var className = _ref.className,
11
- document = _ref.document,
12
- permissions = _ref.permissions;
18
+ document = _ref.document;
13
19
 
14
20
  var _useI18n = useI18n(),
15
21
  t = _useI18n.t;
@@ -18,29 +24,70 @@ var LinkRecipientPermissions = function LinkRecipientPermissions(_ref) {
18
24
 
19
25
  var _useState = useState(false),
20
26
  _useState2 = _slicedToArray(_useState, 2),
21
- openShareRestrictionModal = _useState2[0],
22
- setOpenShareRestrictionModal = _useState2[1];
27
+ isMenuDisplayed = _useState2[0],
28
+ setMenuDisplayed = _useState2[1];
23
29
 
30
+ var _useSharingContext = useSharingContext(),
31
+ updateDocumentPermissions = _useSharingContext.updateDocumentPermissions,
32
+ revokeSharingLink = _useSharingContext.revokeSharingLink,
33
+ documentType = _useSharingContext.documentType,
34
+ getDocumentPermissions = _useSharingContext.getDocumentPermissions;
35
+
36
+ var permissions = getDocumentPermissions(document._id);
24
37
  var isReadOnlyPermissions = checkIsReadOnlyPermissions(permissions);
38
+ var type = isReadOnlyPermissions ? 'one-way' : 'two-way';
39
+
40
+ var toggleMenu = function toggleMenu() {
41
+ return setMenuDisplayed(!isMenuDisplayed);
42
+ };
43
+
44
+ var hideMenu = function hideMenu() {
45
+ return setMenuDisplayed(false);
46
+ };
47
+
48
+ var handleSetType = function handleSetType(newType) {
49
+ var verbs = newType === 'one-way' ? READ_ONLY_PERMS : WRITE_PERMS;
50
+ updateDocumentPermissions(document, {
51
+ verbs: verbs
52
+ });
53
+ };
54
+
55
+ var handleRevocation = function handleRevocation() {
56
+ return revokeSharingLink(document);
57
+ };
58
+
59
+ var actionsToShow = [setReadOnlySharedPermission];
60
+
61
+ if (!isOnlyReadOnlyLinkAllowed({
62
+ documentType: documentType
63
+ })) {
64
+ actionsToShow.push(setReadWriteSharedPermission);
65
+ }
66
+
67
+ actionsToShow.push(divider, revokeLink);
68
+ var actions = makeActions(actionsToShow, {
69
+ t: t,
70
+ type: type,
71
+ setType: handleSetType,
72
+ handleRevocation: handleRevocation
73
+ });
25
74
  return /*#__PURE__*/React.createElement("div", {
26
75
  className: className
27
76
  }, /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DropdownButton, {
28
77
  ref: buttonRef,
29
78
  textVariant: "body2",
30
- onClick: function onClick() {
31
- return setOpenShareRestrictionModal(true);
32
- }
33
- }, t("Share.type.".concat(isReadOnlyPermissions ? 'one-way' : 'two-way')).toLowerCase()), openShareRestrictionModal && /*#__PURE__*/React.createElement(ShareRestrictionModal, {
34
- file: document,
35
- onClose: function onClose() {
36
- return setOpenShareRestrictionModal(false);
37
- }
79
+ onClick: toggleMenu
80
+ }, t("Share.type.".concat(type)).toLowerCase()), /*#__PURE__*/React.createElement(ActionsMenu, {
81
+ ref: buttonRef,
82
+ open: isMenuDisplayed,
83
+ actions: actions,
84
+ autoClose: true,
85
+ onClose: hideMenu
38
86
  })));
39
87
  };
40
88
 
41
89
  LinkRecipientPermissions.propTypes = {
42
90
  className: PropTypes.string,
43
- document: PropTypes.object.isRequired,
44
- permissions: PropTypes.arrayOf(PropTypes.object).isRequired
91
+ document: PropTypes.object.isRequired
45
92
  };
46
93
  export default LinkRecipientPermissions;
@@ -0,0 +1,34 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
2
+ import React, { forwardRef } from 'react';
3
+ import ActionsMenuItem from 'cozy-ui/transpiled/react/ActionsMenu/ActionsMenuItem';
4
+ import Icon from 'cozy-ui/transpiled/react/Icon';
5
+ import TrashIcon from 'cozy-ui/transpiled/react/Icons/Trash';
6
+ import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon';
7
+ import ListItemText from 'cozy-ui/transpiled/react/ListItemText';
8
+ import Typography from 'cozy-ui/transpiled/react/Typography';
9
+
10
+ var revokeLink = function revokeLink(_ref) {
11
+ var t = _ref.t,
12
+ handleRevocation = _ref.handleRevocation;
13
+ var title = t('Share.permissionLink.deactivate');
14
+ return {
15
+ name: 'revokeLink',
16
+ label: title,
17
+ icon: TrashIcon,
18
+ action: handleRevocation,
19
+ Component: /*#__PURE__*/forwardRef(function RevokeLinkItem(props, ref) {
20
+ return /*#__PURE__*/React.createElement(ActionsMenuItem, _extends({}, props, {
21
+ ref: ref
22
+ }), /*#__PURE__*/React.createElement(ListItemIcon, null, /*#__PURE__*/React.createElement(Icon, {
23
+ icon: TrashIcon,
24
+ color: "var(--errorColor)"
25
+ })), /*#__PURE__*/React.createElement(ListItemText, {
26
+ primary: /*#__PURE__*/React.createElement(Typography, {
27
+ color: "error"
28
+ }, title)
29
+ }));
30
+ })
31
+ };
32
+ };
33
+
34
+ export { revokeLink };
@@ -14,7 +14,7 @@ import withLocales from '../../hoc/withLocales';
14
14
  import { useSharingContext } from '../../hooks/useSharingContext';
15
15
  import { FederatedFolderModal } from '../FederatedFolder/FederatedFolderModal';
16
16
  export var ShareModal = withLocales(function (props) {
17
- var _location$state, _byDocId$document$id;
17
+ var _location$state;
18
18
 
19
19
  var location = useLocation === null || useLocation === void 0 ? void 0 : useLocation();
20
20
  var locationProps = location === null || location === void 0 || (_location$state = location.state) === null || _location$state === void 0 ? void 0 : _location$state.modalProps;
@@ -59,12 +59,8 @@ export var ShareModal = withLocales(function (props) {
59
59
  }();
60
60
 
61
61
  var isFederatedMode = flag('drive.federated-shared-folder.enabled');
62
- var hasSharings = ((_byDocId$document$id = byDocId[document.id]) === null || _byDocId$document$id === void 0 || (_byDocId$document$id = _byDocId$document$id.sharings) === null || _byDocId$document$id === void 0 ? void 0 : _byDocId$document$id.length) > 0; // In federated mode, show FederatedFolderModal for unshared documents (sharing an existing folder).
63
- // When isFederatedMode && hasSharings is true, we intentionally fall through to the legacy
64
- // modals (EditableSharingModal / SharingDetailsModal) because federated-shared folders are
65
- // handled by those components.
66
62
 
67
- if (isFederatedMode && allLoaded && !hasSharings) {
63
+ if (isFederatedMode && allLoaded) {
68
64
  return /*#__PURE__*/React.createElement(FederatedFolderModal, {
69
65
  document: document,
70
66
  onClose: rest.onClose
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-sharing",
3
- "version": "28.6.0",
3
+ "version": "28.8.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": "64311a9d08372224b5214cbf49f7d658d93f9af9"
86
+ "gitHead": "a7ccd04fd67452a62b7459d3814fcff565063289"
87
87
  }
@@ -488,6 +488,7 @@ export class SharingProvider extends Component {
488
488
  updatedPermissions,
489
489
  { expiresAt, password }
490
490
  )
491
+ this.dispatch(updateSharingLink(resp.data))
491
492
  return resp
492
493
  })
493
494
  )
@@ -25,7 +25,9 @@ export const FederatedFolderModal = withLocales(
25
25
  getFederatedShareLink,
26
26
  getDocumentPermissions,
27
27
  getOwner,
28
- getSharingById
28
+ getSharingById,
29
+ getRecipients,
30
+ revoke
29
31
  } = useSharingContext()
30
32
  const { showAlert } = useAlert()
31
33
 
@@ -107,18 +109,29 @@ export const FederatedFolderModal = withLocales(
107
109
  }
108
110
  }
109
111
 
110
- const onRevoke = index => {
111
- const _id = index.split(RECIPIENT_INDEX_PREFIX)[1]
112
-
113
- setFederatedRecipients(prev => {
114
- return {
112
+ const onRevoke = async (documentOrIndex, sharingId, memberIndex) => {
113
+ if (
114
+ typeof documentOrIndex === 'string' &&
115
+ documentOrIndex.startsWith(RECIPIENT_INDEX_PREFIX)
116
+ ) {
117
+ const _id = documentOrIndex.split(RECIPIENT_INDEX_PREFIX)[1]
118
+ setFederatedRecipients(prev => ({
115
119
  recipients: prev.recipients.filter(r => r._id !== _id),
116
120
  readOnlyRecipients: prev.readOnlyRecipients.filter(r => r._id !== _id)
117
- }
118
- })
121
+ }))
122
+ } else {
123
+ await revoke(documentOrIndex, sharingId, memberIndex)
124
+ }
119
125
  }
120
126
 
121
- const recipients = formatRecipients(federatedRecipients)
127
+ const existingRecipients = existingDocument
128
+ ? getRecipients(existingDocument._id)
129
+ : []
130
+
131
+ const recipients = [
132
+ ...existingRecipients,
133
+ ...formatRecipients(federatedRecipients)
134
+ ]
122
135
 
123
136
  const modalTitle = t('FederatedFolder.shareTitle', { name: folderName })
124
137
 
@@ -131,7 +144,7 @@ export const FederatedFolderModal = withLocales(
131
144
  createContact={contact => client.create('io.cozy.contacts', contact)}
132
145
  recipients={recipients}
133
146
  readOnlyRecipients={federatedRecipients.readOnlyRecipients}
134
- currentRecipients={[]}
147
+ currentRecipients={existingRecipients}
135
148
  onRevoke={onRevoke}
136
149
  onSetType={onSetType}
137
150
  onSend={onSend}
@@ -7,15 +7,19 @@ import { FederatedFolderModal } from './FederatedFolderModal'
7
7
  import AppLike from '../SharingBanner/test/AppLike'
8
8
 
9
9
  const mockShare = jest.fn()
10
+ const mockRevoke = jest.fn()
10
11
  const mockGetSharingLink = jest.fn()
12
+ const mockGetRecipients = jest.fn().mockReturnValue([])
11
13
  const mockShowAlert = jest.fn()
12
14
  const mockOnClose = jest.fn()
13
15
 
14
16
  jest.mock('../../hooks/useSharingContext', () => ({
15
17
  useSharingContext: () => ({
16
18
  share: mockShare,
19
+ revoke: mockRevoke,
17
20
  getSharingLink: mockGetSharingLink,
18
- getDocumentPermissions: jest.fn().mockReturnValue([])
21
+ getDocumentPermissions: jest.fn().mockReturnValue([]),
22
+ getRecipients: mockGetRecipients
19
23
  })
20
24
  }))
21
25
 
@@ -79,6 +83,12 @@ jest.mock('./DumbFederatedFolderModal', () => ({
79
83
  >
80
84
  Revoke
81
85
  </button>
86
+ <button
87
+ data-testid="btn-revoke-existing"
88
+ onClick={() => onRevoke({ _id: 'folder-123' }, 'abc', 1)}
89
+ >
90
+ Revoke Existing
91
+ </button>
82
92
  <button data-testid="btn-close" onClick={onClose}>
83
93
  Close
84
94
  </button>
@@ -135,6 +145,7 @@ describe('FederatedFolderModal', () => {
135
145
  beforeEach(() => {
136
146
  jest.clearAllMocks()
137
147
  mockGetSharingLink.mockReturnValue('https://example.com/share/abc123')
148
+ mockGetRecipients.mockReturnValue([])
138
149
  client = createTestClient()
139
150
  sharingContextValue = createSharingContextValue()
140
151
  })
@@ -356,6 +367,64 @@ describe('FederatedFolderModal', () => {
356
367
  })
357
368
  })
358
369
 
370
+ describe('existing recipients', () => {
371
+ const existingRecipients = [
372
+ {
373
+ index: 'sharing-abc-member-0',
374
+ name: 'Charlie',
375
+ email: 'charlie@example.com',
376
+ status: 'owner',
377
+ type: 'two-way',
378
+ sharingId: 'abc',
379
+ memberIndex: 0
380
+ },
381
+ {
382
+ index: 'sharing-abc-member-1',
383
+ name: 'Diana',
384
+ email: 'diana@example.com',
385
+ status: 'ready',
386
+ type: 'one-way',
387
+ sharingId: 'abc',
388
+ memberIndex: 1
389
+ }
390
+ ]
391
+
392
+ it('should display existing recipients from getRecipients', async () => {
393
+ mockGetRecipients.mockReturnValue(existingRecipients)
394
+ const { getByTestId } = setup()
395
+
396
+ await waitFor(() => {
397
+ expect(getByTestId('recipients-count').textContent).toBe('2')
398
+ })
399
+ })
400
+
401
+ it('should pass existing recipients as currentRecipients', async () => {
402
+ mockGetRecipients.mockReturnValue(existingRecipients)
403
+ const { getByTestId } = setup()
404
+
405
+ await waitFor(() => {
406
+ expect(getByTestId('recipients-count').textContent).toBe('2')
407
+ })
408
+ })
409
+
410
+ it('should call revoke for existing member', async () => {
411
+ mockGetRecipients.mockReturnValue(existingRecipients)
412
+ mockRevoke.mockResolvedValueOnce({})
413
+ const { getByTestId } = setup()
414
+
415
+ await waitFor(() => {
416
+ expect(getByTestId('dumb-modal')).toBeTruthy()
417
+ })
418
+
419
+ // Simulate revoke of an existing member (non-virtual-shared-drive index)
420
+ fireEvent.click(getByTestId('btn-revoke-existing'))
421
+
422
+ await waitFor(() => {
423
+ expect(mockRevoke).toHaveBeenCalledWith({ _id: 'folder-123' }, 'abc', 1)
424
+ })
425
+ })
426
+ })
427
+
359
428
  describe('createContact callback', () => {
360
429
  it('should call client.create when creating a contact', async () => {
361
430
  const { getByTestId } = setup()
@@ -1,15 +1,15 @@
1
1
  import PropTypes from 'prop-types'
2
- import React from 'react'
2
+ import React, { useState } from 'react'
3
3
  import { useI18n } from 'twake-i18n'
4
4
 
5
5
  import Avatar from 'cozy-ui/transpiled/react/Avatar'
6
+ import DropdownButton from 'cozy-ui/transpiled/react/DropdownButton'
6
7
  import Fade from 'cozy-ui/transpiled/react/Fade'
7
8
  import Icon from 'cozy-ui/transpiled/react/Icon'
8
9
  import LinkIcon from 'cozy-ui/transpiled/react/Icons/Link'
9
10
  import ListItem from 'cozy-ui/transpiled/react/ListItem'
10
11
  import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'
11
12
  import ListItemText from 'cozy-ui/transpiled/react/ListItemText'
12
- import Typography from 'cozy-ui/transpiled/react/Typography'
13
13
  import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints'
14
14
 
15
15
  import LinkRecipientPermissions from './LinkRecipientPermissions'
@@ -20,6 +20,7 @@ import {
20
20
  } from '../../helpers/permissions'
21
21
  import { FADE_IN_DURATION } from '../../helpers/recipients'
22
22
  import { useSharingContext } from '../../hooks/useSharingContext'
23
+ import { ShareRestrictionModal } from '../ShareRestrictionModal/ShareRestrictionModal'
23
24
 
24
25
  const LinkRecipient = props => {
25
26
  const { t, f, lang } = useI18n()
@@ -28,6 +29,9 @@ const LinkRecipient = props => {
28
29
 
29
30
  const { recipientConfirmationData, verifyRecipient, fadeIn, document } = props
30
31
 
32
+ const [openShareRestrictionModal, setOpenShareRestrictionModal] =
33
+ useState(false)
34
+
31
35
  const permissions = getDocumentPermissions(document?._id)
32
36
  const hasPassword = checkIsPermissionHasPassword(permissions)
33
37
  const expiresDate = getPermissionExpiresDate(permissions)
@@ -63,10 +67,23 @@ const LinkRecipient = props => {
63
67
  </Avatar>
64
68
  </ListItemIcon>
65
69
  <ListItemText
66
- primary={<Typography variant="body1">{textPrimary}</Typography>}
70
+ primary={
71
+ <DropdownButton
72
+ textVariant="body1"
73
+ onClick={() => setOpenShareRestrictionModal(true)}
74
+ >
75
+ {textPrimary}
76
+ </DropdownButton>
77
+ }
67
78
  secondary={textSecondary}
68
79
  />
69
80
  {RightPart}
81
+ {openShareRestrictionModal && (
82
+ <ShareRestrictionModal
83
+ file={document}
84
+ onClose={() => setOpenShareRestrictionModal(false)}
85
+ />
86
+ )}
70
87
  </ListItem>
71
88
  </Fade>
72
89
  )
@@ -2,18 +2,58 @@ import PropTypes from 'prop-types'
2
2
  import React, { useState, useRef } from 'react'
3
3
  import { useI18n } from 'twake-i18n'
4
4
 
5
+ import ActionsMenu from 'cozy-ui/transpiled/react/ActionsMenu'
6
+ import {
7
+ makeActions,
8
+ divider
9
+ } from 'cozy-ui/transpiled/react/ActionsMenu/Actions'
5
10
  import DropdownButton from 'cozy-ui/transpiled/react/DropdownButton'
6
11
 
12
+ import { revokeLink } from './actions/revokeLink'
13
+ import { setReadOnlySharedPermission } from './actions/setReadOnlySharedPermission'
14
+ import { setReadWriteSharedPermission } from './actions/setReadWriteSharedPermission'
15
+ import { isOnlyReadOnlyLinkAllowed } from '../../helpers/link'
7
16
  import { checkIsReadOnlyPermissions } from '../../helpers/permissions'
8
- import { ShareRestrictionModal } from '../ShareRestrictionModal/ShareRestrictionModal'
17
+ import { useSharingContext } from '../../hooks/useSharingContext'
18
+ import { WRITE_PERMS, READ_ONLY_PERMS } from '../ShareRestrictionModal/helpers'
9
19
 
10
- const LinkRecipientPermissions = ({ className, document, permissions }) => {
20
+ const LinkRecipientPermissions = ({ className, document }) => {
11
21
  const { t } = useI18n()
12
22
  const buttonRef = useRef()
13
- const [openShareRestrictionModal, setOpenShareRestrictionModal] =
14
- useState(false)
23
+ const [isMenuDisplayed, setMenuDisplayed] = useState(false)
24
+ const {
25
+ updateDocumentPermissions,
26
+ revokeSharingLink,
27
+ documentType,
28
+ getDocumentPermissions
29
+ } = useSharingContext()
15
30
 
31
+ const permissions = getDocumentPermissions(document._id)
16
32
  const isReadOnlyPermissions = checkIsReadOnlyPermissions(permissions)
33
+ const type = isReadOnlyPermissions ? 'one-way' : 'two-way'
34
+
35
+ const toggleMenu = () => setMenuDisplayed(!isMenuDisplayed)
36
+ const hideMenu = () => setMenuDisplayed(false)
37
+
38
+ const handleSetType = newType => {
39
+ const verbs = newType === 'one-way' ? READ_ONLY_PERMS : WRITE_PERMS
40
+ updateDocumentPermissions(document, { verbs })
41
+ }
42
+
43
+ const handleRevocation = () => revokeSharingLink(document)
44
+
45
+ const actionsToShow = [setReadOnlySharedPermission]
46
+ if (!isOnlyReadOnlyLinkAllowed({ documentType })) {
47
+ actionsToShow.push(setReadWriteSharedPermission)
48
+ }
49
+ actionsToShow.push(divider, revokeLink)
50
+
51
+ const actions = makeActions(actionsToShow, {
52
+ t,
53
+ type,
54
+ setType: handleSetType,
55
+ handleRevocation
56
+ })
17
57
 
18
58
  return (
19
59
  <div className={className}>
@@ -21,18 +61,17 @@ const LinkRecipientPermissions = ({ className, document, permissions }) => {
21
61
  <DropdownButton
22
62
  ref={buttonRef}
23
63
  textVariant="body2"
24
- onClick={() => setOpenShareRestrictionModal(true)}
64
+ onClick={toggleMenu}
25
65
  >
26
- {t(
27
- `Share.type.${isReadOnlyPermissions ? 'one-way' : 'two-way'}`
28
- ).toLowerCase()}
66
+ {t(`Share.type.${type}`).toLowerCase()}
29
67
  </DropdownButton>
30
- {openShareRestrictionModal && (
31
- <ShareRestrictionModal
32
- file={document}
33
- onClose={() => setOpenShareRestrictionModal(false)}
34
- />
35
- )}
68
+ <ActionsMenu
69
+ ref={buttonRef}
70
+ open={isMenuDisplayed}
71
+ actions={actions}
72
+ autoClose
73
+ onClose={hideMenu}
74
+ />
36
75
  </>
37
76
  </div>
38
77
  )
@@ -40,8 +79,7 @@ const LinkRecipientPermissions = ({ className, document, permissions }) => {
40
79
 
41
80
  LinkRecipientPermissions.propTypes = {
42
81
  className: PropTypes.string,
43
- document: PropTypes.object.isRequired,
44
- permissions: PropTypes.arrayOf(PropTypes.object).isRequired
82
+ document: PropTypes.object.isRequired
45
83
  }
46
84
 
47
85
  export default LinkRecipientPermissions
@@ -0,0 +1,33 @@
1
+ import React, { forwardRef } from 'react'
2
+
3
+ import ActionsMenuItem from 'cozy-ui/transpiled/react/ActionsMenu/ActionsMenuItem'
4
+ import Icon from 'cozy-ui/transpiled/react/Icon'
5
+ import TrashIcon from 'cozy-ui/transpiled/react/Icons/Trash'
6
+ import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'
7
+ import ListItemText from 'cozy-ui/transpiled/react/ListItemText'
8
+ import Typography from 'cozy-ui/transpiled/react/Typography'
9
+
10
+ const revokeLink = ({ t, handleRevocation }) => {
11
+ const title = t('Share.permissionLink.deactivate')
12
+
13
+ return {
14
+ name: 'revokeLink',
15
+ label: title,
16
+ icon: TrashIcon,
17
+ action: handleRevocation,
18
+ Component: forwardRef(function RevokeLinkItem(props, ref) {
19
+ return (
20
+ <ActionsMenuItem {...props} ref={ref}>
21
+ <ListItemIcon>
22
+ <Icon icon={TrashIcon} color="var(--errorColor)" />
23
+ </ListItemIcon>
24
+ <ListItemText
25
+ primary={<Typography color="error">{title}</Typography>}
26
+ />
27
+ </ActionsMenuItem>
28
+ )
29
+ })
30
+ }
31
+ }
32
+
33
+ export { revokeLink }
@@ -35,13 +35,7 @@ export const ShareModal = withLocales(props => {
35
35
  }
36
36
 
37
37
  const isFederatedMode = flag('drive.federated-shared-folder.enabled')
38
- const hasSharings = byDocId[document.id]?.sharings?.length > 0
39
-
40
- // In federated mode, show FederatedFolderModal for unshared documents (sharing an existing folder).
41
- // When isFederatedMode && hasSharings is true, we intentionally fall through to the legacy
42
- // modals (EditableSharingModal / SharingDetailsModal) because federated-shared folders are
43
- // handled by those components.
44
- if (isFederatedMode && allLoaded && !hasSharings) {
38
+ if (isFederatedMode && allLoaded) {
45
39
  return <FederatedFolderModal document={document} onClose={rest.onClose} />
46
40
  }
47
41