cozy-sharing 33.4.0 → 33.4.2
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 +13 -0
- package/dist/SharingProvider.js +40 -15
- package/dist/SharingProvider.spec.js +100 -8
- package/dist/components/Avatar/MemberAvatar.js +21 -5
- package/dist/components/Avatar/MemberAvatar.spec.js +77 -0
- package/dist/components/Recipient/MemberRecipientPermissions.js +9 -11
- package/dist/components/Recipient/OwnerRecipientDefault.js +8 -1
- package/dist/components/Recipient/OwnerRecipientDefaultLite.js +5 -1
- package/package.json +2 -2
- package/src/SharingProvider.jsx +35 -0
- package/src/SharingProvider.spec.jsx +121 -7
- package/src/components/Avatar/MemberAvatar.jsx +19 -4
- package/src/components/Avatar/MemberAvatar.spec.jsx +80 -0
- package/src/components/Recipient/MemberRecipientPermissions.jsx +1 -1
- package/src/components/Recipient/OwnerRecipientDefault.jsx +8 -0
- package/src/components/Recipient/OwnerRecipientDefaultLite.jsx +5 -0
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.4.2](https://github.com/cozy/cozy-libs/compare/cozy-sharing@33.4.1...cozy-sharing@33.4.2) (2026-06-16)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **cozy-sharing:** Preserve sharing updates on rollback ([7c6db1a](https://github.com/cozy/cozy-libs/commit/7c6db1ad63f3997f01bdfee7140f9dad36dde2c2))
|
|
11
|
+
- **sharing:** Update member permissions optimistically ([5c1ac86](https://github.com/cozy/cozy-libs/commit/5c1ac8631e4598ac6d2bc08761d2064c9b396f66))
|
|
12
|
+
|
|
13
|
+
## [33.4.1](https://github.com/cozy/cozy-libs/compare/cozy-sharing@33.4.0...cozy-sharing@33.4.1) (2026-06-15)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **cozy-sharing:** Keep the owner avatar visible in the share modal ([f63ae1d](https://github.com/cozy/cozy-libs/commit/f63ae1de24de5ca7746b0c6da579f52fa5593806))
|
|
18
|
+
|
|
6
19
|
# [33.4.0](https://github.com/cozy/cozy-libs/compare/cozy-sharing@33.3.4...cozy-sharing@33.4.0) (2026-06-15)
|
|
7
20
|
|
|
8
21
|
### Bug Fixes
|
package/dist/SharingProvider.js
CHANGED
|
@@ -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, makeReadOnly;
|
|
474
|
+
var sharing, member, makeReadOnly, optimisticSharing, currentSharing, rollbackSharing;
|
|
475
475
|
return _regeneratorRuntime.wrap(function _callee10$(_context10) {
|
|
476
476
|
while (1) switch (_context10.prev = _context10.next) {
|
|
477
477
|
case 0:
|
|
@@ -505,39 +505,64 @@ export var SharingProvider = /*#__PURE__*/function (_Component) {
|
|
|
505
505
|
throw new Error('Unsupported sharing type: ' + newType);
|
|
506
506
|
|
|
507
507
|
case 9:
|
|
508
|
-
|
|
508
|
+
optimisticSharing = _objectSpread(_objectSpread({}, sharing), {}, {
|
|
509
|
+
attributes: _objectSpread(_objectSpread({}, sharing.attributes), {}, {
|
|
510
|
+
members: sharing.attributes.members.map(function (member, index) {
|
|
511
|
+
return index === memberIndex ? _objectSpread(_objectSpread({}, member), {}, {
|
|
512
|
+
read_only: makeReadOnly
|
|
513
|
+
}) : member;
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
_this2.dispatch(updateSharing(optimisticSharing));
|
|
519
|
+
|
|
520
|
+
_context10.prev = 11;
|
|
509
521
|
|
|
510
522
|
if (!makeReadOnly) {
|
|
511
|
-
_context10.next =
|
|
523
|
+
_context10.next = 17;
|
|
512
524
|
break;
|
|
513
525
|
}
|
|
514
526
|
|
|
515
|
-
_context10.next =
|
|
527
|
+
_context10.next = 15;
|
|
516
528
|
return _this2.sharingCol.setReadOnly(sharing, memberIndex);
|
|
517
529
|
|
|
518
|
-
case
|
|
519
|
-
_context10.next =
|
|
530
|
+
case 15:
|
|
531
|
+
_context10.next = 19;
|
|
520
532
|
break;
|
|
521
533
|
|
|
522
|
-
case
|
|
523
|
-
_context10.next =
|
|
534
|
+
case 17:
|
|
535
|
+
_context10.next = 19;
|
|
524
536
|
return _this2.sharingCol.setReadWrite(sharing, memberIndex);
|
|
525
537
|
|
|
526
|
-
case
|
|
527
|
-
_context10.next =
|
|
538
|
+
case 19:
|
|
539
|
+
_context10.next = 28;
|
|
528
540
|
break;
|
|
529
541
|
|
|
530
|
-
case
|
|
531
|
-
_context10.prev =
|
|
532
|
-
_context10.t0 = _context10["catch"](
|
|
542
|
+
case 21:
|
|
543
|
+
_context10.prev = 21;
|
|
544
|
+
_context10.t0 = _context10["catch"](11);
|
|
545
|
+
currentSharing = _getSharingById(_this2.state, sharingId) || sharing;
|
|
546
|
+
rollbackSharing = _objectSpread(_objectSpread({}, currentSharing), {}, {
|
|
547
|
+
attributes: _objectSpread(_objectSpread({}, currentSharing.attributes), {}, {
|
|
548
|
+
members: currentSharing.attributes.members.map(function (member, index) {
|
|
549
|
+
return index === memberIndex ? _objectSpread(_objectSpread({}, member), {}, {
|
|
550
|
+
read_only: !makeReadOnly
|
|
551
|
+
}) : member;
|
|
552
|
+
})
|
|
553
|
+
})
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
_this2.dispatch(updateSharing(rollbackSharing));
|
|
557
|
+
|
|
533
558
|
log.error("Failed to change member ".concat(member.email, " permission type to ").concat(newType), _context10.t0);
|
|
534
559
|
throw _context10.t0;
|
|
535
560
|
|
|
536
|
-
case
|
|
561
|
+
case 28:
|
|
537
562
|
case "end":
|
|
538
563
|
return _context10.stop();
|
|
539
564
|
}
|
|
540
|
-
}, _callee10, null, [[
|
|
565
|
+
}, _callee10, null, [[11, 21]]);
|
|
541
566
|
}));
|
|
542
567
|
|
|
543
568
|
return function (_x16, _x17, _x18) {
|
|
@@ -744,7 +744,9 @@ describe('updateSharingMemberType', function () {
|
|
|
744
744
|
setReadOnly: jest.fn().mockResolvedValue({}),
|
|
745
745
|
setReadWrite: jest.fn().mockResolvedValue({})
|
|
746
746
|
};
|
|
747
|
-
jest.
|
|
747
|
+
instance.dispatch = jest.fn(function (action) {
|
|
748
|
+
instance.state = _objectSpread(_objectSpread({}, instance.state), reducer(instance.state, action));
|
|
749
|
+
});
|
|
748
750
|
});
|
|
749
751
|
it('should throw when sharing is not found', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee15() {
|
|
750
752
|
return _regeneratorRuntime.wrap(function _callee15$(_context15) {
|
|
@@ -806,7 +808,7 @@ describe('updateSharingMemberType', function () {
|
|
|
806
808
|
}
|
|
807
809
|
}, _callee18);
|
|
808
810
|
})));
|
|
809
|
-
it('should
|
|
811
|
+
it('should optimistically dispatch a state update when switching to one-way', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee19() {
|
|
810
812
|
return _regeneratorRuntime.wrap(function _callee19$(_context19) {
|
|
811
813
|
while (1) switch (_context19.prev = _context19.next) {
|
|
812
814
|
case 0:
|
|
@@ -814,7 +816,16 @@ describe('updateSharingMemberType', function () {
|
|
|
814
816
|
return instance.updateSharingMemberType('sharing-123', 1, 'one-way');
|
|
815
817
|
|
|
816
818
|
case 2:
|
|
817
|
-
expect(instance.
|
|
819
|
+
expect(instance.dispatch).toHaveBeenCalledWith({
|
|
820
|
+
type: 'UPDATE_SHARING',
|
|
821
|
+
sharing: _objectSpread(_objectSpread({}, mockSharing), {}, {
|
|
822
|
+
attributes: _objectSpread(_objectSpread({}, mockSharing.attributes), {}, {
|
|
823
|
+
members: [mockSharing.attributes.members[0], _objectSpread(_objectSpread({}, mockSharing.attributes.members[1]), {}, {
|
|
824
|
+
read_only: true
|
|
825
|
+
})]
|
|
826
|
+
})
|
|
827
|
+
})
|
|
828
|
+
});
|
|
818
829
|
|
|
819
830
|
case 3:
|
|
820
831
|
case "end":
|
|
@@ -822,7 +833,7 @@ describe('updateSharingMemberType', function () {
|
|
|
822
833
|
}
|
|
823
834
|
}, _callee19);
|
|
824
835
|
})));
|
|
825
|
-
it('should
|
|
836
|
+
it('should optimistically dispatch a state update when switching to two-way', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee20() {
|
|
826
837
|
return _regeneratorRuntime.wrap(function _callee20$(_context20) {
|
|
827
838
|
while (1) switch (_context20.prev = _context20.next) {
|
|
828
839
|
case 0:
|
|
@@ -830,7 +841,16 @@ describe('updateSharingMemberType', function () {
|
|
|
830
841
|
return instance.updateSharingMemberType('sharing-123', 1, 'two-way');
|
|
831
842
|
|
|
832
843
|
case 2:
|
|
833
|
-
expect(instance.
|
|
844
|
+
expect(instance.dispatch).toHaveBeenCalledWith({
|
|
845
|
+
type: 'UPDATE_SHARING',
|
|
846
|
+
sharing: _objectSpread(_objectSpread({}, mockSharing), {}, {
|
|
847
|
+
attributes: _objectSpread(_objectSpread({}, mockSharing.attributes), {}, {
|
|
848
|
+
members: [mockSharing.attributes.members[0], _objectSpread(_objectSpread({}, mockSharing.attributes.members[1]), {}, {
|
|
849
|
+
read_only: false
|
|
850
|
+
})]
|
|
851
|
+
})
|
|
852
|
+
})
|
|
853
|
+
});
|
|
834
854
|
|
|
835
855
|
case 3:
|
|
836
856
|
case "end":
|
|
@@ -866,7 +886,7 @@ describe('updateSharingMemberType', function () {
|
|
|
866
886
|
}
|
|
867
887
|
}, _callee22);
|
|
868
888
|
})));
|
|
869
|
-
it('should
|
|
889
|
+
it('should rollback optimistic state update if API call fails', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee23() {
|
|
870
890
|
return _regeneratorRuntime.wrap(function _callee23$(_context23) {
|
|
871
891
|
while (1) switch (_context23.prev = _context23.next) {
|
|
872
892
|
case 0:
|
|
@@ -875,14 +895,86 @@ describe('updateSharingMemberType', function () {
|
|
|
875
895
|
return expect(instance.updateSharingMemberType('sharing-123', 1, 'one-way')).rejects.toThrow('Network error');
|
|
876
896
|
|
|
877
897
|
case 3:
|
|
878
|
-
expect(instance.
|
|
898
|
+
expect(instance.dispatch).toHaveBeenNthCalledWith(1, {
|
|
899
|
+
type: 'UPDATE_SHARING',
|
|
900
|
+
sharing: _objectSpread(_objectSpread({}, mockSharing), {}, {
|
|
901
|
+
attributes: _objectSpread(_objectSpread({}, mockSharing.attributes), {}, {
|
|
902
|
+
members: [mockSharing.attributes.members[0], _objectSpread(_objectSpread({}, mockSharing.attributes.members[1]), {}, {
|
|
903
|
+
read_only: true
|
|
904
|
+
})]
|
|
905
|
+
})
|
|
906
|
+
})
|
|
907
|
+
});
|
|
908
|
+
expect(instance.dispatch).toHaveBeenNthCalledWith(2, {
|
|
909
|
+
type: 'UPDATE_SHARING',
|
|
910
|
+
sharing: _objectSpread(_objectSpread({}, mockSharing), {}, {
|
|
911
|
+
attributes: _objectSpread(_objectSpread({}, mockSharing.attributes), {}, {
|
|
912
|
+
members: [mockSharing.attributes.members[0], _objectSpread(_objectSpread({}, mockSharing.attributes.members[1]), {}, {
|
|
913
|
+
read_only: false
|
|
914
|
+
})]
|
|
915
|
+
})
|
|
916
|
+
})
|
|
917
|
+
});
|
|
879
918
|
|
|
880
|
-
case
|
|
919
|
+
case 5:
|
|
881
920
|
case "end":
|
|
882
921
|
return _context23.stop();
|
|
883
922
|
}
|
|
884
923
|
}, _callee23);
|
|
885
924
|
})));
|
|
925
|
+
it('should preserve realtime changes when rolling back an API failure', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee25() {
|
|
926
|
+
var realtimeSharing;
|
|
927
|
+
return _regeneratorRuntime.wrap(function _callee25$(_context25) {
|
|
928
|
+
while (1) switch (_context25.prev = _context25.next) {
|
|
929
|
+
case 0:
|
|
930
|
+
realtimeSharing = _objectSpread(_objectSpread({}, mockSharing), {}, {
|
|
931
|
+
attributes: _objectSpread(_objectSpread({}, mockSharing.attributes), {}, {
|
|
932
|
+
description: 'updated by realtime',
|
|
933
|
+
members: [_objectSpread(_objectSpread({}, mockSharing.attributes.members[0]), {}, {
|
|
934
|
+
status: 'seen'
|
|
935
|
+
}), _objectSpread(_objectSpread({}, mockSharing.attributes.members[1]), {}, {
|
|
936
|
+
read_only: true
|
|
937
|
+
})]
|
|
938
|
+
})
|
|
939
|
+
});
|
|
940
|
+
instance.sharingCol.setReadOnly.mockImplementation( /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee24() {
|
|
941
|
+
return _regeneratorRuntime.wrap(function _callee24$(_context24) {
|
|
942
|
+
while (1) switch (_context24.prev = _context24.next) {
|
|
943
|
+
case 0:
|
|
944
|
+
instance.state = _objectSpread(_objectSpread({}, instance.state), {}, {
|
|
945
|
+
sharings: instance.state.sharings.map(function (sharing) {
|
|
946
|
+
return sharing.id === realtimeSharing.id ? realtimeSharing : sharing;
|
|
947
|
+
})
|
|
948
|
+
});
|
|
949
|
+
throw new Error('Network error');
|
|
950
|
+
|
|
951
|
+
case 2:
|
|
952
|
+
case "end":
|
|
953
|
+
return _context24.stop();
|
|
954
|
+
}
|
|
955
|
+
}, _callee24);
|
|
956
|
+
})));
|
|
957
|
+
_context25.next = 4;
|
|
958
|
+
return expect(instance.updateSharingMemberType('sharing-123', 1, 'one-way')).rejects.toThrow('Network error');
|
|
959
|
+
|
|
960
|
+
case 4:
|
|
961
|
+
expect(instance.dispatch).toHaveBeenNthCalledWith(2, {
|
|
962
|
+
type: 'UPDATE_SHARING',
|
|
963
|
+
sharing: _objectSpread(_objectSpread({}, realtimeSharing), {}, {
|
|
964
|
+
attributes: _objectSpread(_objectSpread({}, realtimeSharing.attributes), {}, {
|
|
965
|
+
members: [realtimeSharing.attributes.members[0], _objectSpread(_objectSpread({}, realtimeSharing.attributes.members[1]), {}, {
|
|
966
|
+
read_only: false
|
|
967
|
+
})]
|
|
968
|
+
})
|
|
969
|
+
})
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
case 5:
|
|
973
|
+
case "end":
|
|
974
|
+
return _context25.stop();
|
|
975
|
+
}
|
|
976
|
+
}, _callee25);
|
|
977
|
+
})));
|
|
886
978
|
}); // TODO Convert with react-testing-library
|
|
887
979
|
// describe('hasWriteAccess', () => {
|
|
888
980
|
// it('tells if a doc is writtable', () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
2
3
|
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
|
|
3
4
|
var _excluded = ["recipient"];
|
|
4
5
|
|
|
@@ -6,7 +7,7 @@ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (O
|
|
|
6
7
|
|
|
7
8
|
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
9
|
|
|
9
|
-
import React from 'react';
|
|
10
|
+
import React, { useState } from 'react';
|
|
10
11
|
import { useClient } from 'cozy-client';
|
|
11
12
|
import Avatar from 'cozy-ui/transpiled/react/Avatar';
|
|
12
13
|
import logger from '../../logger';
|
|
@@ -16,11 +17,19 @@ var MemberAvatar = function MemberAvatar(_ref) {
|
|
|
16
17
|
var recipient = _ref.recipient,
|
|
17
18
|
rest = _objectWithoutProperties(_ref, _excluded);
|
|
18
19
|
|
|
19
|
-
var client = useClient(); //
|
|
20
|
+
var client = useClient(); // Remember which image url failed to load so we can fall back to the
|
|
21
|
+
// initials. Storing the url (instead of a boolean) means a new url is
|
|
22
|
+
// retried automatically, without an effect to reset the error state.
|
|
23
|
+
|
|
24
|
+
var _useState = useState(null),
|
|
25
|
+
_useState2 = _slicedToArray(_useState, 2),
|
|
26
|
+
erroredSrc = _useState2[0],
|
|
27
|
+
setErroredSrc = _useState2[1]; // There are cases when due to apparent memory leaks, the recipient does not exist
|
|
20
28
|
// This will trigger an unhanded error in the Avatar component and crash the app
|
|
21
29
|
// At the moment, we are not sure where is the root cause of this cascading undefined props
|
|
22
30
|
// Nevertheless, we can prevent the crash by returning null if the recipient is undefined
|
|
23
31
|
|
|
32
|
+
|
|
24
33
|
if (!recipient) {
|
|
25
34
|
// eslint-disable-next-line
|
|
26
35
|
logger.warn('RecipientAvatar: recipient is missing, see props:', _objectSpread({
|
|
@@ -40,11 +49,18 @@ var MemberAvatar = function MemberAvatar(_ref) {
|
|
|
40
49
|
*/
|
|
41
50
|
|
|
42
51
|
|
|
43
|
-
var image = recipient.avatarPath && recipient.status ? "".concat(client.options.uri).concat(recipient.avatarPath, "
|
|
44
|
-
|
|
52
|
+
var image = recipient.avatarPath && recipient.status ? "".concat(client.options.uri).concat(recipient.avatarPath).concat(recipient.avatarPath.includes('?') ? '&' : '?', "v=").concat(recipient.status) : null; // The avatar can become unreachable (eg. the related sharing is revoked
|
|
53
|
+
// while its members are still rendered), in which case the image request
|
|
54
|
+
// fails. Without a fallback the browser shows a broken-image icon, so we
|
|
55
|
+
// render the initials instead.
|
|
56
|
+
|
|
57
|
+
return /*#__PURE__*/React.createElement(Avatar, rest, image && erroredSrc !== image ? /*#__PURE__*/React.createElement("img", {
|
|
45
58
|
width: "100%",
|
|
46
59
|
height: "100%",
|
|
47
|
-
src: image
|
|
60
|
+
src: image,
|
|
61
|
+
onError: function onError() {
|
|
62
|
+
return setErroredSrc(image);
|
|
63
|
+
}
|
|
48
64
|
}) : getInitials(recipient));
|
|
49
65
|
};
|
|
50
66
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { createMockClient } from 'cozy-client';
|
|
4
|
+
import { MemberAvatar } from './MemberAvatar';
|
|
5
|
+
import AppLike from '../../../test/AppLike';
|
|
6
|
+
describe('MemberAvatar', function () {
|
|
7
|
+
var client = createMockClient({});
|
|
8
|
+
client.options = {
|
|
9
|
+
uri: 'http://cozy.example.com'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
var setup = function setup(recipient) {
|
|
13
|
+
return render( /*#__PURE__*/React.createElement(AppLike, {
|
|
14
|
+
client: client
|
|
15
|
+
}, /*#__PURE__*/React.createElement(MemberAvatar, {
|
|
16
|
+
recipient: recipient
|
|
17
|
+
})));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
it('renders nothing when the recipient is missing', function () {
|
|
21
|
+
var _setup = setup(undefined),
|
|
22
|
+
container = _setup.container; // The component returns null; only the provider wrappers remain, with no
|
|
23
|
+
// avatar rendered inside.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
expect(container.querySelector('[class*="MuiAvatar-root"]')).toBeNull();
|
|
27
|
+
expect(container.querySelector('img')).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
it('renders the avatar image when the recipient has an avatarPath and a status', function () {
|
|
30
|
+
var _setup2 = setup({
|
|
31
|
+
public_name: 'Bob',
|
|
32
|
+
status: 'owner',
|
|
33
|
+
avatarPath: '/sharings/123/recipients/0/avatar'
|
|
34
|
+
}),
|
|
35
|
+
container = _setup2.container;
|
|
36
|
+
|
|
37
|
+
var img = container.querySelector('img');
|
|
38
|
+
expect(img).toBeTruthy();
|
|
39
|
+
expect(img.getAttribute('src')).toBe('http://cozy.example.com/sharings/123/recipients/0/avatar?v=owner');
|
|
40
|
+
});
|
|
41
|
+
it('appends the cache-busting param with & when avatarPath already has a query string', function () {
|
|
42
|
+
var _setup3 = setup({
|
|
43
|
+
public_name: 'Bob',
|
|
44
|
+
status: 'owner',
|
|
45
|
+
avatarPath: '/public/avatar?fallback=initials'
|
|
46
|
+
}),
|
|
47
|
+
container = _setup3.container;
|
|
48
|
+
|
|
49
|
+
expect(container.querySelector('img').getAttribute('src')).toBe('http://cozy.example.com/public/avatar?fallback=initials&v=owner');
|
|
50
|
+
});
|
|
51
|
+
it('renders the initials when there is no avatarPath', function () {
|
|
52
|
+
var _setup4 = setup({
|
|
53
|
+
public_name: 'Bob',
|
|
54
|
+
status: 'owner'
|
|
55
|
+
}),
|
|
56
|
+
container = _setup4.container,
|
|
57
|
+
getByText = _setup4.getByText;
|
|
58
|
+
|
|
59
|
+
expect(container.querySelector('img')).toBeNull();
|
|
60
|
+
expect(getByText('B')).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
it('falls back to the initials when the avatar image fails to load', function () {
|
|
63
|
+
var _setup5 = setup({
|
|
64
|
+
public_name: 'Bob',
|
|
65
|
+
status: 'owner',
|
|
66
|
+
avatarPath: '/sharings/123/recipients/0/avatar'
|
|
67
|
+
}),
|
|
68
|
+
container = _setup5.container,
|
|
69
|
+
getByText = _setup5.getByText;
|
|
70
|
+
|
|
71
|
+
var img = container.querySelector('img');
|
|
72
|
+
expect(img).toBeTruthy();
|
|
73
|
+
fireEvent.error(img);
|
|
74
|
+
expect(container.querySelector('img')).toBeNull();
|
|
75
|
+
expect(getByText('B')).toBeTruthy();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -112,17 +112,18 @@ var MemberRecipientPermissions = function MemberRecipientPermissions(_ref) {
|
|
|
112
112
|
return _context2.abrupt("return");
|
|
113
113
|
|
|
114
114
|
case 3:
|
|
115
|
-
|
|
116
|
-
_context2.
|
|
115
|
+
hideMenu();
|
|
116
|
+
_context2.prev = 4;
|
|
117
|
+
_context2.next = 7;
|
|
117
118
|
return updateSharingMemberType(sharingId, memberIndex, newType);
|
|
118
119
|
|
|
119
|
-
case
|
|
120
|
-
_context2.next =
|
|
120
|
+
case 7:
|
|
121
|
+
_context2.next = 13;
|
|
121
122
|
break;
|
|
122
123
|
|
|
123
|
-
case
|
|
124
|
-
_context2.prev =
|
|
125
|
-
_context2.t0 = _context2["catch"](
|
|
124
|
+
case 9:
|
|
125
|
+
_context2.prev = 9;
|
|
126
|
+
_context2.t0 = _context2["catch"](4);
|
|
126
127
|
log.error('Failed to change member permission type', _context2.t0);
|
|
127
128
|
showAlert({
|
|
128
129
|
message: t('Share.members.error.changePermission'),
|
|
@@ -130,14 +131,11 @@ var MemberRecipientPermissions = function MemberRecipientPermissions(_ref) {
|
|
|
130
131
|
variant: 'filled'
|
|
131
132
|
});
|
|
132
133
|
|
|
133
|
-
case 12:
|
|
134
|
-
hideMenu();
|
|
135
|
-
|
|
136
134
|
case 13:
|
|
137
135
|
case "end":
|
|
138
136
|
return _context2.stop();
|
|
139
137
|
}
|
|
140
|
-
}, _callee2, null, [[
|
|
138
|
+
}, _callee2, null, [[4, 9]]);
|
|
141
139
|
}));
|
|
142
140
|
|
|
143
141
|
return function (_x) {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useClient, useQuery, hasQueryBeenLoaded } from 'cozy-client';
|
|
3
3
|
import MemberRecipient from './MemberRecipient';
|
|
4
|
-
import { buildInstanceSettingsQuery } from '../../queries/queries';
|
|
4
|
+
import { buildInstanceSettingsQuery } from '../../queries/queries'; // When the owner is not part of a sharing's members (eg. the folder only has
|
|
5
|
+
// a link), there is no per-sharing avatar to reference. The instance's public
|
|
6
|
+
// avatar still holds the owner's picture. fallback=initials makes the stack
|
|
7
|
+
// serve a generated initials avatar (instead of the app icon) when no picture
|
|
8
|
+
// has been uploaded, matching how avatars are rendered everywhere else.
|
|
9
|
+
|
|
10
|
+
var OWNER_PUBLIC_AVATAR_PATH = '/public/avatar?fallback=initials';
|
|
5
11
|
|
|
6
12
|
var OwnerRecipientDefault = function OwnerRecipientDefault() {
|
|
7
13
|
var _instanceSettingsResu;
|
|
@@ -13,6 +19,7 @@ var OwnerRecipientDefault = function OwnerRecipientDefault() {
|
|
|
13
19
|
isOwner: true,
|
|
14
20
|
status: "owner",
|
|
15
21
|
instance: client.options.uri,
|
|
22
|
+
avatarPath: OWNER_PUBLIC_AVATAR_PATH,
|
|
16
23
|
public_name: instanceSettingsResult === null || instanceSettingsResult === void 0 || (_instanceSettingsResu = instanceSettingsResult.data) === null || _instanceSettingsResu === void 0 || (_instanceSettingsResu = _instanceSettingsResu.attributes) === null || _instanceSettingsResu === void 0 ? void 0 : _instanceSettingsResu.public_name
|
|
17
24
|
});
|
|
18
25
|
};
|
|
@@ -2,7 +2,10 @@ import React from 'react';
|
|
|
2
2
|
import { useClient, useQuery, hasQueryBeenLoaded } from 'cozy-client';
|
|
3
3
|
import MemberRecipientLite from './MemberRecipientLite';
|
|
4
4
|
import withLocales from '../../hoc/withLocales';
|
|
5
|
-
import { buildInstanceSettingsQuery } from '../../queries/queries';
|
|
5
|
+
import { buildInstanceSettingsQuery } from '../../queries/queries'; // See OwnerRecipientDefault: show the instance public avatar (with an initials
|
|
6
|
+
// fallback) when the owner has no per-sharing avatar to reference.
|
|
7
|
+
|
|
8
|
+
var OWNER_PUBLIC_AVATAR_PATH = '/public/avatar?fallback=initials';
|
|
6
9
|
|
|
7
10
|
var OwnerRecipientDefaultLite = function OwnerRecipientDefaultLite() {
|
|
8
11
|
var _instanceSettingsResu;
|
|
@@ -14,6 +17,7 @@ var OwnerRecipientDefaultLite = function OwnerRecipientDefaultLite() {
|
|
|
14
17
|
recipient: {
|
|
15
18
|
status: 'owner',
|
|
16
19
|
instance: client.options.uri,
|
|
20
|
+
avatarPath: OWNER_PUBLIC_AVATAR_PATH,
|
|
17
21
|
public_name: instanceSettingsResult === null || instanceSettingsResult === void 0 || (_instanceSettingsResu = instanceSettingsResult.data) === null || _instanceSettingsResu === void 0 || (_instanceSettingsResu = _instanceSettingsResu.attributes) === null || _instanceSettingsResu === void 0 ? void 0 : _instanceSettingsResu.public_name
|
|
18
22
|
},
|
|
19
23
|
isOwner: true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozy-sharing",
|
|
3
|
-
"version": "33.4.
|
|
3
|
+
"version": "33.4.2",
|
|
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": "
|
|
86
|
+
"gitHead": "ce6730dfc4d047382e31c566f7e066ad9d7eb195"
|
|
87
87
|
}
|
package/src/SharingProvider.jsx
CHANGED
|
@@ -358,6 +358,24 @@ export class SharingProvider extends Component {
|
|
|
358
358
|
if (newType !== 'one-way' && newType !== 'two-way') {
|
|
359
359
|
throw new Error('Unsupported sharing type: ' + newType)
|
|
360
360
|
}
|
|
361
|
+
|
|
362
|
+
const optimisticSharing = {
|
|
363
|
+
...sharing,
|
|
364
|
+
attributes: {
|
|
365
|
+
...sharing.attributes,
|
|
366
|
+
members: sharing.attributes.members.map((member, index) =>
|
|
367
|
+
index === memberIndex
|
|
368
|
+
? {
|
|
369
|
+
...member,
|
|
370
|
+
read_only: makeReadOnly
|
|
371
|
+
}
|
|
372
|
+
: member
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.dispatch(updateSharing(optimisticSharing))
|
|
378
|
+
|
|
361
379
|
try {
|
|
362
380
|
if (makeReadOnly) {
|
|
363
381
|
await this.sharingCol.setReadOnly(sharing, memberIndex)
|
|
@@ -365,6 +383,23 @@ export class SharingProvider extends Component {
|
|
|
365
383
|
await this.sharingCol.setReadWrite(sharing, memberIndex)
|
|
366
384
|
}
|
|
367
385
|
} catch (error) {
|
|
386
|
+
const currentSharing = getSharingById(this.state, sharingId) || sharing
|
|
387
|
+
const rollbackSharing = {
|
|
388
|
+
...currentSharing,
|
|
389
|
+
attributes: {
|
|
390
|
+
...currentSharing.attributes,
|
|
391
|
+
members: currentSharing.attributes.members.map((member, index) =>
|
|
392
|
+
index === memberIndex
|
|
393
|
+
? {
|
|
394
|
+
...member,
|
|
395
|
+
read_only: !makeReadOnly
|
|
396
|
+
}
|
|
397
|
+
: member
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
this.dispatch(updateSharing(rollbackSharing))
|
|
368
403
|
log.error(
|
|
369
404
|
`Failed to change member ${member.email} permission type to ${newType}`,
|
|
370
405
|
error
|
|
@@ -524,7 +524,9 @@ describe('updateSharingMemberType', () => {
|
|
|
524
524
|
setReadOnly: jest.fn().mockResolvedValue({}),
|
|
525
525
|
setReadWrite: jest.fn().mockResolvedValue({})
|
|
526
526
|
}
|
|
527
|
-
jest.
|
|
527
|
+
instance.dispatch = jest.fn(action => {
|
|
528
|
+
instance.state = { ...instance.state, ...reducer(instance.state, action) }
|
|
529
|
+
})
|
|
528
530
|
})
|
|
529
531
|
|
|
530
532
|
it('should throw when sharing is not found', async () => {
|
|
@@ -556,16 +558,46 @@ describe('updateSharingMemberType', () => {
|
|
|
556
558
|
expect(instance.sharingCol.setReadOnly).not.toHaveBeenCalled()
|
|
557
559
|
})
|
|
558
560
|
|
|
559
|
-
it('should
|
|
561
|
+
it('should optimistically dispatch a state update when switching to one-way', async () => {
|
|
560
562
|
await instance.updateSharingMemberType('sharing-123', 1, 'one-way')
|
|
561
563
|
|
|
562
|
-
expect(instance.
|
|
564
|
+
expect(instance.dispatch).toHaveBeenCalledWith({
|
|
565
|
+
type: 'UPDATE_SHARING',
|
|
566
|
+
sharing: {
|
|
567
|
+
...mockSharing,
|
|
568
|
+
attributes: {
|
|
569
|
+
...mockSharing.attributes,
|
|
570
|
+
members: [
|
|
571
|
+
mockSharing.attributes.members[0],
|
|
572
|
+
{
|
|
573
|
+
...mockSharing.attributes.members[1],
|
|
574
|
+
read_only: true
|
|
575
|
+
}
|
|
576
|
+
]
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
})
|
|
563
580
|
})
|
|
564
581
|
|
|
565
|
-
it('should
|
|
582
|
+
it('should optimistically dispatch a state update when switching to two-way', async () => {
|
|
566
583
|
await instance.updateSharingMemberType('sharing-123', 1, 'two-way')
|
|
567
584
|
|
|
568
|
-
expect(instance.
|
|
585
|
+
expect(instance.dispatch).toHaveBeenCalledWith({
|
|
586
|
+
type: 'UPDATE_SHARING',
|
|
587
|
+
sharing: {
|
|
588
|
+
...mockSharing,
|
|
589
|
+
attributes: {
|
|
590
|
+
...mockSharing.attributes,
|
|
591
|
+
members: [
|
|
592
|
+
mockSharing.attributes.members[0],
|
|
593
|
+
{
|
|
594
|
+
...mockSharing.attributes.members[1],
|
|
595
|
+
read_only: false
|
|
596
|
+
}
|
|
597
|
+
]
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
})
|
|
569
601
|
})
|
|
570
602
|
|
|
571
603
|
it('should rethrow error if setReadOnly fails', async () => {
|
|
@@ -588,7 +620,7 @@ describe('updateSharingMemberType', () => {
|
|
|
588
620
|
).rejects.toThrow('Network error')
|
|
589
621
|
})
|
|
590
622
|
|
|
591
|
-
it('should
|
|
623
|
+
it('should rollback optimistic state update if API call fails', async () => {
|
|
592
624
|
instance.sharingCol.setReadOnly.mockRejectedValue(
|
|
593
625
|
new Error('Network error')
|
|
594
626
|
)
|
|
@@ -597,7 +629,89 @@ describe('updateSharingMemberType', () => {
|
|
|
597
629
|
instance.updateSharingMemberType('sharing-123', 1, 'one-way')
|
|
598
630
|
).rejects.toThrow('Network error')
|
|
599
631
|
|
|
600
|
-
expect(instance.
|
|
632
|
+
expect(instance.dispatch).toHaveBeenNthCalledWith(1, {
|
|
633
|
+
type: 'UPDATE_SHARING',
|
|
634
|
+
sharing: {
|
|
635
|
+
...mockSharing,
|
|
636
|
+
attributes: {
|
|
637
|
+
...mockSharing.attributes,
|
|
638
|
+
members: [
|
|
639
|
+
mockSharing.attributes.members[0],
|
|
640
|
+
{
|
|
641
|
+
...mockSharing.attributes.members[1],
|
|
642
|
+
read_only: true
|
|
643
|
+
}
|
|
644
|
+
]
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
})
|
|
648
|
+
expect(instance.dispatch).toHaveBeenNthCalledWith(2, {
|
|
649
|
+
type: 'UPDATE_SHARING',
|
|
650
|
+
sharing: {
|
|
651
|
+
...mockSharing,
|
|
652
|
+
attributes: {
|
|
653
|
+
...mockSharing.attributes,
|
|
654
|
+
members: [
|
|
655
|
+
mockSharing.attributes.members[0],
|
|
656
|
+
{
|
|
657
|
+
...mockSharing.attributes.members[1],
|
|
658
|
+
read_only: false
|
|
659
|
+
}
|
|
660
|
+
]
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
})
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
it('should preserve realtime changes when rolling back an API failure', async () => {
|
|
667
|
+
const realtimeSharing = {
|
|
668
|
+
...mockSharing,
|
|
669
|
+
attributes: {
|
|
670
|
+
...mockSharing.attributes,
|
|
671
|
+
description: 'updated by realtime',
|
|
672
|
+
members: [
|
|
673
|
+
{
|
|
674
|
+
...mockSharing.attributes.members[0],
|
|
675
|
+
status: 'seen'
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
...mockSharing.attributes.members[1],
|
|
679
|
+
read_only: true
|
|
680
|
+
}
|
|
681
|
+
]
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
instance.sharingCol.setReadOnly.mockImplementation(async () => {
|
|
686
|
+
instance.state = {
|
|
687
|
+
...instance.state,
|
|
688
|
+
sharings: instance.state.sharings.map(sharing =>
|
|
689
|
+
sharing.id === realtimeSharing.id ? realtimeSharing : sharing
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
throw new Error('Network error')
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
await expect(
|
|
696
|
+
instance.updateSharingMemberType('sharing-123', 1, 'one-way')
|
|
697
|
+
).rejects.toThrow('Network error')
|
|
698
|
+
|
|
699
|
+
expect(instance.dispatch).toHaveBeenNthCalledWith(2, {
|
|
700
|
+
type: 'UPDATE_SHARING',
|
|
701
|
+
sharing: {
|
|
702
|
+
...realtimeSharing,
|
|
703
|
+
attributes: {
|
|
704
|
+
...realtimeSharing.attributes,
|
|
705
|
+
members: [
|
|
706
|
+
realtimeSharing.attributes.members[0],
|
|
707
|
+
{
|
|
708
|
+
...realtimeSharing.attributes.members[1],
|
|
709
|
+
read_only: false
|
|
710
|
+
}
|
|
711
|
+
]
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
})
|
|
601
715
|
})
|
|
602
716
|
})
|
|
603
717
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useClient } from 'cozy-client'
|
|
4
4
|
import Avatar from 'cozy-ui/transpiled/react/Avatar'
|
|
@@ -8,6 +8,10 @@ import { getInitials } from '../../models'
|
|
|
8
8
|
|
|
9
9
|
const MemberAvatar = ({ recipient, ...rest }) => {
|
|
10
10
|
const client = useClient()
|
|
11
|
+
// Remember which image url failed to load so we can fall back to the
|
|
12
|
+
// initials. Storing the url (instead of a boolean) means a new url is
|
|
13
|
+
// retried automatically, without an effect to reset the error state.
|
|
14
|
+
const [erroredSrc, setErroredSrc] = useState(null)
|
|
11
15
|
|
|
12
16
|
// There are cases when due to apparent memory leaks, the recipient does not exist
|
|
13
17
|
// This will trigger an unhanded error in the Avatar component and crash the app
|
|
@@ -31,13 +35,24 @@ const MemberAvatar = ({ recipient, ...rest }) => {
|
|
|
31
35
|
*/
|
|
32
36
|
const image =
|
|
33
37
|
recipient.avatarPath && recipient.status
|
|
34
|
-
? `${client.options.uri}${recipient.avatarPath}
|
|
38
|
+
? `${client.options.uri}${recipient.avatarPath}${
|
|
39
|
+
recipient.avatarPath.includes('?') ? '&' : '?'
|
|
40
|
+
}v=${recipient.status}`
|
|
35
41
|
: null
|
|
36
42
|
|
|
43
|
+
// The avatar can become unreachable (eg. the related sharing is revoked
|
|
44
|
+
// while its members are still rendered), in which case the image request
|
|
45
|
+
// fails. Without a fallback the browser shows a broken-image icon, so we
|
|
46
|
+
// render the initials instead.
|
|
37
47
|
return (
|
|
38
48
|
<Avatar {...rest}>
|
|
39
|
-
{image ? (
|
|
40
|
-
<img
|
|
49
|
+
{image && erroredSrc !== image ? (
|
|
50
|
+
<img
|
|
51
|
+
width="100%"
|
|
52
|
+
height="100%"
|
|
53
|
+
src={image}
|
|
54
|
+
onError={() => setErroredSrc(image)}
|
|
55
|
+
/>
|
|
41
56
|
) : (
|
|
42
57
|
getInitials(recipient)
|
|
43
58
|
)}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { createMockClient } from 'cozy-client'
|
|
5
|
+
|
|
6
|
+
import { MemberAvatar } from './MemberAvatar'
|
|
7
|
+
import AppLike from '../../../test/AppLike'
|
|
8
|
+
|
|
9
|
+
describe('MemberAvatar', () => {
|
|
10
|
+
const client = createMockClient({})
|
|
11
|
+
client.options = { uri: 'http://cozy.example.com' }
|
|
12
|
+
|
|
13
|
+
const setup = recipient =>
|
|
14
|
+
render(
|
|
15
|
+
<AppLike client={client}>
|
|
16
|
+
<MemberAvatar recipient={recipient} />
|
|
17
|
+
</AppLike>
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
it('renders nothing when the recipient is missing', () => {
|
|
21
|
+
const { container } = setup(undefined)
|
|
22
|
+
|
|
23
|
+
// The component returns null; only the provider wrappers remain, with no
|
|
24
|
+
// avatar rendered inside.
|
|
25
|
+
expect(container.querySelector('[class*="MuiAvatar-root"]')).toBeNull()
|
|
26
|
+
expect(container.querySelector('img')).toBeNull()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('renders the avatar image when the recipient has an avatarPath and a status', () => {
|
|
30
|
+
const { container } = setup({
|
|
31
|
+
public_name: 'Bob',
|
|
32
|
+
status: 'owner',
|
|
33
|
+
avatarPath: '/sharings/123/recipients/0/avatar'
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const img = container.querySelector('img')
|
|
37
|
+
expect(img).toBeTruthy()
|
|
38
|
+
expect(img.getAttribute('src')).toBe(
|
|
39
|
+
'http://cozy.example.com/sharings/123/recipients/0/avatar?v=owner'
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('appends the cache-busting param with & when avatarPath already has a query string', () => {
|
|
44
|
+
const { container } = setup({
|
|
45
|
+
public_name: 'Bob',
|
|
46
|
+
status: 'owner',
|
|
47
|
+
avatarPath: '/public/avatar?fallback=initials'
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
expect(container.querySelector('img').getAttribute('src')).toBe(
|
|
51
|
+
'http://cozy.example.com/public/avatar?fallback=initials&v=owner'
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('renders the initials when there is no avatarPath', () => {
|
|
56
|
+
const { container, getByText } = setup({
|
|
57
|
+
public_name: 'Bob',
|
|
58
|
+
status: 'owner'
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(container.querySelector('img')).toBeNull()
|
|
62
|
+
expect(getByText('B')).toBeTruthy()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('falls back to the initials when the avatar image fails to load', () => {
|
|
66
|
+
const { container, getByText } = setup({
|
|
67
|
+
public_name: 'Bob',
|
|
68
|
+
status: 'owner',
|
|
69
|
+
avatarPath: '/sharings/123/recipients/0/avatar'
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const img = container.querySelector('img')
|
|
73
|
+
expect(img).toBeTruthy()
|
|
74
|
+
|
|
75
|
+
fireEvent.error(img)
|
|
76
|
+
|
|
77
|
+
expect(container.querySelector('img')).toBeNull()
|
|
78
|
+
expect(getByText('B')).toBeTruthy()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -74,6 +74,7 @@ const MemberRecipientPermissions = ({
|
|
|
74
74
|
hideMenu()
|
|
75
75
|
return
|
|
76
76
|
}
|
|
77
|
+
hideMenu()
|
|
77
78
|
try {
|
|
78
79
|
await updateSharingMemberType(sharingId, memberIndex, newType)
|
|
79
80
|
} catch (error) {
|
|
@@ -84,7 +85,6 @@ const MemberRecipientPermissions = ({
|
|
|
84
85
|
variant: 'filled'
|
|
85
86
|
})
|
|
86
87
|
}
|
|
87
|
-
hideMenu()
|
|
88
88
|
},
|
|
89
89
|
[
|
|
90
90
|
hideMenu,
|
|
@@ -5,6 +5,13 @@ import { useClient, useQuery, hasQueryBeenLoaded } from 'cozy-client'
|
|
|
5
5
|
import MemberRecipient from './MemberRecipient'
|
|
6
6
|
import { buildInstanceSettingsQuery } from '../../queries/queries'
|
|
7
7
|
|
|
8
|
+
// When the owner is not part of a sharing's members (eg. the folder only has
|
|
9
|
+
// a link), there is no per-sharing avatar to reference. The instance's public
|
|
10
|
+
// avatar still holds the owner's picture. fallback=initials makes the stack
|
|
11
|
+
// serve a generated initials avatar (instead of the app icon) when no picture
|
|
12
|
+
// has been uploaded, matching how avatars are rendered everywhere else.
|
|
13
|
+
const OWNER_PUBLIC_AVATAR_PATH = '/public/avatar?fallback=initials'
|
|
14
|
+
|
|
8
15
|
const OwnerRecipientDefault = () => {
|
|
9
16
|
const client = useClient()
|
|
10
17
|
|
|
@@ -20,6 +27,7 @@ const OwnerRecipientDefault = () => {
|
|
|
20
27
|
isOwner={true}
|
|
21
28
|
status="owner"
|
|
22
29
|
instance={client.options.uri}
|
|
30
|
+
avatarPath={OWNER_PUBLIC_AVATAR_PATH}
|
|
23
31
|
public_name={instanceSettingsResult?.data?.attributes?.public_name}
|
|
24
32
|
/>
|
|
25
33
|
)
|
|
@@ -6,6 +6,10 @@ import MemberRecipientLite from './MemberRecipientLite'
|
|
|
6
6
|
import withLocales from '../../hoc/withLocales'
|
|
7
7
|
import { buildInstanceSettingsQuery } from '../../queries/queries'
|
|
8
8
|
|
|
9
|
+
// See OwnerRecipientDefault: show the instance public avatar (with an initials
|
|
10
|
+
// fallback) when the owner has no per-sharing avatar to reference.
|
|
11
|
+
const OWNER_PUBLIC_AVATAR_PATH = '/public/avatar?fallback=initials'
|
|
12
|
+
|
|
9
13
|
const OwnerRecipientDefaultLite = () => {
|
|
10
14
|
const client = useClient()
|
|
11
15
|
|
|
@@ -21,6 +25,7 @@ const OwnerRecipientDefaultLite = () => {
|
|
|
21
25
|
recipient={{
|
|
22
26
|
status: 'owner',
|
|
23
27
|
instance: client.options.uri,
|
|
28
|
+
avatarPath: OWNER_PUBLIC_AVATAR_PATH,
|
|
24
29
|
public_name: instanceSettingsResult?.data?.attributes?.public_name
|
|
25
30
|
}}
|
|
26
31
|
isOwner={true}
|