cozy-sharing 38.1.2 → 38.1.4

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,16 @@
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
+ ## [38.1.4](https://github.com/cozy/cozy-libs/compare/cozy-sharing@38.1.3...cozy-sharing@38.1.4) (2026-09-30)
7
+
8
+ **Note:** Version bump only for package cozy-sharing
9
+
10
+ ## [38.1.3](https://github.com/cozy/cozy-libs/compare/cozy-sharing@38.1.2...cozy-sharing@38.1.3) (2026-09-23)
11
+
12
+ ### Bug Fixes
13
+
14
+ - **sharing:** Downgrade member on shared parents when confirming ([3602a68](https://github.com/cozy/cozy-libs/commit/3602a6879e6a7c585f9233d2336812ee9972e612))
15
+
6
16
  ## [38.1.2](https://github.com/cozy/cozy-libs/compare/cozy-sharing@38.1.1...cozy-sharing@38.1.2) (2026-09-23)
7
17
 
8
18
  ### Bug Fixes
@@ -1,6 +1,13 @@
1
1
  import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
2
2
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
3
  import _regeneratorRuntime from "@babel/runtime/regenerator";
4
+
5
+ function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
6
+
7
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
8
+
9
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
10
+
4
11
  import React, { useCallback, useRef, useState } from 'react';
5
12
  import { useClient } from 'cozy-client';
6
13
  import minilog from 'cozy-minilog';
@@ -53,38 +60,96 @@ var PermissionTypeMenuComponent = function PermissionTypeMenuComponent(_ref) {
53
60
  }, []);
54
61
  var applyType = useCallback( /*#__PURE__*/function () {
55
62
  var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(newType) {
63
+ var _ref3,
64
+ _ref3$withAncestors,
65
+ withAncestors,
66
+ ancestors,
67
+ _iterator,
68
+ _step,
69
+ ancestor,
70
+ _args = arguments;
71
+
56
72
  return _regeneratorRuntime.wrap(function _callee$(_context) {
57
73
  while (1) switch (_context.prev = _context.next) {
58
74
  case 0:
59
- _context.prev = 0;
60
- _context.next = 3;
75
+ _ref3 = _args.length > 1 && _args[1] !== undefined ? _args[1] : {}, _ref3$withAncestors = _ref3.withAncestors, withAncestors = _ref3$withAncestors === void 0 ? false : _ref3$withAncestors;
76
+ _context.prev = 1;
77
+
78
+ if (!withAncestors) {
79
+ _context.next = 21;
80
+ break;
81
+ }
82
+
83
+ // Ancestors first: only the target's own update refetches its
84
+ // effective recipients, which must already see them downgraded.
85
+ ancestors = ((recipient === null || recipient === void 0 ? void 0 : recipient.sources) || []).filter(function (s) {
86
+ return s.kind === 'ancestor' && !s.read_only;
87
+ });
88
+ _iterator = _createForOfIteratorHelper(ancestors);
89
+ _context.prev = 5;
90
+
91
+ _iterator.s();
92
+
93
+ case 7:
94
+ if ((_step = _iterator.n()).done) {
95
+ _context.next = 13;
96
+ break;
97
+ }
98
+
99
+ ancestor = _step.value;
100
+ _context.next = 11;
101
+ return updateSharingMemberType(ancestor.sharing_id, ancestor.member_index, newType);
102
+
103
+ case 11:
104
+ _context.next = 7;
105
+ break;
106
+
107
+ case 13:
108
+ _context.next = 18;
109
+ break;
110
+
111
+ case 15:
112
+ _context.prev = 15;
113
+ _context.t0 = _context["catch"](5);
114
+
115
+ _iterator.e(_context.t0);
116
+
117
+ case 18:
118
+ _context.prev = 18;
119
+
120
+ _iterator.f();
121
+
122
+ return _context.finish(18);
123
+
124
+ case 21:
125
+ _context.next = 23;
61
126
  return updateSharingMemberType(sharingId, memberIndex, newType);
62
127
 
63
- case 3:
64
- _context.next = 9;
128
+ case 23:
129
+ _context.next = 29;
65
130
  break;
66
131
 
67
- case 5:
68
- _context.prev = 5;
69
- _context.t0 = _context["catch"](0);
70
- log.error('Failed to change member permission type', _context.t0);
132
+ case 25:
133
+ _context.prev = 25;
134
+ _context.t1 = _context["catch"](1);
135
+ log.error('Failed to change member permission type', _context.t1);
71
136
  showAlert({
72
137
  message: t('Share.members.error.changePermission'),
73
138
  severity: 'error',
74
139
  variant: 'filled'
75
140
  });
76
141
 
77
- case 9:
142
+ case 29:
78
143
  case "end":
79
144
  return _context.stop();
80
145
  }
81
- }, _callee, null, [[0, 5]]);
146
+ }, _callee, null, [[1, 25], [5, 15, 18, 21]]);
82
147
  }));
83
148
 
84
149
  return function (_x) {
85
150
  return _ref2.apply(this, arguments);
86
151
  };
87
- }(), [memberIndex, sharingId, showAlert, t, updateSharingMemberType]); // Downgrading to viewer on a folder inside a shared parent also reduces
152
+ }(), [memberIndex, recipient, sharingId, showAlert, t, updateSharingMemberType]); // Downgrading to viewer on a folder inside a shared parent also reduces
88
153
  // the member's rights on the parent: require explicit confirmation first.
89
154
 
90
155
  var shouldConfirmDowngrade = type === 'two-way' && Boolean(documentPath) && (hasSharedParent === null || hasSharedParent === void 0 ? void 0 : hasSharedParent(documentPath));
@@ -102,7 +167,9 @@ var PermissionTypeMenuComponent = function PermissionTypeMenuComponent(_ref) {
102
167
  var handleConfirmDowngrade = useCallback(function () {
103
168
  var newType = pendingType;
104
169
  setPendingType(null);
105
- if (newType) applyType(newType);
170
+ if (newType) applyType(newType, {
171
+ withAncestors: true
172
+ });
106
173
  }, [applyType, pendingType]);
107
174
  var handleCancelDowngrade = useCallback(function () {
108
175
  setPendingType(null);
@@ -242,13 +242,62 @@ describe('PermissionTypeMenu component', function () {
242
242
  }
243
243
  }, _callee5);
244
244
  })));
245
+ it('should also downgrade the member on read-write ancestor shares when confirming', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee6() {
246
+ var _setup7, getByRole;
247
+
248
+ return _regeneratorRuntime.wrap(function _callee6$(_context6) {
249
+ while (1) switch (_context6.prev = _context6.next) {
250
+ case 0:
251
+ mockUpdateSharingMemberType.mockResolvedValue(undefined);
252
+ _setup7 = setup({
253
+ type: 'two-way',
254
+ document: folderDocument,
255
+ recipient: {
256
+ name: 'Bob',
257
+ sources: [{
258
+ kind: 'self',
259
+ sharing_id: 'sharing-123',
260
+ member_index: 1
261
+ }, {
262
+ kind: 'ancestor',
263
+ sharing_id: 'sharing-parent',
264
+ member_index: 2,
265
+ read_only: false
266
+ }, {
267
+ kind: 'ancestor',
268
+ sharing_id: 'sharing-grandparent',
269
+ member_index: 3,
270
+ read_only: true
271
+ }]
272
+ }
273
+ }), getByRole = _setup7.getByRole;
274
+ fireEvent.click(getByRole('button', {
275
+ name: 'Editor'
276
+ }));
277
+ fireEvent.click(getByRole('menuitem', {
278
+ name: 'Viewer'
279
+ }));
280
+ fireEvent.click(getByRole('button', {
281
+ name: 'Update parent'
282
+ }));
283
+ _context6.next = 7;
284
+ return waitFor(function () {
285
+ expect(mockUpdateSharingMemberType.mock.calls).toEqual([['sharing-parent', 2, 'one-way'], ['sharing-123', 1, 'one-way']]);
286
+ });
287
+
288
+ case 7:
289
+ case "end":
290
+ return _context6.stop();
291
+ }
292
+ }, _callee6);
293
+ })));
245
294
  it('should not call updateSharingMemberType when cancelling', function () {
246
- var _setup7 = setup({
295
+ var _setup8 = setup({
247
296
  type: 'two-way',
248
297
  document: folderDocument
249
298
  }),
250
- getByRole = _setup7.getByRole,
251
- queryByRole = _setup7.queryByRole;
299
+ getByRole = _setup8.getByRole,
300
+ queryByRole = _setup8.queryByRole;
252
301
 
253
302
  fireEvent.click(getByRole('button', {
254
303
  name: 'Editor'
@@ -267,12 +316,12 @@ describe('PermissionTypeMenu component', function () {
267
316
  it('should update directly when the folder has no shared parent', function () {
268
317
  mockHasSharedParent.mockReturnValue(false);
269
318
 
270
- var _setup8 = setup({
319
+ var _setup9 = setup({
271
320
  type: 'two-way',
272
321
  document: folderDocument
273
322
  }),
274
- getByRole = _setup8.getByRole,
275
- queryByRole = _setup8.queryByRole;
323
+ getByRole = _setup9.getByRole,
324
+ queryByRole = _setup9.queryByRole;
276
325
 
277
326
  fireEvent.click(getByRole('button', {
278
327
  name: 'Editor'
@@ -28,7 +28,8 @@ var mapToRecipient = function mapToRecipient(effectiveRecipient, source) {
28
28
  sharingId: source.sharing_id,
29
29
  memberIndex: source.member_index,
30
30
  avatarPath: "/sharings/".concat(source.sharing_id, "/recipients/").concat(source.member_index, "/avatar"),
31
- type: effectiveRecipient.read_only ? 'one-way' : 'two-way'
31
+ type: effectiveRecipient.read_only ? 'one-way' : 'two-way',
32
+ sources: effectiveRecipient.sources
32
33
  };
33
34
  };
34
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-sharing",
3
- "version": "38.1.2",
3
+ "version": "38.1.4",
4
4
  "description": "Provides sharing login for React applications.",
5
5
  "main": "dist/index.js",
6
6
  "author": "Cozy",
@@ -59,7 +59,7 @@
59
59
  "cozy-intent": "^2.31.1",
60
60
  "cozy-minilog": "^3.10.1",
61
61
  "cozy-ui": "^140.5.0",
62
- "cozy-ui-plus": "^13.0.6",
62
+ "cozy-ui-plus": "^13.0.8",
63
63
  "jest": "30.3.0",
64
64
  "jest-environment-jsdom": "30.3.0",
65
65
  "react": "16.12.0",
@@ -85,5 +85,5 @@
85
85
  "sideEffects": [
86
86
  "*.css"
87
87
  ],
88
- "gitHead": "f3a4ca49d7e1fea6099fe103720926959ed02e1a"
88
+ "gitHead": "2928778c60c287a513ae3e131177ef20b3d35ef5"
89
89
  }
@@ -40,8 +40,22 @@ const PermissionTypeMenuComponent = ({
40
40
  }, [])
41
41
 
42
42
  const applyType = useCallback(
43
- async newType => {
43
+ async (newType, { withAncestors = false } = {}) => {
44
44
  try {
45
+ if (withAncestors) {
46
+ // Ancestors first: only the target's own update refetches its
47
+ // effective recipients, which must already see them downgraded.
48
+ const ancestors = (recipient?.sources || []).filter(
49
+ s => s.kind === 'ancestor' && !s.read_only
50
+ )
51
+ for (const ancestor of ancestors) {
52
+ await updateSharingMemberType(
53
+ ancestor.sharing_id,
54
+ ancestor.member_index,
55
+ newType
56
+ )
57
+ }
58
+ }
45
59
  await updateSharingMemberType(sharingId, memberIndex, newType)
46
60
  } catch (error) {
47
61
  log.error('Failed to change member permission type', error)
@@ -52,7 +66,7 @@ const PermissionTypeMenuComponent = ({
52
66
  })
53
67
  }
54
68
  },
55
- [memberIndex, sharingId, showAlert, t, updateSharingMemberType]
69
+ [memberIndex, recipient, sharingId, showAlert, t, updateSharingMemberType]
56
70
  )
57
71
 
58
72
  // Downgrading to viewer on a folder inside a shared parent also reduces
@@ -78,7 +92,7 @@ const PermissionTypeMenuComponent = ({
78
92
  const handleConfirmDowngrade = useCallback(() => {
79
93
  const newType = pendingType
80
94
  setPendingType(null)
81
- if (newType) applyType(newType)
95
+ if (newType) applyType(newType, { withAncestors: true })
82
96
  }, [applyType, pendingType])
83
97
 
84
98
  const handleCancelDowngrade = useCallback(() => {
@@ -179,6 +179,44 @@ describe('PermissionTypeMenu component', () => {
179
179
  })
180
180
  })
181
181
 
182
+ it('should also downgrade the member on read-write ancestor shares when confirming', async () => {
183
+ mockUpdateSharingMemberType.mockResolvedValue(undefined)
184
+
185
+ const { getByRole } = setup({
186
+ type: 'two-way',
187
+ document: folderDocument,
188
+ recipient: {
189
+ name: 'Bob',
190
+ sources: [
191
+ { kind: 'self', sharing_id: 'sharing-123', member_index: 1 },
192
+ {
193
+ kind: 'ancestor',
194
+ sharing_id: 'sharing-parent',
195
+ member_index: 2,
196
+ read_only: false
197
+ },
198
+ {
199
+ kind: 'ancestor',
200
+ sharing_id: 'sharing-grandparent',
201
+ member_index: 3,
202
+ read_only: true
203
+ }
204
+ ]
205
+ }
206
+ })
207
+
208
+ fireEvent.click(getByRole('button', { name: 'Editor' }))
209
+ fireEvent.click(getByRole('menuitem', { name: 'Viewer' }))
210
+ fireEvent.click(getByRole('button', { name: 'Update parent' }))
211
+
212
+ await waitFor(() => {
213
+ expect(mockUpdateSharingMemberType.mock.calls).toEqual([
214
+ ['sharing-parent', 2, 'one-way'],
215
+ ['sharing-123', 1, 'one-way']
216
+ ])
217
+ })
218
+ })
219
+
182
220
  it('should not call updateSharingMemberType when cancelling', () => {
183
221
  const { getByRole, queryByRole } = setup({
184
222
  type: 'two-way',
@@ -24,7 +24,8 @@ const mapToRecipient = (effectiveRecipient, source) => ({
24
24
  sharingId: source.sharing_id,
25
25
  memberIndex: source.member_index,
26
26
  avatarPath: `/sharings/${source.sharing_id}/recipients/${source.member_index}/avatar`,
27
- type: effectiveRecipient.read_only ? 'one-way' : 'two-way'
27
+ type: effectiveRecipient.read_only ? 'one-way' : 'two-way',
28
+ sources: effectiveRecipient.sources
28
29
  })
29
30
 
30
31
  export const mapEffectiveRecipients = effectiveRecipients =>