cozy-sharing 37.6.1 → 37.7.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 +14 -0
- package/README.md +1 -0
- package/dist/components/Identity/ExtraIdentity.js +6 -6
- package/dist/components/Recipient/DowngradePermissionConfirmDialog.js +127 -0
- package/dist/components/Recipient/MemberRecipient.js +6 -1
- package/dist/components/Recipient/MemberRecipientPermissions.js +7 -2
- package/dist/components/Recipient/PermissionTypeMenu.js +52 -19
- package/dist/components/Recipient/PermissionTypeMenu.spec.js +110 -1
- package/dist/components/Tooltip.js +18 -58
- package/dist/hooks/useFetchDocumentPath.js +19 -10
- package/dist/state.js +1 -1
- package/locales/en.json +6 -0
- package/locales/fr.json +6 -0
- package/locales/ru.json +6 -0
- package/locales/vi.json +6 -0
- package/package.json +4 -4
- package/src/components/Identity/ExtraIdentity.jsx +17 -16
- package/src/components/Recipient/DowngradePermissionConfirmDialog.jsx +146 -0
- package/src/components/Recipient/MemberRecipient.jsx +6 -1
- package/src/components/Recipient/MemberRecipientPermissions.jsx +4 -1
- package/src/components/Recipient/PermissionTypeMenu.jsx +63 -23
- package/src/components/Recipient/PermissionTypeMenu.spec.jsx +85 -1
- package/src/components/Tooltip.jsx +18 -22
- package/src/hooks/useFetchDocumentPath.jsx +3 -1
- package/src/state.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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
|
+
# [37.7.0](https://github.com/cozy/cozy-libs/compare/cozy-sharing@37.6.2...cozy-sharing@37.7.0) (2026-09-15)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **cozy-sharing:** Stop getSharedParentPath self-matching ([54c6bfe](https://github.com/cozy/cozy-libs/commit/54c6bfe7874ebdfd8174905e749e9a0b584457a1))
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- **cozy-sharing:** Confirm downgrade affecting a shared parent folder ([7a57bd9](https://github.com/cozy/cozy-libs/commit/7a57bd9b34ae93a0a394e69fc3f272a3ca4969d6))
|
|
15
|
+
|
|
16
|
+
## [37.6.2](https://github.com/cozy/cozy-libs/compare/cozy-sharing@37.6.1...cozy-sharing@37.6.2) (2026-09-14)
|
|
17
|
+
|
|
18
|
+
**Note:** Version bump only for package cozy-sharing
|
|
19
|
+
|
|
6
20
|
## [37.6.1](https://github.com/cozy/cozy-libs/compare/cozy-sharing@37.6.0...cozy-sharing@37.6.1) (2026-09-10)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package cozy-sharing
|
package/README.md
CHANGED
|
@@ -178,6 +178,7 @@ Link and email sharing can coexist on the same resource. The recipient's permiss
|
|
|
178
178
|
| Read-only sharing | If the existing sharing is read-only, only the `readOnly` option is offered | `ShareByEmail.jsx:46-57` |
|
|
179
179
|
| Contacts shown | Only contacts with a defined `email` or `cozy` URL | `helpers/recipients.js:72-73` |
|
|
180
180
|
| Group sharing | All members get the same permission; dynamic membership (add/remove) propagates | `state.js:414-451` |
|
|
181
|
+
| Downgrade confirmation | Switching a member from `two-way` (editor) to `one-way` (viewer) on a folder with a shared parent first opens `DowngradePermissionConfirmDialog`: the change is applied only on confirm, cancel keeps the editor permission (the downgrade also reduces the member's rights on the parent folder) | `Recipient/PermissionTypeMenu.jsx`, `Recipient/DowngradePermissionConfirmDialog.jsx` |
|
|
181
182
|
|
|
182
183
|
### Shared drives & federated folders
|
|
183
184
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import PropTypes from 'prop-types';
|
|
2
1
|
import React from 'react';
|
|
2
|
+
import { useI18n } from 'twake-i18n';
|
|
3
3
|
import Identity from './Identity';
|
|
4
4
|
import { getDisplayName } from '../../models';
|
|
5
5
|
var styles = {
|
|
@@ -17,9 +17,12 @@ var styles = {
|
|
|
17
17
|
};
|
|
18
18
|
import { ExtraAvatar } from '../Avatar/ExtraAvatar';
|
|
19
19
|
|
|
20
|
-
var ExtraIdentity = function ExtraIdentity(_ref
|
|
20
|
+
var ExtraIdentity = function ExtraIdentity(_ref) {
|
|
21
21
|
var extraRecipients = _ref.extraRecipients;
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
var _useI18n = useI18n(),
|
|
24
|
+
t = _useI18n.t;
|
|
25
|
+
|
|
23
26
|
return /*#__PURE__*/React.createElement("div", {
|
|
24
27
|
className: styles['recipient']
|
|
25
28
|
}, /*#__PURE__*/React.createElement(ExtraAvatar, {
|
|
@@ -33,7 +36,4 @@ var ExtraIdentity = function ExtraIdentity(_ref, _ref2) {
|
|
|
33
36
|
})));
|
|
34
37
|
};
|
|
35
38
|
|
|
36
|
-
ExtraIdentity.contextTypes = {
|
|
37
|
-
t: PropTypes.func.isRequired
|
|
38
|
-
};
|
|
39
39
|
export { ExtraIdentity };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import snarkdown from 'snarkdown';
|
|
5
|
+
import { Icon, FolderOutlined, ArrowUp } from '@linagora/twake-icons';
|
|
6
|
+
import { useClient } from 'cozy-client';
|
|
7
|
+
import Button from 'cozy-ui/transpiled/react/Buttons';
|
|
8
|
+
import { ConfirmDialog } from 'cozy-ui/transpiled/react/CozyDialogs';
|
|
9
|
+
import Timeline from 'cozy-ui/transpiled/react/Timeline';
|
|
10
|
+
import TimelineConnector from 'cozy-ui/transpiled/react/TimelineConnector';
|
|
11
|
+
import TimelineContent from 'cozy-ui/transpiled/react/TimelineContent';
|
|
12
|
+
import TimelineDot from 'cozy-ui/transpiled/react/TimelineDot';
|
|
13
|
+
import TimelineItem from 'cozy-ui/transpiled/react/TimelineItem';
|
|
14
|
+
import TimelineOppositeContent from 'cozy-ui/transpiled/react/TimelineOppositeContent';
|
|
15
|
+
import TimelineSeparator from 'cozy-ui/transpiled/react/TimelineSeparator';
|
|
16
|
+
import Typography from 'cozy-ui/transpiled/react/Typography';
|
|
17
|
+
import { useI18n } from 'twake-i18n';
|
|
18
|
+
import { DEFAULT_DISPLAY_NAME } from '../../helpers/recipients';
|
|
19
|
+
import { useFetchDocumentPath } from '../../hooks/useFetchDocumentPath';
|
|
20
|
+
import { useSharingContext } from '../../hooks/useSharingContext';
|
|
21
|
+
import { getDisplayName } from '../../models';
|
|
22
|
+
|
|
23
|
+
var DowngradePermissionConfirmDialog = function DowngradePermissionConfirmDialog(_ref) {
|
|
24
|
+
var document = _ref.document,
|
|
25
|
+
recipient = _ref.recipient,
|
|
26
|
+
onCancel = _ref.onCancel,
|
|
27
|
+
onConfirm = _ref.onConfirm;
|
|
28
|
+
|
|
29
|
+
var _useI18n = useI18n(),
|
|
30
|
+
t = _useI18n.t;
|
|
31
|
+
|
|
32
|
+
var client = useClient();
|
|
33
|
+
|
|
34
|
+
var _useSharingContext = useSharingContext(),
|
|
35
|
+
getSharedParentPath = _useSharingContext.getSharedParentPath;
|
|
36
|
+
|
|
37
|
+
var documentPath = useFetchDocumentPath(client, document);
|
|
38
|
+
var sharedParentPath = documentPath && getSharedParentPath(documentPath);
|
|
39
|
+
var parentFolderName = sharedParentPath === null || sharedParentPath === void 0 ? void 0 : sharedParentPath.split('/').pop();
|
|
40
|
+
var contactName = getDisplayName(recipient, t(DEFAULT_DISPLAY_NAME));
|
|
41
|
+
|
|
42
|
+
var _snarkdown$split = snarkdown(t('DowngradePermissionConfirmDialog.content', {
|
|
43
|
+
contactName: contactName
|
|
44
|
+
})).split('<strong>'),
|
|
45
|
+
_snarkdown$split2 = _slicedToArray(_snarkdown$split, 2),
|
|
46
|
+
before = _snarkdown$split2[0],
|
|
47
|
+
afterOpen = _snarkdown$split2[1];
|
|
48
|
+
|
|
49
|
+
var _afterOpen$split = afterOpen.split('</strong>'),
|
|
50
|
+
_afterOpen$split2 = _slicedToArray(_afterOpen$split, 2),
|
|
51
|
+
boldContactName = _afterOpen$split2[0],
|
|
52
|
+
after = _afterOpen$split2[1];
|
|
53
|
+
|
|
54
|
+
return /*#__PURE__*/React.createElement(ConfirmDialog, {
|
|
55
|
+
open: true,
|
|
56
|
+
title: t('DowngradePermissionConfirmDialog.title'),
|
|
57
|
+
content: /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Typography, null, before, /*#__PURE__*/React.createElement("strong", null, boldContactName), after), /*#__PURE__*/React.createElement(Timeline, null, /*#__PURE__*/React.createElement(TimelineItem, null, /*#__PURE__*/React.createElement(TimelineOppositeContent, {
|
|
58
|
+
style: {
|
|
59
|
+
display: 'none'
|
|
60
|
+
}
|
|
61
|
+
}), /*#__PURE__*/React.createElement(TimelineSeparator, null, /*#__PURE__*/React.createElement(TimelineDot, {
|
|
62
|
+
className: "u-bg-transparent u-bdw-0 u-elevation-0 u-p-0"
|
|
63
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
64
|
+
className: "u-w-1-half u-flex u-flex-justify-center"
|
|
65
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
66
|
+
icon: FolderOutlined,
|
|
67
|
+
size: 24
|
|
68
|
+
}))), /*#__PURE__*/React.createElement(TimelineConnector, null)), /*#__PURE__*/React.createElement(TimelineContent, null, /*#__PURE__*/React.createElement(Typography, {
|
|
69
|
+
variant: "body2"
|
|
70
|
+
}, parentFolderName), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("s", null, /*#__PURE__*/React.createElement(Typography, {
|
|
71
|
+
display: "inline",
|
|
72
|
+
variant: "caption"
|
|
73
|
+
}, t('Share.type.two-way'))), /*#__PURE__*/React.createElement(Icon, {
|
|
74
|
+
className: "u-mh-half",
|
|
75
|
+
color: "var(--secondaryTextColor)",
|
|
76
|
+
icon: ArrowUp,
|
|
77
|
+
rotate: 90,
|
|
78
|
+
size: 12
|
|
79
|
+
}), /*#__PURE__*/React.createElement(Typography, {
|
|
80
|
+
display: "inline",
|
|
81
|
+
variant: "caption"
|
|
82
|
+
}, t('Share.type.one-way'))))), /*#__PURE__*/React.createElement(TimelineItem, null, /*#__PURE__*/React.createElement(TimelineOppositeContent, {
|
|
83
|
+
style: {
|
|
84
|
+
display: 'none'
|
|
85
|
+
}
|
|
86
|
+
}), /*#__PURE__*/React.createElement(TimelineSeparator, null, /*#__PURE__*/React.createElement(TimelineDot, {
|
|
87
|
+
className: "u-bg-transparent u-bdw-0 u-elevation-0 u-p-0"
|
|
88
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
89
|
+
className: "u-w-1-half u-flex u-flex-justify-center"
|
|
90
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
91
|
+
icon: FolderOutlined,
|
|
92
|
+
size: 16
|
|
93
|
+
})))), /*#__PURE__*/React.createElement(TimelineContent, null, /*#__PURE__*/React.createElement(Typography, {
|
|
94
|
+
variant: "body2"
|
|
95
|
+
}, document.name), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("s", null, /*#__PURE__*/React.createElement(Typography, {
|
|
96
|
+
display: "inline",
|
|
97
|
+
variant: "caption"
|
|
98
|
+
}, t('Share.type.two-way'))), /*#__PURE__*/React.createElement(Icon, {
|
|
99
|
+
className: "u-mh-half",
|
|
100
|
+
color: "var(--secondaryTextColor)",
|
|
101
|
+
icon: ArrowUp,
|
|
102
|
+
rotate: 90,
|
|
103
|
+
size: 12
|
|
104
|
+
}), /*#__PURE__*/React.createElement(Typography, {
|
|
105
|
+
display: "inline",
|
|
106
|
+
variant: "caption"
|
|
107
|
+
}, t('Share.type.one-way'))))))),
|
|
108
|
+
actions: /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Button, {
|
|
109
|
+
variant: "secondary",
|
|
110
|
+
label: t('DowngradePermissionConfirmDialog.cancel'),
|
|
111
|
+
onClick: onCancel
|
|
112
|
+
}), /*#__PURE__*/React.createElement(Button, {
|
|
113
|
+
variant: "primary",
|
|
114
|
+
label: t('DowngradePermissionConfirmDialog.confirm'),
|
|
115
|
+
onClick: onConfirm
|
|
116
|
+
})),
|
|
117
|
+
onClose: onCancel
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
DowngradePermissionConfirmDialog.propTypes = {
|
|
122
|
+
document: PropTypes.object.isRequired,
|
|
123
|
+
recipient: PropTypes.object.isRequired,
|
|
124
|
+
onCancel: PropTypes.func.isRequired,
|
|
125
|
+
onConfirm: PropTypes.func.isRequired
|
|
126
|
+
};
|
|
127
|
+
export default DowngradePermissionConfirmDialog;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import _extends from "@babel/runtime/helpers/extends";
|
|
1
2
|
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
|
|
2
3
|
var _excluded = ["instance", "isOwner", "status", "recipientConfirmationData", "verifyRecipient", "fadeIn", "isOrgSharedDrive"];
|
|
3
4
|
import PropTypes from 'prop-types';
|
|
@@ -41,7 +42,11 @@ var MemberRecipient = function MemberRecipient(props) {
|
|
|
41
42
|
var RightPart = recipientConfirmationData ? /*#__PURE__*/React.createElement(RecipientConfirm, {
|
|
42
43
|
recipientConfirmationData: recipientConfirmationData,
|
|
43
44
|
verifyRecipient: verifyRecipient
|
|
44
|
-
}) : /*#__PURE__*/React.createElement(MemberRecipientPermissions,
|
|
45
|
+
}) : /*#__PURE__*/React.createElement(MemberRecipientPermissions, _extends({}, rest, {
|
|
46
|
+
instance: instance,
|
|
47
|
+
isOwner: isOwner,
|
|
48
|
+
status: status
|
|
49
|
+
}));
|
|
45
50
|
return /*#__PURE__*/React.createElement(Fade, {
|
|
46
51
|
in: true,
|
|
47
52
|
timeout: fadeIn ? FADE_IN_DURATION : 0
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
|
|
2
2
|
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
3
|
+
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
|
|
4
|
+
var _excluded = ["isOwner", "canManageSharing", "canManageMembers", "status", "instance", "type", "document", "onRevoke", "onRevokeSelf", "sharingId", "memberIndex"];
|
|
3
5
|
import _regeneratorRuntime from "@babel/runtime/regenerator";
|
|
4
6
|
import React, { useState, useCallback } from 'react';
|
|
5
7
|
import { Icon, CrossCircleOutline } from '@linagora/twake-icons';
|
|
@@ -23,7 +25,8 @@ var MemberRecipientPermissions = function MemberRecipientPermissions(_ref) {
|
|
|
23
25
|
onRevoke = _ref.onRevoke,
|
|
24
26
|
onRevokeSelf = _ref.onRevokeSelf,
|
|
25
27
|
sharingId = _ref.sharingId,
|
|
26
|
-
memberIndex = _ref.memberIndex
|
|
28
|
+
memberIndex = _ref.memberIndex,
|
|
29
|
+
recipient = _objectWithoutProperties(_ref, _excluded);
|
|
27
30
|
|
|
28
31
|
var _useI18n = useI18n(),
|
|
29
32
|
t = _useI18n.t;
|
|
@@ -77,7 +80,9 @@ var MemberRecipientPermissions = function MemberRecipientPermissions(_ref) {
|
|
|
77
80
|
}, contactIsOwner ? t("Share.status.".concat(status)) : t("Share.type.".concat(type))), shouldShowMenu && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(PermissionTypeMenu, {
|
|
78
81
|
sharingId: sharingId,
|
|
79
82
|
memberIndex: memberIndex,
|
|
80
|
-
type: type !== null && type !== void 0 ? type : 'one-way'
|
|
83
|
+
type: type !== null && type !== void 0 ? type : 'one-way',
|
|
84
|
+
document: document,
|
|
85
|
+
recipient: recipient
|
|
81
86
|
}), /*#__PURE__*/React.createElement(IconButton, {
|
|
82
87
|
onClick: handleRevocation,
|
|
83
88
|
size: "small",
|
|
@@ -2,29 +2,36 @@ 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
4
|
import React, { useCallback, useRef, useState } from 'react';
|
|
5
|
+
import { useClient } from 'cozy-client';
|
|
5
6
|
import minilog from 'cozy-minilog';
|
|
6
7
|
import ActionsMenu from 'cozy-ui/transpiled/react/ActionsMenu';
|
|
7
8
|
import { makeActions } from 'cozy-ui/transpiled/react/ActionsMenu/Actions';
|
|
8
9
|
import DropdownButton from 'cozy-ui/transpiled/react/DropdownButton';
|
|
9
10
|
import { useAlert } from 'cozy-ui/transpiled/react/providers/Alert';
|
|
10
11
|
import { useI18n } from 'twake-i18n';
|
|
12
|
+
import DowngradePermissionConfirmDialog from './DowngradePermissionConfirmDialog';
|
|
11
13
|
import { setReadOnlySharedPermission } from './actions/setReadOnlySharedPermission';
|
|
12
14
|
import { setReadWriteSharedPermission } from './actions/setReadWriteSharedPermission';
|
|
13
15
|
import withLocales from '../../hoc/withLocales';
|
|
16
|
+
import { useFetchDocumentPath } from '../../hooks/useFetchDocumentPath';
|
|
14
17
|
import { useSharingContext } from '../../hooks/useSharingContext';
|
|
15
18
|
var log = minilog('PermissionTypeMenu');
|
|
16
19
|
|
|
17
20
|
var PermissionTypeMenuComponent = function PermissionTypeMenuComponent(_ref) {
|
|
18
21
|
var sharingId = _ref.sharingId,
|
|
19
22
|
memberIndex = _ref.memberIndex,
|
|
20
|
-
type = _ref.type
|
|
23
|
+
type = _ref.type,
|
|
24
|
+
document = _ref.document,
|
|
25
|
+
recipient = _ref.recipient;
|
|
21
26
|
|
|
22
27
|
var _useI18n = useI18n(),
|
|
23
28
|
t = _useI18n.t;
|
|
24
29
|
|
|
30
|
+
var client = useClient();
|
|
25
31
|
var buttonRef = useRef();
|
|
26
32
|
|
|
27
33
|
var _useSharingContext = useSharingContext(),
|
|
34
|
+
hasSharedParent = _useSharingContext.hasSharedParent,
|
|
28
35
|
updateSharingMemberType = _useSharingContext.updateSharingMemberType;
|
|
29
36
|
|
|
30
37
|
var _useAlert = useAlert(),
|
|
@@ -35,32 +42,31 @@ var PermissionTypeMenuComponent = function PermissionTypeMenuComponent(_ref) {
|
|
|
35
42
|
isMenuDisplayed = _useState2[0],
|
|
36
43
|
setMenuDisplayed = _useState2[1];
|
|
37
44
|
|
|
45
|
+
var _useState3 = useState(null),
|
|
46
|
+
_useState4 = _slicedToArray(_useState3, 2),
|
|
47
|
+
pendingType = _useState4[0],
|
|
48
|
+
setPendingType = _useState4[1];
|
|
49
|
+
|
|
50
|
+
var documentPath = useFetchDocumentPath(client, document);
|
|
38
51
|
var hideMenu = useCallback(function () {
|
|
39
52
|
setMenuDisplayed(false);
|
|
40
53
|
}, []);
|
|
41
|
-
var
|
|
54
|
+
var applyType = useCallback( /*#__PURE__*/function () {
|
|
42
55
|
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(newType) {
|
|
43
56
|
return _regeneratorRuntime.wrap(function _callee$(_context) {
|
|
44
57
|
while (1) switch (_context.prev = _context.next) {
|
|
45
58
|
case 0:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!(newType !== type)) {
|
|
49
|
-
_context.next = 11;
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
_context.prev = 2;
|
|
54
|
-
_context.next = 5;
|
|
59
|
+
_context.prev = 0;
|
|
60
|
+
_context.next = 3;
|
|
55
61
|
return updateSharingMemberType(sharingId, memberIndex, newType);
|
|
56
62
|
|
|
57
|
-
case
|
|
58
|
-
_context.next =
|
|
63
|
+
case 3:
|
|
64
|
+
_context.next = 9;
|
|
59
65
|
break;
|
|
60
66
|
|
|
61
|
-
case
|
|
62
|
-
_context.prev =
|
|
63
|
-
_context.t0 = _context["catch"](
|
|
67
|
+
case 5:
|
|
68
|
+
_context.prev = 5;
|
|
69
|
+
_context.t0 = _context["catch"](0);
|
|
64
70
|
log.error('Failed to change member permission type', _context.t0);
|
|
65
71
|
showAlert({
|
|
66
72
|
message: t('Share.members.error.changePermission'),
|
|
@@ -68,17 +74,39 @@ var PermissionTypeMenuComponent = function PermissionTypeMenuComponent(_ref) {
|
|
|
68
74
|
variant: 'filled'
|
|
69
75
|
});
|
|
70
76
|
|
|
71
|
-
case
|
|
77
|
+
case 9:
|
|
72
78
|
case "end":
|
|
73
79
|
return _context.stop();
|
|
74
80
|
}
|
|
75
|
-
}, _callee, null, [[
|
|
81
|
+
}, _callee, null, [[0, 5]]);
|
|
76
82
|
}));
|
|
77
83
|
|
|
78
84
|
return function (_x) {
|
|
79
85
|
return _ref2.apply(this, arguments);
|
|
80
86
|
};
|
|
81
|
-
}(), [
|
|
87
|
+
}(), [memberIndex, sharingId, showAlert, t, updateSharingMemberType]); // Downgrading to viewer on a folder inside a shared parent also reduces
|
|
88
|
+
// the member's rights on the parent: require explicit confirmation first.
|
|
89
|
+
|
|
90
|
+
var shouldConfirmDowngrade = type === 'two-way' && Boolean(documentPath) && (hasSharedParent === null || hasSharedParent === void 0 ? void 0 : hasSharedParent(documentPath));
|
|
91
|
+
var setType = useCallback(function (newType) {
|
|
92
|
+
hideMenu();
|
|
93
|
+
if (newType === type) return;
|
|
94
|
+
|
|
95
|
+
if (newType === 'one-way' && shouldConfirmDowngrade) {
|
|
96
|
+
setPendingType(newType);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
applyType(newType);
|
|
101
|
+
}, [applyType, hideMenu, shouldConfirmDowngrade, type]);
|
|
102
|
+
var handleConfirmDowngrade = useCallback(function () {
|
|
103
|
+
var newType = pendingType;
|
|
104
|
+
setPendingType(null);
|
|
105
|
+
if (newType) applyType(newType);
|
|
106
|
+
}, [applyType, pendingType]);
|
|
107
|
+
var handleCancelDowngrade = useCallback(function () {
|
|
108
|
+
setPendingType(null);
|
|
109
|
+
}, []);
|
|
82
110
|
var actions = makeActions([setReadOnlySharedPermission, setReadWriteSharedPermission], {
|
|
83
111
|
t: t,
|
|
84
112
|
type: type !== null && type !== void 0 ? type : 'one-way',
|
|
@@ -98,6 +126,11 @@ var PermissionTypeMenuComponent = function PermissionTypeMenuComponent(_ref) {
|
|
|
98
126
|
actions: actions,
|
|
99
127
|
autoClose: true,
|
|
100
128
|
onClose: hideMenu
|
|
129
|
+
}), pendingType && /*#__PURE__*/React.createElement(DowngradePermissionConfirmDialog, {
|
|
130
|
+
document: document,
|
|
131
|
+
recipient: recipient,
|
|
132
|
+
onCancel: handleCancelDowngrade,
|
|
133
|
+
onConfirm: handleConfirmDowngrade
|
|
101
134
|
}));
|
|
102
135
|
};
|
|
103
136
|
|
|
@@ -7,11 +7,15 @@ import { createMockClient } from 'cozy-client';
|
|
|
7
7
|
import { PermissionTypeMenu } from './PermissionTypeMenu';
|
|
8
8
|
import AppLike from '../../../test/AppLike';
|
|
9
9
|
var mockUpdateSharingMemberType = jest.fn();
|
|
10
|
+
var mockHasSharedParent = jest.fn();
|
|
11
|
+
var mockGetSharedParentPath = jest.fn();
|
|
10
12
|
var mockShowAlert = jest.fn();
|
|
11
13
|
jest.mock('../../hooks/useSharingContext', function () {
|
|
12
14
|
return {
|
|
13
15
|
useSharingContext: function useSharingContext() {
|
|
14
16
|
return {
|
|
17
|
+
hasSharedParent: mockHasSharedParent,
|
|
18
|
+
getSharedParentPath: mockGetSharedParentPath,
|
|
15
19
|
updateSharingMemberType: mockUpdateSharingMemberType
|
|
16
20
|
};
|
|
17
21
|
}
|
|
@@ -39,7 +43,10 @@ describe('PermissionTypeMenu component', function () {
|
|
|
39
43
|
var defaultProps = {
|
|
40
44
|
sharingId: 'sharing-123',
|
|
41
45
|
memberIndex: 1,
|
|
42
|
-
type: 'two-way'
|
|
46
|
+
type: 'two-way',
|
|
47
|
+
recipient: {
|
|
48
|
+
name: 'Alice'
|
|
49
|
+
}
|
|
43
50
|
};
|
|
44
51
|
beforeEach(function () {
|
|
45
52
|
jest.clearAllMocks();
|
|
@@ -177,4 +184,106 @@ describe('PermissionTypeMenu component', function () {
|
|
|
177
184
|
}
|
|
178
185
|
}, _callee4);
|
|
179
186
|
})));
|
|
187
|
+
describe('when downgrading to viewer on a folder with a shared parent', function () {
|
|
188
|
+
var folderDocument = {
|
|
189
|
+
_id: 'doc-1',
|
|
190
|
+
type: 'directory',
|
|
191
|
+
path: '/parent/child'
|
|
192
|
+
};
|
|
193
|
+
beforeEach(function () {
|
|
194
|
+
mockHasSharedParent.mockReturnValue(true);
|
|
195
|
+
});
|
|
196
|
+
it('should ask for confirmation instead of updating directly', function () {
|
|
197
|
+
var _setup5 = setup({
|
|
198
|
+
type: 'two-way',
|
|
199
|
+
document: folderDocument
|
|
200
|
+
}),
|
|
201
|
+
getByRole = _setup5.getByRole;
|
|
202
|
+
|
|
203
|
+
fireEvent.click(getByRole('button', {
|
|
204
|
+
name: 'Editor'
|
|
205
|
+
}));
|
|
206
|
+
fireEvent.click(getByRole('menuitem', {
|
|
207
|
+
name: 'Viewer'
|
|
208
|
+
}));
|
|
209
|
+
expect(mockUpdateSharingMemberType).not.toHaveBeenCalled();
|
|
210
|
+
expect(getByRole('button', {
|
|
211
|
+
name: 'Update parent'
|
|
212
|
+
})).toBeInTheDocument();
|
|
213
|
+
});
|
|
214
|
+
it('should call updateSharingMemberType when confirming', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee5() {
|
|
215
|
+
var _setup6, getByRole;
|
|
216
|
+
|
|
217
|
+
return _regeneratorRuntime.wrap(function _callee5$(_context5) {
|
|
218
|
+
while (1) switch (_context5.prev = _context5.next) {
|
|
219
|
+
case 0:
|
|
220
|
+
mockUpdateSharingMemberType.mockResolvedValue(undefined);
|
|
221
|
+
_setup6 = setup({
|
|
222
|
+
type: 'two-way',
|
|
223
|
+
document: folderDocument
|
|
224
|
+
}), getByRole = _setup6.getByRole;
|
|
225
|
+
fireEvent.click(getByRole('button', {
|
|
226
|
+
name: 'Editor'
|
|
227
|
+
}));
|
|
228
|
+
fireEvent.click(getByRole('menuitem', {
|
|
229
|
+
name: 'Viewer'
|
|
230
|
+
}));
|
|
231
|
+
fireEvent.click(getByRole('button', {
|
|
232
|
+
name: 'Update parent'
|
|
233
|
+
}));
|
|
234
|
+
_context5.next = 7;
|
|
235
|
+
return waitFor(function () {
|
|
236
|
+
expect(mockUpdateSharingMemberType).toHaveBeenCalledWith('sharing-123', 1, 'one-way');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
case 7:
|
|
240
|
+
case "end":
|
|
241
|
+
return _context5.stop();
|
|
242
|
+
}
|
|
243
|
+
}, _callee5);
|
|
244
|
+
})));
|
|
245
|
+
it('should not call updateSharingMemberType when cancelling', function () {
|
|
246
|
+
var _setup7 = setup({
|
|
247
|
+
type: 'two-way',
|
|
248
|
+
document: folderDocument
|
|
249
|
+
}),
|
|
250
|
+
getByRole = _setup7.getByRole,
|
|
251
|
+
queryByRole = _setup7.queryByRole;
|
|
252
|
+
|
|
253
|
+
fireEvent.click(getByRole('button', {
|
|
254
|
+
name: 'Editor'
|
|
255
|
+
}));
|
|
256
|
+
fireEvent.click(getByRole('menuitem', {
|
|
257
|
+
name: 'Viewer'
|
|
258
|
+
}));
|
|
259
|
+
fireEvent.click(getByRole('button', {
|
|
260
|
+
name: 'Cancel'
|
|
261
|
+
}));
|
|
262
|
+
expect(mockUpdateSharingMemberType).not.toHaveBeenCalled();
|
|
263
|
+
expect(queryByRole('button', {
|
|
264
|
+
name: 'Update parent'
|
|
265
|
+
})).toBe(null);
|
|
266
|
+
});
|
|
267
|
+
it('should update directly when the folder has no shared parent', function () {
|
|
268
|
+
mockHasSharedParent.mockReturnValue(false);
|
|
269
|
+
|
|
270
|
+
var _setup8 = setup({
|
|
271
|
+
type: 'two-way',
|
|
272
|
+
document: folderDocument
|
|
273
|
+
}),
|
|
274
|
+
getByRole = _setup8.getByRole,
|
|
275
|
+
queryByRole = _setup8.queryByRole;
|
|
276
|
+
|
|
277
|
+
fireEvent.click(getByRole('button', {
|
|
278
|
+
name: 'Editor'
|
|
279
|
+
}));
|
|
280
|
+
fireEvent.click(getByRole('menuitem', {
|
|
281
|
+
name: 'Viewer'
|
|
282
|
+
}));
|
|
283
|
+
expect(mockUpdateSharingMemberType).toHaveBeenCalledWith('sharing-123', 1, 'one-way');
|
|
284
|
+
expect(queryByRole('button', {
|
|
285
|
+
name: 'Update parent'
|
|
286
|
+
})).toBe(null);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
180
289
|
});
|
|
@@ -1,31 +1,8 @@
|
|
|
1
|
-
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
|
|
2
|
-
import _createClass from "@babel/runtime/helpers/createClass";
|
|
3
|
-
import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";
|
|
4
|
-
import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";
|
|
5
|
-
import _inherits from "@babel/runtime/helpers/inherits";
|
|
6
|
-
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
7
1
|
import _extends from "@babel/runtime/helpers/extends";
|
|
8
|
-
|
|
9
|
-
function _callSuper(_this, derived, args) {
|
|
10
|
-
function isNativeReflectConstruct() {
|
|
11
|
-
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
|
|
12
|
-
if (Reflect.construct.sham) return false;
|
|
13
|
-
if (typeof Proxy === "function") return true;
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
return !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
|
|
17
|
-
} catch (e) {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
derived = _getPrototypeOf(derived);
|
|
23
|
-
return _possibleConstructorReturn(_this, isNativeReflectConstruct() ? Reflect.construct(derived, args || [], _getPrototypeOf(_this).constructor) : derived.apply(_this, args));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
2
|
import PropTypes from 'prop-types';
|
|
27
3
|
import React from 'react';
|
|
28
4
|
import ReactTooltip from 'react-tooltip';
|
|
5
|
+
import { useI18n } from 'twake-i18n';
|
|
29
6
|
var styles = {
|
|
30
7
|
"shared-tooltip": "tooltip__shared-tooltip___7Dys3",
|
|
31
8
|
"shared-tooltip-list": "tooltip__shared-tooltip-list___jD5hi"
|
|
@@ -38,40 +15,23 @@ export var SharingTooltip = function SharingTooltip(props) {
|
|
|
38
15
|
}, props), props.children);
|
|
39
16
|
}; // accepts all the props from https://github.com/wwayne/react-tooltip#options
|
|
40
17
|
|
|
41
|
-
export var TooltipRecipientList =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return /*#__PURE__*/React.createElement("ul", {
|
|
59
|
-
className: styles['shared-tooltip-list']
|
|
60
|
-
}, recipientNames.slice(0, cutoff).map(function (name, i) {
|
|
61
|
-
return /*#__PURE__*/React.createElement("li", {
|
|
62
|
-
key: "key_name_".concat(name, "_").concat(i)
|
|
63
|
-
}, name);
|
|
64
|
-
}), recipientNames.length > cutoff && /*#__PURE__*/React.createElement("li", null, t('Share.members.others', {
|
|
65
|
-
smart_count: recipientNames.slice(cutoff).length
|
|
66
|
-
})));
|
|
67
|
-
}
|
|
68
|
-
}]);
|
|
69
|
-
}(React.Component);
|
|
70
|
-
|
|
71
|
-
_defineProperty(TooltipRecipientList, "contextTypes", {
|
|
72
|
-
t: PropTypes.func.isRequired
|
|
73
|
-
});
|
|
74
|
-
|
|
18
|
+
export var TooltipRecipientList = function TooltipRecipientList(_ref) {
|
|
19
|
+
var recipientNames = _ref.recipientNames,
|
|
20
|
+
cutoff = _ref.cutoff;
|
|
21
|
+
|
|
22
|
+
var _useI18n = useI18n(),
|
|
23
|
+
t = _useI18n.t;
|
|
24
|
+
|
|
25
|
+
return /*#__PURE__*/React.createElement("ul", {
|
|
26
|
+
className: styles['shared-tooltip-list']
|
|
27
|
+
}, recipientNames.slice(0, cutoff).map(function (name, i) {
|
|
28
|
+
return /*#__PURE__*/React.createElement("li", {
|
|
29
|
+
key: "key_name_".concat(name, "_").concat(i)
|
|
30
|
+
}, name);
|
|
31
|
+
}), recipientNames.length > cutoff && /*#__PURE__*/React.createElement("li", null, t('Share.members.others', {
|
|
32
|
+
smart_count: recipientNames.slice(cutoff).length
|
|
33
|
+
})));
|
|
34
|
+
};
|
|
75
35
|
TooltipRecipientList.propTypes = {
|
|
76
36
|
recipientNames: PropTypes.arrayOf(PropTypes.string),
|
|
77
37
|
cutoff: PropTypes.number
|
|
@@ -4,7 +4,7 @@ import _regeneratorRuntime from "@babel/runtime/regenerator";
|
|
|
4
4
|
import { useState, useEffect } from 'react';
|
|
5
5
|
import { fetchFilesPaths } from '../helpers/files';
|
|
6
6
|
export var useFetchDocumentPath = function useFetchDocumentPath(client, document) {
|
|
7
|
-
var _useState = useState(document.path ? document.path : null),
|
|
7
|
+
var _useState = useState(document !== null && document !== void 0 && document.path ? document.path : null),
|
|
8
8
|
_useState2 = _slicedToArray(_useState, 2),
|
|
9
9
|
documentPath = _useState2[0],
|
|
10
10
|
setDocumentPath = _useState2[1];
|
|
@@ -18,35 +18,44 @@ export var useFetchDocumentPath = function useFetchDocumentPath(client, document
|
|
|
18
18
|
while (1) switch (_context.prev = _context.next) {
|
|
19
19
|
case 0:
|
|
20
20
|
_context.prev = 0;
|
|
21
|
+
|
|
22
|
+
if (document) {
|
|
23
|
+
_context.next = 3;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return _context.abrupt("return", setDocumentPath(null));
|
|
28
|
+
|
|
29
|
+
case 3:
|
|
21
30
|
isDirectory = document.type === 'directory';
|
|
22
31
|
|
|
23
32
|
if (!isDirectory) {
|
|
24
|
-
_context.next =
|
|
33
|
+
_context.next = 6;
|
|
25
34
|
break;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
return _context.abrupt("return", setDocumentPath(document.path));
|
|
29
38
|
|
|
30
|
-
case
|
|
31
|
-
_context.next =
|
|
39
|
+
case 6:
|
|
40
|
+
_context.next = 8;
|
|
32
41
|
return fetchFilesPaths(client, document._type, [document]);
|
|
33
42
|
|
|
34
|
-
case
|
|
43
|
+
case 8:
|
|
35
44
|
path = _context.sent;
|
|
36
45
|
setDocumentPath(path[0]); // eslint-disable-next-line
|
|
37
46
|
|
|
38
|
-
_context.next =
|
|
47
|
+
_context.next = 14;
|
|
39
48
|
break;
|
|
40
49
|
|
|
41
|
-
case
|
|
42
|
-
_context.prev =
|
|
50
|
+
case 12:
|
|
51
|
+
_context.prev = 12;
|
|
43
52
|
_context.t0 = _context["catch"](0);
|
|
44
53
|
|
|
45
|
-
case
|
|
54
|
+
case 14:
|
|
46
55
|
case "end":
|
|
47
56
|
return _context.stop();
|
|
48
57
|
}
|
|
49
|
-
}, _callee, null, [[0,
|
|
58
|
+
}, _callee, null, [[0, 12]]);
|
|
50
59
|
}))();
|
|
51
60
|
}, [client, document]);
|
|
52
61
|
return documentPath;
|
package/dist/state.js
CHANGED
|
@@ -700,7 +700,7 @@ export var hasSharedChild = function hasSharedChild(state, documentPath) {
|
|
|
700
700
|
export var getSharedParentPath = function getSharedParentPath(state, documentPath) {
|
|
701
701
|
if (hasSharedParent(state, documentPath)) {
|
|
702
702
|
return state.sharedPaths.find(function (path) {
|
|
703
|
-
return documentPath.startsWith(path);
|
|
703
|
+
return documentPath.startsWith("".concat(path, "/"));
|
|
704
704
|
});
|
|
705
705
|
}
|
|
706
706
|
|
package/locales/en.json
CHANGED
|
@@ -386,6 +386,12 @@
|
|
|
386
386
|
"cancel": "Cancel",
|
|
387
387
|
"discard": "Discard"
|
|
388
388
|
},
|
|
389
|
+
"DowngradePermissionConfirmDialog": {
|
|
390
|
+
"title": "Update role for parent folder?",
|
|
391
|
+
"content": "Changing **%{contactName}** permissions on this item will aslo change permissions on a parent folder. Alternatively, apply limited access to this folder.",
|
|
392
|
+
"cancel": "Cancel",
|
|
393
|
+
"confirm": "Update parent"
|
|
394
|
+
},
|
|
389
395
|
"GroupRecipient": {
|
|
390
396
|
"secondary": "%{memberCount} member |||| %{memberCount} members",
|
|
391
397
|
"secondary_you": "including you"
|
package/locales/fr.json
CHANGED
|
@@ -384,6 +384,12 @@
|
|
|
384
384
|
"cancel": "Annuler",
|
|
385
385
|
"discard": "Abandonner"
|
|
386
386
|
},
|
|
387
|
+
"DowngradePermissionConfirmDialog": {
|
|
388
|
+
"title": "Modifier le rôle du dossier parent ?",
|
|
389
|
+
"content": "Modifier les permissions de **%{contactName}** sur cet élément modifiera également les permissions sur un dossier parent. Vous pouvez aussi appliquer un accès limité à ce dossier.",
|
|
390
|
+
"cancel": "Annuler",
|
|
391
|
+
"confirm": "Mettre à jour le parent"
|
|
392
|
+
},
|
|
387
393
|
"GroupRecipient": {
|
|
388
394
|
"secondary": "%{memberCount} membre |||| %{memberCount} membres",
|
|
389
395
|
"secondary_you": "dont vous"
|
package/locales/ru.json
CHANGED
|
@@ -386,6 +386,12 @@
|
|
|
386
386
|
"cancel": "Отмена",
|
|
387
387
|
"discard": "Отменить"
|
|
388
388
|
},
|
|
389
|
+
"DowngradePermissionConfirmDialog": {
|
|
390
|
+
"title": "Обновить роль родительской папки?",
|
|
391
|
+
"content": "Изменение прав доступа **%{contactName}** для этого элемента также изменит права доступа для родительской папки. Либо примените ограниченный доступ к этой папке.",
|
|
392
|
+
"cancel": "Отмена",
|
|
393
|
+
"confirm": "Обновить родительскую папку"
|
|
394
|
+
},
|
|
389
395
|
"GroupRecipient": {
|
|
390
396
|
"secondary": "%{memberCount} участник |||| %{memberCount} участников",
|
|
391
397
|
"secondary_you": "включая вас"
|
package/locales/vi.json
CHANGED
|
@@ -386,6 +386,12 @@
|
|
|
386
386
|
"cancel": "Hủy",
|
|
387
387
|
"discard": "Bỏ"
|
|
388
388
|
},
|
|
389
|
+
"DowngradePermissionConfirmDialog": {
|
|
390
|
+
"title": "Cập nhật vai trò cho thư mục cha?",
|
|
391
|
+
"content": "Việc thay đổi quyền của **%{contactName}** trên mục này cũng sẽ thay đổi quyền trên thư mục cha. Ngoài ra, bạn có thể áp dụng quyền truy cập giới hạn cho thư mục này.",
|
|
392
|
+
"cancel": "Hủy",
|
|
393
|
+
"confirm": "Cập nhật thư mục cha"
|
|
394
|
+
},
|
|
389
395
|
"GroupRecipient": {
|
|
390
396
|
"secondary": "%{memberCount} thành viên |||| %{memberCount} thành viên",
|
|
391
397
|
"secondary_you": "bao gồm cả bạn"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozy-sharing",
|
|
3
|
-
"version": "37.
|
|
3
|
+
"version": "37.7.0",
|
|
4
4
|
"description": "Provides sharing login for React applications.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"author": "Cozy",
|
|
@@ -59,14 +59,14 @@
|
|
|
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.
|
|
62
|
+
"cozy-ui-plus": "^13.0.2",
|
|
63
63
|
"jest": "30.3.0",
|
|
64
64
|
"jest-environment-jsdom": "30.3.0",
|
|
65
65
|
"react": "16.12.0",
|
|
66
66
|
"react-dom": "16.13.0",
|
|
67
67
|
"react-router": "^5.0.1",
|
|
68
68
|
"storybook": "7.6.0",
|
|
69
|
-
"twake-i18n": "^0.
|
|
69
|
+
"twake-i18n": "^0.5.0"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"@linagora/twake-icons": ">=2.6.6",
|
|
@@ -85,5 +85,5 @@
|
|
|
85
85
|
"sideEffects": [
|
|
86
86
|
"*.css"
|
|
87
87
|
],
|
|
88
|
-
"gitHead": "
|
|
88
|
+
"gitHead": "b204dc35cf01f135f6b8ba4a5ae4acbb0753456f"
|
|
89
89
|
}
|
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
import PropTypes from 'prop-types'
|
|
2
1
|
import React from 'react'
|
|
3
2
|
|
|
3
|
+
import { useI18n } from 'twake-i18n'
|
|
4
|
+
|
|
4
5
|
import Identity from './Identity'
|
|
5
6
|
import { getDisplayName } from '../../models'
|
|
6
7
|
import styles from '../../styles/recipient.styl'
|
|
7
8
|
import { ExtraAvatar } from '../Avatar/ExtraAvatar'
|
|
8
9
|
|
|
9
|
-
const ExtraIdentity = ({ extraRecipients }
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
name={t('Share.members.otherContacts', extraRecipients.length)}
|
|
10
|
+
const ExtraIdentity = ({ extraRecipients }) => {
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className={styles['recipient']}>
|
|
15
|
+
<ExtraAvatar
|
|
16
|
+
extraRecipients={extraRecipients.map(recipient =>
|
|
17
|
+
getDisplayName(recipient)
|
|
18
|
+
)}
|
|
19
19
|
/>
|
|
20
|
+
<div className={styles['recipient-ident-status']}>
|
|
21
|
+
<Identity
|
|
22
|
+
name={t('Share.members.otherContacts', extraRecipients.length)}
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
20
25
|
</div>
|
|
21
|
-
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
ExtraIdentity.contextTypes = {
|
|
25
|
-
t: PropTypes.func.isRequired
|
|
26
|
+
)
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export { ExtraIdentity }
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import PropTypes from 'prop-types'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import snarkdown from 'snarkdown'
|
|
4
|
+
|
|
5
|
+
import { Icon, FolderOutlined, ArrowUp } from '@linagora/twake-icons'
|
|
6
|
+
import { useClient } from 'cozy-client'
|
|
7
|
+
import Button from 'cozy-ui/transpiled/react/Buttons'
|
|
8
|
+
import { ConfirmDialog } from 'cozy-ui/transpiled/react/CozyDialogs'
|
|
9
|
+
import Timeline from 'cozy-ui/transpiled/react/Timeline'
|
|
10
|
+
import TimelineConnector from 'cozy-ui/transpiled/react/TimelineConnector'
|
|
11
|
+
import TimelineContent from 'cozy-ui/transpiled/react/TimelineContent'
|
|
12
|
+
import TimelineDot from 'cozy-ui/transpiled/react/TimelineDot'
|
|
13
|
+
import TimelineItem from 'cozy-ui/transpiled/react/TimelineItem'
|
|
14
|
+
import TimelineOppositeContent from 'cozy-ui/transpiled/react/TimelineOppositeContent'
|
|
15
|
+
import TimelineSeparator from 'cozy-ui/transpiled/react/TimelineSeparator'
|
|
16
|
+
import Typography from 'cozy-ui/transpiled/react/Typography'
|
|
17
|
+
import { useI18n } from 'twake-i18n'
|
|
18
|
+
|
|
19
|
+
import { DEFAULT_DISPLAY_NAME } from '../../helpers/recipients'
|
|
20
|
+
import { useFetchDocumentPath } from '../../hooks/useFetchDocumentPath'
|
|
21
|
+
import { useSharingContext } from '../../hooks/useSharingContext'
|
|
22
|
+
import { getDisplayName } from '../../models'
|
|
23
|
+
|
|
24
|
+
const DowngradePermissionConfirmDialog = ({
|
|
25
|
+
document,
|
|
26
|
+
recipient,
|
|
27
|
+
onCancel,
|
|
28
|
+
onConfirm
|
|
29
|
+
}) => {
|
|
30
|
+
const { t } = useI18n()
|
|
31
|
+
const client = useClient()
|
|
32
|
+
const { getSharedParentPath } = useSharingContext()
|
|
33
|
+
|
|
34
|
+
const documentPath = useFetchDocumentPath(client, document)
|
|
35
|
+
const sharedParentPath = documentPath && getSharedParentPath(documentPath)
|
|
36
|
+
const parentFolderName = sharedParentPath?.split('/').pop()
|
|
37
|
+
const contactName = getDisplayName(recipient, t(DEFAULT_DISPLAY_NAME))
|
|
38
|
+
const [before, afterOpen] = snarkdown(
|
|
39
|
+
t('DowngradePermissionConfirmDialog.content', { contactName })
|
|
40
|
+
).split('<strong>')
|
|
41
|
+
const [boldContactName, after] = afterOpen.split('</strong>')
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<ConfirmDialog
|
|
45
|
+
open
|
|
46
|
+
title={t('DowngradePermissionConfirmDialog.title')}
|
|
47
|
+
content={
|
|
48
|
+
<>
|
|
49
|
+
<Typography>
|
|
50
|
+
{before}
|
|
51
|
+
<strong>{boldContactName}</strong>
|
|
52
|
+
{after}
|
|
53
|
+
</Typography>
|
|
54
|
+
|
|
55
|
+
<Timeline>
|
|
56
|
+
<TimelineItem>
|
|
57
|
+
<TimelineOppositeContent style={{ display: 'none' }} />
|
|
58
|
+
<TimelineSeparator>
|
|
59
|
+
<TimelineDot className="u-bg-transparent u-bdw-0 u-elevation-0 u-p-0">
|
|
60
|
+
<div className="u-w-1-half u-flex u-flex-justify-center">
|
|
61
|
+
<Icon icon={FolderOutlined} size={24} />
|
|
62
|
+
</div>
|
|
63
|
+
</TimelineDot>
|
|
64
|
+
<TimelineConnector />
|
|
65
|
+
</TimelineSeparator>
|
|
66
|
+
<TimelineContent>
|
|
67
|
+
<Typography variant="body2">{parentFolderName}</Typography>
|
|
68
|
+
<div>
|
|
69
|
+
<s>
|
|
70
|
+
<Typography display="inline" variant="caption">
|
|
71
|
+
{t('Share.type.two-way')}
|
|
72
|
+
</Typography>
|
|
73
|
+
</s>
|
|
74
|
+
<Icon
|
|
75
|
+
className="u-mh-half"
|
|
76
|
+
color="var(--secondaryTextColor)"
|
|
77
|
+
icon={ArrowUp}
|
|
78
|
+
rotate={90}
|
|
79
|
+
size={12}
|
|
80
|
+
/>
|
|
81
|
+
<Typography display="inline" variant="caption">
|
|
82
|
+
{t('Share.type.one-way')}
|
|
83
|
+
</Typography>
|
|
84
|
+
</div>
|
|
85
|
+
</TimelineContent>
|
|
86
|
+
</TimelineItem>
|
|
87
|
+
<TimelineItem>
|
|
88
|
+
<TimelineOppositeContent style={{ display: 'none' }} />
|
|
89
|
+
<TimelineSeparator>
|
|
90
|
+
<TimelineDot className="u-bg-transparent u-bdw-0 u-elevation-0 u-p-0">
|
|
91
|
+
<div className="u-w-1-half u-flex u-flex-justify-center">
|
|
92
|
+
<Icon icon={FolderOutlined} size={16} />
|
|
93
|
+
</div>
|
|
94
|
+
</TimelineDot>
|
|
95
|
+
</TimelineSeparator>
|
|
96
|
+
<TimelineContent>
|
|
97
|
+
<Typography variant="body2">{document.name}</Typography>
|
|
98
|
+
<div>
|
|
99
|
+
<s>
|
|
100
|
+
<Typography display="inline" variant="caption">
|
|
101
|
+
{t('Share.type.two-way')}
|
|
102
|
+
</Typography>
|
|
103
|
+
</s>
|
|
104
|
+
<Icon
|
|
105
|
+
className="u-mh-half"
|
|
106
|
+
color="var(--secondaryTextColor)"
|
|
107
|
+
icon={ArrowUp}
|
|
108
|
+
rotate={90}
|
|
109
|
+
size={12}
|
|
110
|
+
/>
|
|
111
|
+
<Typography display="inline" variant="caption">
|
|
112
|
+
{t('Share.type.one-way')}
|
|
113
|
+
</Typography>
|
|
114
|
+
</div>
|
|
115
|
+
</TimelineContent>
|
|
116
|
+
</TimelineItem>
|
|
117
|
+
</Timeline>
|
|
118
|
+
</>
|
|
119
|
+
}
|
|
120
|
+
actions={
|
|
121
|
+
<>
|
|
122
|
+
<Button
|
|
123
|
+
variant="secondary"
|
|
124
|
+
label={t('DowngradePermissionConfirmDialog.cancel')}
|
|
125
|
+
onClick={onCancel}
|
|
126
|
+
/>
|
|
127
|
+
<Button
|
|
128
|
+
variant="primary"
|
|
129
|
+
label={t('DowngradePermissionConfirmDialog.confirm')}
|
|
130
|
+
onClick={onConfirm}
|
|
131
|
+
/>
|
|
132
|
+
</>
|
|
133
|
+
}
|
|
134
|
+
onClose={onCancel}
|
|
135
|
+
/>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
DowngradePermissionConfirmDialog.propTypes = {
|
|
140
|
+
document: PropTypes.object.isRequired,
|
|
141
|
+
recipient: PropTypes.object.isRequired,
|
|
142
|
+
onCancel: PropTypes.func.isRequired,
|
|
143
|
+
onConfirm: PropTypes.func.isRequired
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default DowngradePermissionConfirmDialog
|
|
@@ -47,7 +47,12 @@ const MemberRecipient = props => {
|
|
|
47
47
|
verifyRecipient={verifyRecipient}
|
|
48
48
|
/>
|
|
49
49
|
) : (
|
|
50
|
-
<MemberRecipientPermissions
|
|
50
|
+
<MemberRecipientPermissions
|
|
51
|
+
{...rest}
|
|
52
|
+
instance={instance}
|
|
53
|
+
isOwner={isOwner}
|
|
54
|
+
status={status}
|
|
55
|
+
/>
|
|
51
56
|
)
|
|
52
57
|
|
|
53
58
|
return (
|
|
@@ -20,7 +20,8 @@ const MemberRecipientPermissions = ({
|
|
|
20
20
|
onRevoke,
|
|
21
21
|
onRevokeSelf,
|
|
22
22
|
sharingId,
|
|
23
|
-
memberIndex
|
|
23
|
+
memberIndex,
|
|
24
|
+
...recipient
|
|
24
25
|
}) => {
|
|
25
26
|
const { t } = useI18n()
|
|
26
27
|
const client = useClient()
|
|
@@ -73,6 +74,8 @@ const MemberRecipientPermissions = ({
|
|
|
73
74
|
sharingId={sharingId}
|
|
74
75
|
memberIndex={memberIndex}
|
|
75
76
|
type={type ?? 'one-way'}
|
|
77
|
+
document={document}
|
|
78
|
+
recipient={recipient}
|
|
76
79
|
/>
|
|
77
80
|
<IconButton
|
|
78
81
|
onClick={handleRevocation}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useRef, useState } from 'react'
|
|
2
2
|
|
|
3
|
+
import { useClient } from 'cozy-client'
|
|
3
4
|
import minilog from 'cozy-minilog'
|
|
4
5
|
import ActionsMenu from 'cozy-ui/transpiled/react/ActionsMenu'
|
|
5
6
|
import { makeActions } from 'cozy-ui/transpiled/react/ActionsMenu/Actions'
|
|
@@ -7,52 +8,83 @@ import DropdownButton from 'cozy-ui/transpiled/react/DropdownButton'
|
|
|
7
8
|
import { useAlert } from 'cozy-ui/transpiled/react/providers/Alert'
|
|
8
9
|
import { useI18n } from 'twake-i18n'
|
|
9
10
|
|
|
11
|
+
import DowngradePermissionConfirmDialog from './DowngradePermissionConfirmDialog'
|
|
10
12
|
import { setReadOnlySharedPermission } from './actions/setReadOnlySharedPermission'
|
|
11
13
|
import { setReadWriteSharedPermission } from './actions/setReadWriteSharedPermission'
|
|
12
14
|
import withLocales from '../../hoc/withLocales'
|
|
15
|
+
import { useFetchDocumentPath } from '../../hooks/useFetchDocumentPath'
|
|
13
16
|
import { useSharingContext } from '../../hooks/useSharingContext'
|
|
14
17
|
|
|
15
18
|
const log = minilog('PermissionTypeMenu')
|
|
16
19
|
|
|
17
|
-
const PermissionTypeMenuComponent = ({
|
|
20
|
+
const PermissionTypeMenuComponent = ({
|
|
21
|
+
sharingId,
|
|
22
|
+
memberIndex,
|
|
23
|
+
type,
|
|
24
|
+
document,
|
|
25
|
+
recipient
|
|
26
|
+
}) => {
|
|
18
27
|
const { t } = useI18n()
|
|
28
|
+
const client = useClient()
|
|
19
29
|
const buttonRef = useRef()
|
|
20
|
-
const { updateSharingMemberType } = useSharingContext()
|
|
30
|
+
const { hasSharedParent, updateSharingMemberType } = useSharingContext()
|
|
21
31
|
const { showAlert } = useAlert()
|
|
22
32
|
|
|
23
33
|
const [isMenuDisplayed, setMenuDisplayed] = useState(false)
|
|
34
|
+
const [pendingType, setPendingType] = useState(null)
|
|
35
|
+
|
|
36
|
+
const documentPath = useFetchDocumentPath(client, document)
|
|
24
37
|
|
|
25
38
|
const hideMenu = useCallback(() => {
|
|
26
39
|
setMenuDisplayed(false)
|
|
27
40
|
}, [])
|
|
28
41
|
|
|
29
|
-
const
|
|
42
|
+
const applyType = useCallback(
|
|
30
43
|
async newType => {
|
|
44
|
+
try {
|
|
45
|
+
await updateSharingMemberType(sharingId, memberIndex, newType)
|
|
46
|
+
} catch (error) {
|
|
47
|
+
log.error('Failed to change member permission type', error)
|
|
48
|
+
showAlert({
|
|
49
|
+
message: t('Share.members.error.changePermission'),
|
|
50
|
+
severity: 'error',
|
|
51
|
+
variant: 'filled'
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
[memberIndex, sharingId, showAlert, t, updateSharingMemberType]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// Downgrading to viewer on a folder inside a shared parent also reduces
|
|
59
|
+
// the member's rights on the parent: require explicit confirmation first.
|
|
60
|
+
const shouldConfirmDowngrade =
|
|
61
|
+
type === 'two-way' &&
|
|
62
|
+
Boolean(documentPath) &&
|
|
63
|
+
hasSharedParent?.(documentPath)
|
|
64
|
+
|
|
65
|
+
const setType = useCallback(
|
|
66
|
+
newType => {
|
|
31
67
|
hideMenu()
|
|
32
|
-
if (newType
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
log.error('Failed to change member permission type', error)
|
|
37
|
-
showAlert({
|
|
38
|
-
message: t('Share.members.error.changePermission'),
|
|
39
|
-
severity: 'error',
|
|
40
|
-
variant: 'filled'
|
|
41
|
-
})
|
|
42
|
-
}
|
|
68
|
+
if (newType === type) return
|
|
69
|
+
if (newType === 'one-way' && shouldConfirmDowngrade) {
|
|
70
|
+
setPendingType(newType)
|
|
71
|
+
return
|
|
43
72
|
}
|
|
73
|
+
applyType(newType)
|
|
44
74
|
},
|
|
45
|
-
[
|
|
46
|
-
hideMenu,
|
|
47
|
-
memberIndex,
|
|
48
|
-
sharingId,
|
|
49
|
-
showAlert,
|
|
50
|
-
t,
|
|
51
|
-
type,
|
|
52
|
-
updateSharingMemberType
|
|
53
|
-
]
|
|
75
|
+
[applyType, hideMenu, shouldConfirmDowngrade, type]
|
|
54
76
|
)
|
|
55
77
|
|
|
78
|
+
const handleConfirmDowngrade = useCallback(() => {
|
|
79
|
+
const newType = pendingType
|
|
80
|
+
setPendingType(null)
|
|
81
|
+
if (newType) applyType(newType)
|
|
82
|
+
}, [applyType, pendingType])
|
|
83
|
+
|
|
84
|
+
const handleCancelDowngrade = useCallback(() => {
|
|
85
|
+
setPendingType(null)
|
|
86
|
+
}, [])
|
|
87
|
+
|
|
56
88
|
const actions = makeActions(
|
|
57
89
|
[setReadOnlySharedPermission, setReadWriteSharedPermission],
|
|
58
90
|
{
|
|
@@ -80,6 +112,14 @@ const PermissionTypeMenuComponent = ({ sharingId, memberIndex, type }) => {
|
|
|
80
112
|
autoClose
|
|
81
113
|
onClose={hideMenu}
|
|
82
114
|
/>
|
|
115
|
+
{pendingType && (
|
|
116
|
+
<DowngradePermissionConfirmDialog
|
|
117
|
+
document={document}
|
|
118
|
+
recipient={recipient}
|
|
119
|
+
onCancel={handleCancelDowngrade}
|
|
120
|
+
onConfirm={handleConfirmDowngrade}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
83
123
|
</>
|
|
84
124
|
)
|
|
85
125
|
}
|
|
@@ -7,10 +7,14 @@ import { PermissionTypeMenu } from './PermissionTypeMenu'
|
|
|
7
7
|
import AppLike from '../../../test/AppLike'
|
|
8
8
|
|
|
9
9
|
const mockUpdateSharingMemberType = jest.fn()
|
|
10
|
+
const mockHasSharedParent = jest.fn()
|
|
11
|
+
const mockGetSharedParentPath = jest.fn()
|
|
10
12
|
const mockShowAlert = jest.fn()
|
|
11
13
|
|
|
12
14
|
jest.mock('../../hooks/useSharingContext', () => ({
|
|
13
15
|
useSharingContext: () => ({
|
|
16
|
+
hasSharedParent: mockHasSharedParent,
|
|
17
|
+
getSharedParentPath: mockGetSharedParentPath,
|
|
14
18
|
updateSharingMemberType: mockUpdateSharingMemberType
|
|
15
19
|
})
|
|
16
20
|
}))
|
|
@@ -32,7 +36,8 @@ describe('PermissionTypeMenu component', () => {
|
|
|
32
36
|
const defaultProps = {
|
|
33
37
|
sharingId: 'sharing-123',
|
|
34
38
|
memberIndex: 1,
|
|
35
|
-
type: 'two-way'
|
|
39
|
+
type: 'two-way',
|
|
40
|
+
recipient: { name: 'Alice' }
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
beforeEach(() => {
|
|
@@ -128,4 +133,83 @@ describe('PermissionTypeMenu component', () => {
|
|
|
128
133
|
|
|
129
134
|
expect(mockUpdateSharingMemberType).not.toHaveBeenCalled()
|
|
130
135
|
})
|
|
136
|
+
|
|
137
|
+
describe('when downgrading to viewer on a folder with a shared parent', () => {
|
|
138
|
+
const folderDocument = {
|
|
139
|
+
_id: 'doc-1',
|
|
140
|
+
type: 'directory',
|
|
141
|
+
path: '/parent/child'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
mockHasSharedParent.mockReturnValue(true)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should ask for confirmation instead of updating directly', () => {
|
|
149
|
+
const { getByRole } = setup({
|
|
150
|
+
type: 'two-way',
|
|
151
|
+
document: folderDocument
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
fireEvent.click(getByRole('button', { name: 'Editor' }))
|
|
155
|
+
fireEvent.click(getByRole('menuitem', { name: 'Viewer' }))
|
|
156
|
+
|
|
157
|
+
expect(mockUpdateSharingMemberType).not.toHaveBeenCalled()
|
|
158
|
+
expect(getByRole('button', { name: 'Update parent' })).toBeInTheDocument()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should call updateSharingMemberType when confirming', async () => {
|
|
162
|
+
mockUpdateSharingMemberType.mockResolvedValue(undefined)
|
|
163
|
+
|
|
164
|
+
const { getByRole } = setup({
|
|
165
|
+
type: 'two-way',
|
|
166
|
+
document: folderDocument
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
fireEvent.click(getByRole('button', { name: 'Editor' }))
|
|
170
|
+
fireEvent.click(getByRole('menuitem', { name: 'Viewer' }))
|
|
171
|
+
fireEvent.click(getByRole('button', { name: 'Update parent' }))
|
|
172
|
+
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(mockUpdateSharingMemberType).toHaveBeenCalledWith(
|
|
175
|
+
'sharing-123',
|
|
176
|
+
1,
|
|
177
|
+
'one-way'
|
|
178
|
+
)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should not call updateSharingMemberType when cancelling', () => {
|
|
183
|
+
const { getByRole, queryByRole } = setup({
|
|
184
|
+
type: 'two-way',
|
|
185
|
+
document: folderDocument
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
fireEvent.click(getByRole('button', { name: 'Editor' }))
|
|
189
|
+
fireEvent.click(getByRole('menuitem', { name: 'Viewer' }))
|
|
190
|
+
fireEvent.click(getByRole('button', { name: 'Cancel' }))
|
|
191
|
+
|
|
192
|
+
expect(mockUpdateSharingMemberType).not.toHaveBeenCalled()
|
|
193
|
+
expect(queryByRole('button', { name: 'Update parent' })).toBe(null)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should update directly when the folder has no shared parent', () => {
|
|
197
|
+
mockHasSharedParent.mockReturnValue(false)
|
|
198
|
+
|
|
199
|
+
const { getByRole, queryByRole } = setup({
|
|
200
|
+
type: 'two-way',
|
|
201
|
+
document: folderDocument
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
fireEvent.click(getByRole('button', { name: 'Editor' }))
|
|
205
|
+
fireEvent.click(getByRole('menuitem', { name: 'Viewer' }))
|
|
206
|
+
|
|
207
|
+
expect(mockUpdateSharingMemberType).toHaveBeenCalledWith(
|
|
208
|
+
'sharing-123',
|
|
209
|
+
1,
|
|
210
|
+
'one-way'
|
|
211
|
+
)
|
|
212
|
+
expect(queryByRole('button', { name: 'Update parent' })).toBe(null)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
131
215
|
})
|
|
@@ -2,6 +2,8 @@ import PropTypes from 'prop-types'
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import ReactTooltip from 'react-tooltip'
|
|
4
4
|
|
|
5
|
+
import { useI18n } from 'twake-i18n'
|
|
6
|
+
|
|
5
7
|
import styles from '../styles/tooltip.styl'
|
|
6
8
|
|
|
7
9
|
export const SharingTooltip = props => (
|
|
@@ -16,29 +18,23 @@ export const SharingTooltip = props => (
|
|
|
16
18
|
)
|
|
17
19
|
// accepts all the props from https://github.com/wwayne/react-tooltip#options
|
|
18
20
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
t: PropTypes.func.isRequired
|
|
22
|
-
}
|
|
23
|
-
render() {
|
|
24
|
-
const { t } = this.context
|
|
25
|
-
const { recipientNames, cutoff = 4 } = this.props
|
|
21
|
+
export const TooltipRecipientList = ({ recipientNames, cutoff }) => {
|
|
22
|
+
const { t } = useI18n()
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
24
|
+
return (
|
|
25
|
+
<ul className={styles['shared-tooltip-list']}>
|
|
26
|
+
{recipientNames.slice(0, cutoff).map((name, i) => (
|
|
27
|
+
<li key={`key_name_${name}_${i}`}>{name}</li>
|
|
28
|
+
))}
|
|
29
|
+
{recipientNames.length > cutoff && (
|
|
30
|
+
<li>
|
|
31
|
+
{t('Share.members.others', {
|
|
32
|
+
smart_count: recipientNames.slice(cutoff).length
|
|
33
|
+
})}
|
|
34
|
+
</li>
|
|
35
|
+
)}
|
|
36
|
+
</ul>
|
|
37
|
+
)
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
TooltipRecipientList.propTypes = {
|
|
@@ -4,11 +4,13 @@ import { fetchFilesPaths } from '../helpers/files'
|
|
|
4
4
|
|
|
5
5
|
export const useFetchDocumentPath = (client, document) => {
|
|
6
6
|
const [documentPath, setDocumentPath] = useState(
|
|
7
|
-
document
|
|
7
|
+
document?.path ? document.path : null
|
|
8
8
|
)
|
|
9
9
|
useEffect(() => {
|
|
10
10
|
;(async () => {
|
|
11
11
|
try {
|
|
12
|
+
if (!document) return setDocumentPath(null)
|
|
13
|
+
|
|
12
14
|
const isDirectory = document.type === 'directory'
|
|
13
15
|
|
|
14
16
|
if (isDirectory) return setDocumentPath(document.path)
|
package/src/state.js
CHANGED
|
@@ -570,7 +570,7 @@ export const hasSharedChild = (state, documentPath) => {
|
|
|
570
570
|
*/
|
|
571
571
|
export const getSharedParentPath = (state, documentPath) => {
|
|
572
572
|
if (hasSharedParent(state, documentPath)) {
|
|
573
|
-
return state.sharedPaths.find(path => documentPath.startsWith(path))
|
|
573
|
+
return state.sharedPaths.find(path => documentPath.startsWith(`${path}/`))
|
|
574
574
|
}
|
|
575
575
|
return null
|
|
576
576
|
}
|