@dhis2/analytics 26.0.11 → 26.0.12

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.
Files changed (25) hide show
  1. package/build/cjs/components/Interpretations/InterpretationModal/Comment.js +7 -4
  2. package/build/cjs/components/Interpretations/InterpretationModal/CommentDeleteButton.js +24 -5
  3. package/build/cjs/components/Interpretations/InterpretationModal/CommentUpdateForm.js +1 -1
  4. package/build/cjs/components/Interpretations/InterpretationModal/InterpretationModal.js +1 -1
  5. package/build/cjs/components/Interpretations/InterpretationModal/InterpretationThread.js +8 -5
  6. package/build/cjs/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js +1 -1
  7. package/build/cjs/components/Interpretations/common/Interpretation/Interpretation.js +26 -10
  8. package/build/cjs/components/Interpretations/common/Message/MessageIconButton.js +9 -6
  9. package/build/cjs/components/Interpretations/common/__tests__/getInterpretationAccess.spec.js +152 -0
  10. package/build/cjs/components/Interpretations/common/getInterpretationAccess.js +25 -0
  11. package/build/cjs/components/Interpretations/common/index.js +11 -0
  12. package/build/cjs/locales/en/translations.json +5 -1
  13. package/build/es/components/Interpretations/InterpretationModal/Comment.js +8 -5
  14. package/build/es/components/Interpretations/InterpretationModal/CommentDeleteButton.js +23 -6
  15. package/build/es/components/Interpretations/InterpretationModal/CommentUpdateForm.js +1 -1
  16. package/build/es/components/Interpretations/InterpretationModal/InterpretationModal.js +1 -1
  17. package/build/es/components/Interpretations/InterpretationModal/InterpretationThread.js +9 -6
  18. package/build/es/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js +1 -1
  19. package/build/es/components/Interpretations/common/Interpretation/Interpretation.js +27 -11
  20. package/build/es/components/Interpretations/common/Message/MessageIconButton.js +9 -6
  21. package/build/es/components/Interpretations/common/__tests__/getInterpretationAccess.spec.js +150 -0
  22. package/build/es/components/Interpretations/common/getInterpretationAccess.js +17 -0
  23. package/build/es/components/Interpretations/common/index.js +2 -1
  24. package/build/es/locales/en/translations.json +5 -1
  25. package/package.json +1 -1
@@ -19,9 +19,11 @@ const Comment = _ref => {
19
19
  comment,
20
20
  currentUser,
21
21
  interpretationId,
22
- onThreadUpdated
22
+ onThreadUpdated,
23
+ canComment
23
24
  } = _ref;
24
25
  const [isUpdateMode, setIsUpdateMode] = (0, _react.useState)(false);
26
+ const commentAccess = (0, _index.getCommentAccess)(comment, canComment, currentUser);
25
27
  return isUpdateMode ? /*#__PURE__*/_react.default.createElement(_CommentUpdateForm.CommentUpdateForm, {
26
28
  close: () => setIsUpdateMode(false),
27
29
  commentId: comment.id,
@@ -33,11 +35,11 @@ const Comment = _ref => {
33
35
  text: comment.text,
34
36
  created: comment.created,
35
37
  username: comment.createdBy.displayName
36
- }, /*#__PURE__*/_react.default.createElement(_index.MessageStatsBar, null, comment.access.update && /*#__PURE__*/_react.default.createElement(_index.MessageIconButton, {
38
+ }, commentAccess.edit && /*#__PURE__*/_react.default.createElement(_index.MessageStatsBar, null, /*#__PURE__*/_react.default.createElement(_index.MessageIconButton, {
37
39
  iconComponent: _ui.IconEdit16,
38
40
  tooltipContent: _d2I18n.default.t('Edit'),
39
41
  onClick: () => setIsUpdateMode(true)
40
- }), comment.access.delete && /*#__PURE__*/_react.default.createElement(_CommentDeleteButton.CommentDeleteButton, {
42
+ }), commentAccess.delete && /*#__PURE__*/_react.default.createElement(_CommentDeleteButton.CommentDeleteButton, {
41
43
  commentId: comment.id,
42
44
  interpretationId: interpretationId,
43
45
  onComplete: () => onThreadUpdated(true)
@@ -48,5 +50,6 @@ Comment.propTypes = {
48
50
  comment: _propTypes.default.object.isRequired,
49
51
  currentUser: _propTypes.default.object.isRequired,
50
52
  interpretationId: _propTypes.default.string.isRequired,
51
- onThreadUpdated: _propTypes.default.func.isRequired
53
+ onThreadUpdated: _propTypes.default.func.isRequired,
54
+ canComment: _propTypes.default.bool
52
55
  };
@@ -4,12 +4,15 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.CommentDeleteButton = void 0;
7
+ var _style = _interopRequireDefault(require("styled-jsx/style"));
7
8
  var _appRuntime = require("@dhis2/app-runtime");
8
9
  var _d2I18n = _interopRequireDefault(require("@dhis2/d2-i18n"));
9
10
  var _ui = require("@dhis2/ui");
10
11
  var _propTypes = _interopRequireDefault(require("prop-types"));
11
- var _react = _interopRequireDefault(require("react"));
12
+ var _react = _interopRequireWildcard(require("react"));
12
13
  var _index = require("../common/index.js");
14
+ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
15
+ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
13
16
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14
17
  const mutation = {
15
18
  resource: 'interpretations',
@@ -28,21 +31,37 @@ const CommentDeleteButton = _ref2 => {
28
31
  interpretationId,
29
32
  onComplete
30
33
  } = _ref2;
34
+ const [deleteError, setDeleteError] = (0, _react.useState)(null);
31
35
  const [remove, {
32
36
  loading
33
37
  }] = (0, _appRuntime.useDataMutation)(mutation, {
34
- onComplete,
38
+ onComplete: () => {
39
+ setDeleteError(null);
40
+ onComplete();
41
+ },
42
+ onError: () => setDeleteError(_d2I18n.default.t('Delete failed')),
35
43
  variables: {
36
44
  commentId,
37
45
  interpretationId
38
46
  }
39
47
  });
40
- return /*#__PURE__*/_react.default.createElement(_index.MessageIconButton, {
48
+ const onDelete = () => {
49
+ setDeleteError(null);
50
+ remove();
51
+ };
52
+ return /*#__PURE__*/_react.default.createElement("div", {
53
+ className: _style.default.dynamic([["945681082", [_ui.colors.red500]]]) + " " + "delete-button-container"
54
+ }, /*#__PURE__*/_react.default.createElement(_index.MessageIconButton, {
41
55
  tooltipContent: _d2I18n.default.t('Delete'),
42
56
  iconComponent: _ui.IconDelete16,
43
- onClick: remove,
57
+ onClick: onDelete,
44
58
  disabled: loading
45
- });
59
+ }), deleteError && /*#__PURE__*/_react.default.createElement("span", {
60
+ className: _style.default.dynamic([["945681082", [_ui.colors.red500]]]) + " " + "delete-error"
61
+ }, deleteError), /*#__PURE__*/_react.default.createElement(_style.default, {
62
+ id: "945681082",
63
+ dynamic: [_ui.colors.red500]
64
+ }, [".delete-button-container.__jsx-style-dynamic-selector{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:4px;}", `.delete-error.__jsx-style-dynamic-selector{color:${_ui.colors.red500};font-size:12px;line-height:12px;}`]));
46
65
  };
47
66
  exports.CommentDeleteButton = CommentDeleteButton;
48
67
  CommentDeleteButton.propTypes = {
@@ -44,7 +44,7 @@ const CommentUpdateForm = _ref => {
44
44
  close();
45
45
  }
46
46
  });
47
- const errorText = error ? error.message || _d2I18n.default.t('Could not update comment') : '';
47
+ const errorText = error ? _d2I18n.default.t('Could not update comment') : '';
48
48
  return /*#__PURE__*/_react.default.createElement("div", {
49
49
  className: _style.default.dynamic([["2690082310", [_ui.spacers.dp8, _ui.spacers.dp8, _ui.colors.grey100]]]) + " " + "message"
50
50
  }, /*#__PURE__*/_react.default.createElement(_index.MessageEditorContainer, {
@@ -41,7 +41,7 @@ const query = {
41
41
  return id;
42
42
  },
43
43
  params: {
44
- fields: ['access', 'id', 'text', 'created', 'user[id,displayName]', 'likes', 'likedBy', 'comments[access,id,text,created,createdBy[id,displayName]]']
44
+ fields: ['access[write,manage]', 'id', 'text', 'created', 'createdBy[id,displayName]', 'likes', 'likedBy', 'comments[id,text,created,createdBy[id,displayName]]']
45
45
  }
46
46
  }
47
47
  };
@@ -34,6 +34,7 @@ const InterpretationThread = _ref => {
34
34
  });
35
35
  }
36
36
  }, [initialFocus]);
37
+ const interpretationAccess = (0, _index.getInterpretationAccess)(interpretation, currentUser);
37
38
  return /*#__PURE__*/_react.default.createElement("div", {
38
39
  className: "jsx-615306698" + " " + ((0, _classnames.default)('container', {
39
40
  fetching
@@ -52,12 +53,13 @@ const InterpretationThread = _ref => {
52
53
  }, /*#__PURE__*/_react.default.createElement(_index.Interpretation, {
53
54
  currentUser: currentUser,
54
55
  interpretation: interpretation,
55
- onReplyIconClick: () => {
56
+ onReplyIconClick: interpretationAccess.comment ? () => {
56
57
  var _focusRef$current;
57
58
  return (_focusRef$current = focusRef.current) === null || _focusRef$current === void 0 ? void 0 : _focusRef$current.focus();
58
- },
59
+ } : null,
59
60
  onUpdated: () => onThreadUpdated(true),
60
- onDeleted: onInterpretationDeleted
61
+ onDeleted: onInterpretationDeleted,
62
+ isInThread: true
61
63
  }), /*#__PURE__*/_react.default.createElement("div", {
62
64
  className: "jsx-615306698" + " " + 'comments'
63
65
  }, interpretation.comments.map(comment => /*#__PURE__*/_react.default.createElement(_Comment.Comment, {
@@ -65,8 +67,9 @@ const InterpretationThread = _ref => {
65
67
  comment: comment,
66
68
  currentUser: currentUser,
67
69
  interpretationId: interpretation.id,
68
- onThreadUpdated: onThreadUpdated
69
- }))), /*#__PURE__*/_react.default.createElement(_CommentAddForm.CommentAddForm, {
70
+ onThreadUpdated: onThreadUpdated,
71
+ canComment: interpretationAccess.comment
72
+ }))), interpretationAccess.comment && /*#__PURE__*/_react.default.createElement(_CommentAddForm.CommentAddForm, {
70
73
  currentUser: currentUser,
71
74
  interpretationId: interpretation.id,
72
75
  onSave: () => onThreadUpdated(true),
@@ -25,7 +25,7 @@ const interpretationsQuery = {
25
25
  id
26
26
  } = _ref;
27
27
  return {
28
- fields: ['access', 'id', 'user[displayName]', 'created', 'text', 'comments[id]', 'likes', 'likedBy[id]'],
28
+ fields: ['access[write,manage]', 'id', 'createdBy[id,displayName]', 'created', 'text', 'comments[id]', 'likes', 'likedBy[id]'],
29
29
  filter: `${type}.id:eq:${id}`
30
30
  };
31
31
  }
@@ -23,7 +23,8 @@ const Interpretation = _ref => {
23
23
  onUpdated,
24
24
  onDeleted,
25
25
  disabled,
26
- onReplyIconClick
26
+ onReplyIconClick,
27
+ isInThread
27
28
  } = _ref;
28
29
  const [isUpdateMode, setIsUpdateMode] = (0, _react.useState)(false);
29
30
  const [showSharingDialog, setShowSharingDialog] = (0, _react.useState)(false);
@@ -37,17 +38,30 @@ const Interpretation = _ref => {
37
38
  onComplete: onUpdated
38
39
  });
39
40
  const shouldShowButton = !!onClick && !disabled;
41
+ const interpretationAccess = (0, _index.getInterpretationAccess)(interpretation, currentUser);
42
+ let tooltip = _d2I18n.default.t('Reply');
43
+ if (!interpretationAccess.comment) {
44
+ if (isInThread) {
45
+ tooltip = _d2I18n.default.t('{{count}} replies', {
46
+ count: interpretation.comments.length,
47
+ defaultValue: '{{count}} reply',
48
+ defaultValue_plural: '{{count}} replies'
49
+ });
50
+ } else {
51
+ tooltip = _d2I18n.default.t('View replies');
52
+ }
53
+ }
40
54
  return isUpdateMode ? /*#__PURE__*/_react.default.createElement(_InterpretationUpdateForm.InterpretationUpdateForm, {
41
55
  close: () => setIsUpdateMode(false),
42
56
  id: interpretation.id,
43
- showSharingLink: interpretation.access.manage,
57
+ showSharingLink: interpretationAccess.share,
44
58
  onComplete: onUpdated,
45
59
  text: interpretation.text,
46
60
  currentUser: currentUser
47
61
  }) : /*#__PURE__*/_react.default.createElement(_index.Message, {
48
62
  text: interpretation.text,
49
63
  created: interpretation.created,
50
- username: interpretation.user.displayName
64
+ username: interpretation.createdBy.displayName
51
65
  }, !disabled && /*#__PURE__*/_react.default.createElement(_index.MessageStatsBar, null, /*#__PURE__*/_react.default.createElement(_index.MessageIconButton, {
52
66
  tooltipContent: isLikedByCurrentUser ? _d2I18n.default.t('Unlike') : _d2I18n.default.t('Like'),
53
67
  iconComponent: _ui.IconThumbUp16,
@@ -57,12 +71,13 @@ const Interpretation = _ref => {
57
71
  disabled: toggleLikeInProgress,
58
72
  dataTest: "interpretation-like-unlike-button"
59
73
  }), /*#__PURE__*/_react.default.createElement(_index.MessageIconButton, {
60
- tooltipContent: _d2I18n.default.t('Reply'),
74
+ tooltipContent: tooltip,
61
75
  iconComponent: _ui.IconReply16,
62
- onClick: () => onReplyIconClick(interpretation.id),
76
+ onClick: () => onReplyIconClick && onReplyIconClick(interpretation.id),
63
77
  count: interpretation.comments.length,
64
- dataTest: "interpretation-reply-button"
65
- }), interpretation.access.manage && /*#__PURE__*/_react.default.createElement(_index.MessageIconButton, {
78
+ dataTest: "interpretation-reply-button",
79
+ viewOnly: isInThread && !interpretationAccess.comment
80
+ }), interpretationAccess.share && /*#__PURE__*/_react.default.createElement(_index.MessageIconButton, {
66
81
  iconComponent: _ui.IconShare16,
67
82
  tooltipContent: _d2I18n.default.t('Share'),
68
83
  onClick: () => setShowSharingDialog(true),
@@ -72,15 +87,15 @@ const Interpretation = _ref => {
72
87
  type: 'interpretation',
73
88
  id: interpretation.id,
74
89
  onClose: () => setShowSharingDialog(false)
75
- }), interpretation.access.update && /*#__PURE__*/_react.default.createElement(_index.MessageIconButton, {
90
+ }), /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, interpretationAccess.edit && /*#__PURE__*/_react.default.createElement(_index.MessageIconButton, {
76
91
  iconComponent: _ui.IconEdit16,
77
92
  tooltipContent: _d2I18n.default.t('Edit'),
78
93
  onClick: () => setIsUpdateMode(true),
79
94
  dataTest: "interpretation-edit-button"
80
- }), interpretation.access.delete && /*#__PURE__*/_react.default.createElement(_InterpretationDeleteButton.InterpretationDeleteButton, {
95
+ }), interpretationAccess.delete && /*#__PURE__*/_react.default.createElement(_InterpretationDeleteButton.InterpretationDeleteButton, {
81
96
  id: interpretation.id,
82
97
  onComplete: onDeleted
83
- })), shouldShowButton && /*#__PURE__*/_react.default.createElement(_ui.Button, {
98
+ }))), shouldShowButton && /*#__PURE__*/_react.default.createElement(_ui.Button, {
84
99
  secondary: true,
85
100
  small: true,
86
101
  onClick: (_, event) => {
@@ -97,5 +112,6 @@ Interpretation.propTypes = {
97
112
  onReplyIconClick: _propTypes.default.func.isRequired,
98
113
  onUpdated: _propTypes.default.func.isRequired,
99
114
  disabled: _propTypes.default.bool,
115
+ isInThread: _propTypes.default.bool,
100
116
  onClick: _propTypes.default.func
101
117
  };
@@ -18,7 +18,8 @@ const MessageIconButton = _ref => {
18
18
  selected,
19
19
  count,
20
20
  iconComponent: Icon,
21
- dataTest
21
+ dataTest,
22
+ viewOnly
22
23
  } = _ref;
23
24
  return /*#__PURE__*/_react.default.createElement(_ui.Tooltip, {
24
25
  closeDelay: 200,
@@ -33,7 +34,7 @@ const MessageIconButton = _ref => {
33
34
  ref: ref,
34
35
  onMouseOver: onMouseOver,
35
36
  onMouseOut: onMouseOut,
36
- className: _style.default.dynamic([["250657028", [_ui.spacers.dp4, _ui.colors.grey700, _ui.colors.teal600, _ui.colors.grey900, _ui.colors.teal800, _ui.colors.teal500, _ui.colors.teal700, _ui.theme.disabled, _ui.theme.disabled]]]) + " " + "wrapper"
37
+ className: _style.default.dynamic([["163818318", [_ui.spacers.dp4, _ui.colors.grey700, _ui.colors.teal600, _ui.colors.grey900, _ui.colors.teal800, _ui.colors.teal500, _ui.colors.teal700, _ui.theme.disabled, _ui.theme.disabled]]]) + " " + "wrapper"
37
38
  }, /*#__PURE__*/_react.default.createElement("button", {
38
39
  onClick: event => {
39
40
  event.stopPropagation();
@@ -41,13 +42,14 @@ const MessageIconButton = _ref => {
41
42
  },
42
43
  disabled: disabled,
43
44
  "data-test": dataTest,
44
- className: _style.default.dynamic([["250657028", [_ui.spacers.dp4, _ui.colors.grey700, _ui.colors.teal600, _ui.colors.grey900, _ui.colors.teal800, _ui.colors.teal500, _ui.colors.teal700, _ui.theme.disabled, _ui.theme.disabled]]]) + " " + ((0, _classnames.default)('button', {
45
- selected
45
+ className: _style.default.dynamic([["163818318", [_ui.spacers.dp4, _ui.colors.grey700, _ui.colors.teal600, _ui.colors.grey900, _ui.colors.teal800, _ui.colors.teal500, _ui.colors.teal700, _ui.theme.disabled, _ui.theme.disabled]]]) + " " + ((0, _classnames.default)('button', {
46
+ selected,
47
+ viewOnly
46
48
  }) || "")
47
49
  }, count && count, /*#__PURE__*/_react.default.createElement(Icon, null)), /*#__PURE__*/_react.default.createElement(_style.default, {
48
- id: "250657028",
50
+ id: "163818318",
49
51
  dynamic: [_ui.spacers.dp4, _ui.colors.grey700, _ui.colors.teal600, _ui.colors.grey900, _ui.colors.teal800, _ui.colors.teal500, _ui.colors.teal700, _ui.theme.disabled, _ui.theme.disabled]
50
- }, [".wrapper.__jsx-style-dynamic-selector{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;}", `.button.__jsx-style-dynamic-selector{all:unset;cursor:pointer;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;gap:${_ui.spacers.dp4};-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:12px;line-height:14px;color:${_ui.colors.grey700};}`, `.button.selected.__jsx-style-dynamic-selector{color:${_ui.colors.teal600};font-weight:500;}`, `.button.__jsx-style-dynamic-selector:hover{color:${_ui.colors.grey900};}`, `.button.selected.__jsx-style-dynamic-selector:hover{color:${_ui.colors.teal800};}`, `.button.selected.__jsx-style-dynamic-selector svg{color:${_ui.colors.teal500};}`, `.button.selected.__jsx-style-dynamic-selector:hover svg{color:${_ui.colors.teal700};}`, `.button.__jsx-style-dynamic-selector:disabled{color:${_ui.theme.disabled};cursor:not-allowed;}`, `.button.__jsx-style-dynamic-selector:disabled svg{color:${_ui.theme.disabled};}`]));
52
+ }, [".wrapper.__jsx-style-dynamic-selector{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;}", `.button.__jsx-style-dynamic-selector{all:unset;cursor:pointer;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;gap:${_ui.spacers.dp4};-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:12px;line-height:14px;color:${_ui.colors.grey700};}`, ".viewOnly.__jsx-style-dynamic-selector{cursor:default;}", `.button.selected.__jsx-style-dynamic-selector{color:${_ui.colors.teal600};font-weight:500;}`, `.button.__jsx-style-dynamic-selector:hover{color:${_ui.colors.grey900};}`, `.button.selected.__jsx-style-dynamic-selector:hover{color:${_ui.colors.teal800};}`, `.button.selected.__jsx-style-dynamic-selector svg{color:${_ui.colors.teal500};}`, `.button.selected.__jsx-style-dynamic-selector:hover svg{color:${_ui.colors.teal700};}`, `.button.__jsx-style-dynamic-selector:disabled{color:${_ui.theme.disabled};cursor:not-allowed;}`, `.button.__jsx-style-dynamic-selector:disabled svg{color:${_ui.theme.disabled};}`]));
51
53
  });
52
54
  };
53
55
  exports.MessageIconButton = MessageIconButton;
@@ -58,5 +60,6 @@ MessageIconButton.propTypes = {
58
60
  dataTest: _propTypes.default.string,
59
61
  disabled: _propTypes.default.bool,
60
62
  selected: _propTypes.default.bool,
63
+ viewOnly: _propTypes.default.bool,
61
64
  onClick: _propTypes.default.func
62
65
  };
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+
3
+ var _getInterpretationAccess = require("../getInterpretationAccess.js");
4
+ const superuser = {
5
+ id: 'iamsuper',
6
+ authorities: new Set(['ALL'])
7
+ };
8
+ const userJoe = {
9
+ id: 'johndoe',
10
+ authorities: new Set(['Some'])
11
+ };
12
+ const userJane = {
13
+ id: 'jane',
14
+ authorities: new Set(['Some'])
15
+ };
16
+ describe('interpretation and comment access', () => {
17
+ describe('getInterpretationAccess', () => {
18
+ it('returns true for all accesses for superuser', () => {
19
+ const interpretation = {
20
+ access: {
21
+ write: true,
22
+ manage: true
23
+ },
24
+ createdBy: userJoe
25
+ };
26
+ expect((0, _getInterpretationAccess.getInterpretationAccess)(interpretation, superuser)).toMatchObject({
27
+ share: true,
28
+ comment: true,
29
+ edit: true,
30
+ delete: true
31
+ });
32
+ });
33
+ it('returns true for all accesses for creator', () => {
34
+ const interpretation = {
35
+ access: {
36
+ write: true,
37
+ manage: true
38
+ },
39
+ createdBy: userJoe
40
+ };
41
+ expect((0, _getInterpretationAccess.getInterpretationAccess)(interpretation, userJoe)).toMatchObject({
42
+ share: true,
43
+ comment: true,
44
+ edit: true,
45
+ delete: true
46
+ });
47
+ });
48
+ it('returns false for edit/delete if user is not creator/superuser', () => {
49
+ const interpretation = {
50
+ access: {
51
+ write: true,
52
+ manage: true
53
+ },
54
+ createdBy: userJane
55
+ };
56
+ expect((0, _getInterpretationAccess.getInterpretationAccess)(interpretation, userJoe)).toMatchObject({
57
+ share: true,
58
+ comment: true,
59
+ edit: false,
60
+ delete: false
61
+ });
62
+ });
63
+ it('returns false for comment/edit/delete if user is not creator/superuser and no write access', () => {
64
+ const interpretation = {
65
+ access: {
66
+ write: false,
67
+ manage: true
68
+ },
69
+ createdBy: userJane
70
+ };
71
+ expect((0, _getInterpretationAccess.getInterpretationAccess)(interpretation, userJoe)).toMatchObject({
72
+ share: true,
73
+ comment: false,
74
+ edit: false,
75
+ delete: false
76
+ });
77
+ });
78
+ it('returns false for share/comment/edit/delete if user is not creator/superuser and no write or manage access', () => {
79
+ const interpretation = {
80
+ access: {
81
+ write: false,
82
+ manage: false
83
+ },
84
+ createdBy: userJane
85
+ };
86
+ expect((0, _getInterpretationAccess.getInterpretationAccess)(interpretation, userJoe)).toMatchObject({
87
+ share: false,
88
+ comment: false,
89
+ edit: false,
90
+ delete: false
91
+ });
92
+ });
93
+ });
94
+ describe('getCommentAccess', () => {
95
+ it('returns true for all accesses for superuser', () => {
96
+ const interpretation = {
97
+ access: {
98
+ write: true
99
+ }
100
+ };
101
+ const comment = {
102
+ createdBy: userJoe
103
+ };
104
+ expect((0, _getInterpretationAccess.getCommentAccess)(comment, interpretation.access.write, superuser)).toMatchObject({
105
+ edit: true,
106
+ delete: true
107
+ });
108
+ });
109
+ it('returns true for all accesses for creator when interpretation has write access', () => {
110
+ const interpretation = {
111
+ access: {
112
+ write: true
113
+ }
114
+ };
115
+ const comment = {
116
+ createdBy: userJoe
117
+ };
118
+ expect((0, _getInterpretationAccess.getCommentAccess)(comment, interpretation.access.write, userJoe)).toMatchObject({
119
+ edit: true,
120
+ delete: true
121
+ });
122
+ });
123
+ it('returns true for edit and false for delete for creator when interpretation does not have write access', () => {
124
+ const interpretation = {
125
+ access: {
126
+ write: false
127
+ }
128
+ };
129
+ const comment = {
130
+ createdBy: userJoe
131
+ };
132
+ expect((0, _getInterpretationAccess.getCommentAccess)(comment, interpretation.access.write, userJoe)).toMatchObject({
133
+ edit: true,
134
+ delete: false
135
+ });
136
+ });
137
+ it('returns false for edit/delete for user who is not creator or superuser', () => {
138
+ const interpretation = {
139
+ access: {
140
+ write: true
141
+ }
142
+ };
143
+ const comment = {
144
+ createdBy: userJane
145
+ };
146
+ expect((0, _getInterpretationAccess.getCommentAccess)(comment, interpretation.access.write, userJoe)).toMatchObject({
147
+ edit: false,
148
+ delete: false
149
+ });
150
+ });
151
+ });
152
+ });
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getInterpretationAccess = exports.getCommentAccess = void 0;
7
+ const isCreatorOrSuperuser = (object, currentUser) => (object === null || object === void 0 ? void 0 : object.createdBy.id) === (currentUser === null || currentUser === void 0 ? void 0 : currentUser.id) || (currentUser === null || currentUser === void 0 ? void 0 : currentUser.authorities.has('ALL'));
8
+ const getInterpretationAccess = (interpretation, currentUser) => {
9
+ const canEditDelete = isCreatorOrSuperuser(interpretation, currentUser);
10
+ return {
11
+ share: interpretation.access.manage,
12
+ comment: interpretation.access.write,
13
+ edit: canEditDelete,
14
+ delete: canEditDelete
15
+ };
16
+ };
17
+ exports.getInterpretationAccess = getInterpretationAccess;
18
+ const getCommentAccess = (comment, hasInterpretationReplyAccess, currentUser) => {
19
+ const canEditDelete = isCreatorOrSuperuser(comment, currentUser);
20
+ return {
21
+ edit: canEditDelete,
22
+ delete: canEditDelete && hasInterpretationReplyAccess
23
+ };
24
+ };
25
+ exports.getCommentAccess = getCommentAccess;
@@ -35,4 +35,15 @@ Object.keys(_index3).forEach(function (key) {
35
35
  return _index3[key];
36
36
  }
37
37
  });
38
+ });
39
+ var _getInterpretationAccess = require("./getInterpretationAccess.js");
40
+ Object.keys(_getInterpretationAccess).forEach(function (key) {
41
+ if (key === "default" || key === "__esModule") return;
42
+ if (key in exports && exports[key] === _getInterpretationAccess[key]) return;
43
+ Object.defineProperty(exports, key, {
44
+ enumerable: true,
45
+ get: function () {
46
+ return _getInterpretationAccess[key];
47
+ }
48
+ });
38
49
  });
@@ -107,6 +107,7 @@
107
107
  "Edit": "Edit",
108
108
  "Write a reply": "Write a reply",
109
109
  "Post reply": "Post reply",
110
+ "Delete failed": "Delete failed",
110
111
  "Could not update comment": "Could not update comment",
111
112
  "Enter comment text": "Enter comment text",
112
113
  "Update": "Update",
@@ -117,9 +118,12 @@
117
118
  "Write an interpretation": "Write an interpretation",
118
119
  "Post interpretation": "Post interpretation",
119
120
  "Interpretations": "Interpretations",
121
+ "Reply": "Reply",
122
+ "{{count}} replies": "{{count}} reply",
123
+ "{{count}} replies_plural": "{{count}} replies",
124
+ "View replies": "View replies",
120
125
  "Unlike": "Unlike",
121
126
  "Like": "Like",
122
- "Reply": "Reply",
123
127
  "Share": "Share",
124
128
  "See interpretation": "See interpretation",
125
129
  "Manage sharing": "Manage sharing",
@@ -2,7 +2,7 @@ import i18n from '@dhis2/d2-i18n';
2
2
  import { IconEdit16 } from '@dhis2/ui';
3
3
  import PropTypes from 'prop-types';
4
4
  import React, { useState } from 'react';
5
- import { Message, MessageIconButton, MessageStatsBar } from '../common/index.js';
5
+ import { Message, MessageIconButton, MessageStatsBar, getCommentAccess } from '../common/index.js';
6
6
  import { CommentDeleteButton } from './CommentDeleteButton.js';
7
7
  import { CommentUpdateForm } from './CommentUpdateForm.js';
8
8
  const Comment = _ref => {
@@ -10,9 +10,11 @@ const Comment = _ref => {
10
10
  comment,
11
11
  currentUser,
12
12
  interpretationId,
13
- onThreadUpdated
13
+ onThreadUpdated,
14
+ canComment
14
15
  } = _ref;
15
16
  const [isUpdateMode, setIsUpdateMode] = useState(false);
17
+ const commentAccess = getCommentAccess(comment, canComment, currentUser);
16
18
  return isUpdateMode ? /*#__PURE__*/React.createElement(CommentUpdateForm, {
17
19
  close: () => setIsUpdateMode(false),
18
20
  commentId: comment.id,
@@ -24,11 +26,11 @@ const Comment = _ref => {
24
26
  text: comment.text,
25
27
  created: comment.created,
26
28
  username: comment.createdBy.displayName
27
- }, /*#__PURE__*/React.createElement(MessageStatsBar, null, comment.access.update && /*#__PURE__*/React.createElement(MessageIconButton, {
29
+ }, commentAccess.edit && /*#__PURE__*/React.createElement(MessageStatsBar, null, /*#__PURE__*/React.createElement(MessageIconButton, {
28
30
  iconComponent: IconEdit16,
29
31
  tooltipContent: i18n.t('Edit'),
30
32
  onClick: () => setIsUpdateMode(true)
31
- }), comment.access.delete && /*#__PURE__*/React.createElement(CommentDeleteButton, {
33
+ }), commentAccess.delete && /*#__PURE__*/React.createElement(CommentDeleteButton, {
32
34
  commentId: comment.id,
33
35
  interpretationId: interpretationId,
34
36
  onComplete: () => onThreadUpdated(true)
@@ -38,6 +40,7 @@ Comment.propTypes = {
38
40
  comment: PropTypes.object.isRequired,
39
41
  currentUser: PropTypes.object.isRequired,
40
42
  interpretationId: PropTypes.string.isRequired,
41
- onThreadUpdated: PropTypes.func.isRequired
43
+ onThreadUpdated: PropTypes.func.isRequired,
44
+ canComment: PropTypes.bool
42
45
  };
43
46
  export { Comment };
@@ -1,8 +1,9 @@
1
+ import _JSXStyle from "styled-jsx/style";
1
2
  import { useDataMutation } from '@dhis2/app-runtime';
2
3
  import i18n from '@dhis2/d2-i18n';
3
- import { IconDelete16 } from '@dhis2/ui';
4
+ import { IconDelete16, colors } from '@dhis2/ui';
4
5
  import PropTypes from 'prop-types';
5
- import React from 'react';
6
+ import React, { useState } from 'react';
6
7
  import { MessageIconButton } from '../common/index.js';
7
8
  const mutation = {
8
9
  resource: 'interpretations',
@@ -21,21 +22,37 @@ const CommentDeleteButton = _ref2 => {
21
22
  interpretationId,
22
23
  onComplete
23
24
  } = _ref2;
25
+ const [deleteError, setDeleteError] = useState(null);
24
26
  const [remove, {
25
27
  loading
26
28
  }] = useDataMutation(mutation, {
27
- onComplete,
29
+ onComplete: () => {
30
+ setDeleteError(null);
31
+ onComplete();
32
+ },
33
+ onError: () => setDeleteError(i18n.t('Delete failed')),
28
34
  variables: {
29
35
  commentId,
30
36
  interpretationId
31
37
  }
32
38
  });
33
- return /*#__PURE__*/React.createElement(MessageIconButton, {
39
+ const onDelete = () => {
40
+ setDeleteError(null);
41
+ remove();
42
+ };
43
+ return /*#__PURE__*/React.createElement("div", {
44
+ className: _JSXStyle.dynamic([["945681082", [colors.red500]]]) + " " + "delete-button-container"
45
+ }, /*#__PURE__*/React.createElement(MessageIconButton, {
34
46
  tooltipContent: i18n.t('Delete'),
35
47
  iconComponent: IconDelete16,
36
- onClick: remove,
48
+ onClick: onDelete,
37
49
  disabled: loading
38
- });
50
+ }), deleteError && /*#__PURE__*/React.createElement("span", {
51
+ className: _JSXStyle.dynamic([["945681082", [colors.red500]]]) + " " + "delete-error"
52
+ }, deleteError), /*#__PURE__*/React.createElement(_JSXStyle, {
53
+ id: "945681082",
54
+ dynamic: [colors.red500]
55
+ }, [".delete-button-container.__jsx-style-dynamic-selector{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:4px;}", `.delete-error.__jsx-style-dynamic-selector{color:${colors.red500};font-size:12px;line-height:12px;}`]));
39
56
  };
40
57
  CommentDeleteButton.propTypes = {
41
58
  commentId: PropTypes.string.isRequired,
@@ -35,7 +35,7 @@ export const CommentUpdateForm = _ref => {
35
35
  close();
36
36
  }
37
37
  });
38
- const errorText = error ? error.message || i18n.t('Could not update comment') : '';
38
+ const errorText = error ? i18n.t('Could not update comment') : '';
39
39
  return /*#__PURE__*/React.createElement("div", {
40
40
  className: _JSXStyle.dynamic([["2690082310", [spacers.dp8, spacers.dp8, colors.grey100]]]) + " " + "message"
41
41
  }, /*#__PURE__*/React.createElement(MessageEditorContainer, {
@@ -32,7 +32,7 @@ const query = {
32
32
  return id;
33
33
  },
34
34
  params: {
35
- fields: ['access', 'id', 'text', 'created', 'user[id,displayName]', 'likes', 'likedBy', 'comments[access,id,text,created,createdBy[id,displayName]]']
35
+ fields: ['access[write,manage]', 'id', 'text', 'created', 'createdBy[id,displayName]', 'likes', 'likedBy', 'comments[id,text,created,createdBy[id,displayName]]']
36
36
  }
37
37
  }
38
38
  };
@@ -4,7 +4,7 @@ import cx from 'classnames';
4
4
  import moment from 'moment';
5
5
  import PropTypes from 'prop-types';
6
6
  import React, { useRef, useEffect } from 'react';
7
- import { Interpretation } from '../common/index.js';
7
+ import { Interpretation, getInterpretationAccess } from '../common/index.js';
8
8
  import { Comment } from './Comment.js';
9
9
  import { CommentAddForm } from './CommentAddForm.js';
10
10
  const InterpretationThread = _ref => {
@@ -25,6 +25,7 @@ const InterpretationThread = _ref => {
25
25
  });
26
26
  }
27
27
  }, [initialFocus]);
28
+ const interpretationAccess = getInterpretationAccess(interpretation, currentUser);
28
29
  return /*#__PURE__*/React.createElement("div", {
29
30
  className: "jsx-615306698" + " " + (cx('container', {
30
31
  fetching
@@ -43,12 +44,13 @@ const InterpretationThread = _ref => {
43
44
  }, /*#__PURE__*/React.createElement(Interpretation, {
44
45
  currentUser: currentUser,
45
46
  interpretation: interpretation,
46
- onReplyIconClick: () => {
47
+ onReplyIconClick: interpretationAccess.comment ? () => {
47
48
  var _focusRef$current;
48
49
  return (_focusRef$current = focusRef.current) === null || _focusRef$current === void 0 ? void 0 : _focusRef$current.focus();
49
- },
50
+ } : null,
50
51
  onUpdated: () => onThreadUpdated(true),
51
- onDeleted: onInterpretationDeleted
52
+ onDeleted: onInterpretationDeleted,
53
+ isInThread: true
52
54
  }), /*#__PURE__*/React.createElement("div", {
53
55
  className: "jsx-615306698" + " " + 'comments'
54
56
  }, interpretation.comments.map(comment => /*#__PURE__*/React.createElement(Comment, {
@@ -56,8 +58,9 @@ const InterpretationThread = _ref => {
56
58
  comment: comment,
57
59
  currentUser: currentUser,
58
60
  interpretationId: interpretation.id,
59
- onThreadUpdated: onThreadUpdated
60
- }))), /*#__PURE__*/React.createElement(CommentAddForm, {
61
+ onThreadUpdated: onThreadUpdated,
62
+ canComment: interpretationAccess.comment
63
+ }))), interpretationAccess.comment && /*#__PURE__*/React.createElement(CommentAddForm, {
61
64
  currentUser: currentUser,
62
65
  interpretationId: interpretation.id,
63
66
  onSave: () => onThreadUpdated(true),
@@ -16,7 +16,7 @@ const interpretationsQuery = {
16
16
  id
17
17
  } = _ref;
18
18
  return {
19
- fields: ['access', 'id', 'user[displayName]', 'created', 'text', 'comments[id]', 'likes', 'likedBy[id]'],
19
+ fields: ['access[write,manage]', 'id', 'createdBy[id,displayName]', 'created', 'text', 'comments[id]', 'likes', 'likedBy[id]'],
20
20
  filter: `${type}.id:eq:${id}`
21
21
  };
22
22
  }
@@ -2,7 +2,7 @@ import i18n from '@dhis2/d2-i18n';
2
2
  import { Button, SharingDialog, IconReply16, IconShare16, IconThumbUp16, IconEdit16 } from '@dhis2/ui';
3
3
  import PropTypes from 'prop-types';
4
4
  import React, { useState } from 'react';
5
- import { Message, MessageStatsBar, MessageIconButton } from '../index.js';
5
+ import { Message, MessageStatsBar, MessageIconButton, getInterpretationAccess } from '../index.js';
6
6
  import { InterpretationDeleteButton } from './InterpretationDeleteButton.js';
7
7
  import { InterpretationUpdateForm } from './InterpretationUpdateForm.js';
8
8
  import { useLike } from './useLike.js';
@@ -14,7 +14,8 @@ export const Interpretation = _ref => {
14
14
  onUpdated,
15
15
  onDeleted,
16
16
  disabled,
17
- onReplyIconClick
17
+ onReplyIconClick,
18
+ isInThread
18
19
  } = _ref;
19
20
  const [isUpdateMode, setIsUpdateMode] = useState(false);
20
21
  const [showSharingDialog, setShowSharingDialog] = useState(false);
@@ -28,17 +29,30 @@ export const Interpretation = _ref => {
28
29
  onComplete: onUpdated
29
30
  });
30
31
  const shouldShowButton = !!onClick && !disabled;
32
+ const interpretationAccess = getInterpretationAccess(interpretation, currentUser);
33
+ let tooltip = i18n.t('Reply');
34
+ if (!interpretationAccess.comment) {
35
+ if (isInThread) {
36
+ tooltip = i18n.t('{{count}} replies', {
37
+ count: interpretation.comments.length,
38
+ defaultValue: '{{count}} reply',
39
+ defaultValue_plural: '{{count}} replies'
40
+ });
41
+ } else {
42
+ tooltip = i18n.t('View replies');
43
+ }
44
+ }
31
45
  return isUpdateMode ? /*#__PURE__*/React.createElement(InterpretationUpdateForm, {
32
46
  close: () => setIsUpdateMode(false),
33
47
  id: interpretation.id,
34
- showSharingLink: interpretation.access.manage,
48
+ showSharingLink: interpretationAccess.share,
35
49
  onComplete: onUpdated,
36
50
  text: interpretation.text,
37
51
  currentUser: currentUser
38
52
  }) : /*#__PURE__*/React.createElement(Message, {
39
53
  text: interpretation.text,
40
54
  created: interpretation.created,
41
- username: interpretation.user.displayName
55
+ username: interpretation.createdBy.displayName
42
56
  }, !disabled && /*#__PURE__*/React.createElement(MessageStatsBar, null, /*#__PURE__*/React.createElement(MessageIconButton, {
43
57
  tooltipContent: isLikedByCurrentUser ? i18n.t('Unlike') : i18n.t('Like'),
44
58
  iconComponent: IconThumbUp16,
@@ -48,12 +62,13 @@ export const Interpretation = _ref => {
48
62
  disabled: toggleLikeInProgress,
49
63
  dataTest: "interpretation-like-unlike-button"
50
64
  }), /*#__PURE__*/React.createElement(MessageIconButton, {
51
- tooltipContent: i18n.t('Reply'),
65
+ tooltipContent: tooltip,
52
66
  iconComponent: IconReply16,
53
- onClick: () => onReplyIconClick(interpretation.id),
67
+ onClick: () => onReplyIconClick && onReplyIconClick(interpretation.id),
54
68
  count: interpretation.comments.length,
55
- dataTest: "interpretation-reply-button"
56
- }), interpretation.access.manage && /*#__PURE__*/React.createElement(MessageIconButton, {
69
+ dataTest: "interpretation-reply-button",
70
+ viewOnly: isInThread && !interpretationAccess.comment
71
+ }), interpretationAccess.share && /*#__PURE__*/React.createElement(MessageIconButton, {
57
72
  iconComponent: IconShare16,
58
73
  tooltipContent: i18n.t('Share'),
59
74
  onClick: () => setShowSharingDialog(true),
@@ -63,15 +78,15 @@ export const Interpretation = _ref => {
63
78
  type: 'interpretation',
64
79
  id: interpretation.id,
65
80
  onClose: () => setShowSharingDialog(false)
66
- }), interpretation.access.update && /*#__PURE__*/React.createElement(MessageIconButton, {
81
+ }), /*#__PURE__*/React.createElement(React.Fragment, null, interpretationAccess.edit && /*#__PURE__*/React.createElement(MessageIconButton, {
67
82
  iconComponent: IconEdit16,
68
83
  tooltipContent: i18n.t('Edit'),
69
84
  onClick: () => setIsUpdateMode(true),
70
85
  dataTest: "interpretation-edit-button"
71
- }), interpretation.access.delete && /*#__PURE__*/React.createElement(InterpretationDeleteButton, {
86
+ }), interpretationAccess.delete && /*#__PURE__*/React.createElement(InterpretationDeleteButton, {
72
87
  id: interpretation.id,
73
88
  onComplete: onDeleted
74
- })), shouldShowButton && /*#__PURE__*/React.createElement(Button, {
89
+ }))), shouldShowButton && /*#__PURE__*/React.createElement(Button, {
75
90
  secondary: true,
76
91
  small: true,
77
92
  onClick: (_, event) => {
@@ -87,5 +102,6 @@ Interpretation.propTypes = {
87
102
  onReplyIconClick: PropTypes.func.isRequired,
88
103
  onUpdated: PropTypes.func.isRequired,
89
104
  disabled: PropTypes.bool,
105
+ isInThread: PropTypes.bool,
90
106
  onClick: PropTypes.func
91
107
  };
@@ -11,7 +11,8 @@ const MessageIconButton = _ref => {
11
11
  selected,
12
12
  count,
13
13
  iconComponent: Icon,
14
- dataTest
14
+ dataTest,
15
+ viewOnly
15
16
  } = _ref;
16
17
  return /*#__PURE__*/React.createElement(Tooltip, {
17
18
  closeDelay: 200,
@@ -26,7 +27,7 @@ const MessageIconButton = _ref => {
26
27
  ref: ref,
27
28
  onMouseOver: onMouseOver,
28
29
  onMouseOut: onMouseOut,
29
- className: _JSXStyle.dynamic([["250657028", [spacers.dp4, colors.grey700, colors.teal600, colors.grey900, colors.teal800, colors.teal500, colors.teal700, theme.disabled, theme.disabled]]]) + " " + "wrapper"
30
+ className: _JSXStyle.dynamic([["163818318", [spacers.dp4, colors.grey700, colors.teal600, colors.grey900, colors.teal800, colors.teal500, colors.teal700, theme.disabled, theme.disabled]]]) + " " + "wrapper"
30
31
  }, /*#__PURE__*/React.createElement("button", {
31
32
  onClick: event => {
32
33
  event.stopPropagation();
@@ -34,13 +35,14 @@ const MessageIconButton = _ref => {
34
35
  },
35
36
  disabled: disabled,
36
37
  "data-test": dataTest,
37
- className: _JSXStyle.dynamic([["250657028", [spacers.dp4, colors.grey700, colors.teal600, colors.grey900, colors.teal800, colors.teal500, colors.teal700, theme.disabled, theme.disabled]]]) + " " + (cx('button', {
38
- selected
38
+ className: _JSXStyle.dynamic([["163818318", [spacers.dp4, colors.grey700, colors.teal600, colors.grey900, colors.teal800, colors.teal500, colors.teal700, theme.disabled, theme.disabled]]]) + " " + (cx('button', {
39
+ selected,
40
+ viewOnly
39
41
  }) || "")
40
42
  }, count && count, /*#__PURE__*/React.createElement(Icon, null)), /*#__PURE__*/React.createElement(_JSXStyle, {
41
- id: "250657028",
43
+ id: "163818318",
42
44
  dynamic: [spacers.dp4, colors.grey700, colors.teal600, colors.grey900, colors.teal800, colors.teal500, colors.teal700, theme.disabled, theme.disabled]
43
- }, [".wrapper.__jsx-style-dynamic-selector{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;}", `.button.__jsx-style-dynamic-selector{all:unset;cursor:pointer;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;gap:${spacers.dp4};-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:12px;line-height:14px;color:${colors.grey700};}`, `.button.selected.__jsx-style-dynamic-selector{color:${colors.teal600};font-weight:500;}`, `.button.__jsx-style-dynamic-selector:hover{color:${colors.grey900};}`, `.button.selected.__jsx-style-dynamic-selector:hover{color:${colors.teal800};}`, `.button.selected.__jsx-style-dynamic-selector svg{color:${colors.teal500};}`, `.button.selected.__jsx-style-dynamic-selector:hover svg{color:${colors.teal700};}`, `.button.__jsx-style-dynamic-selector:disabled{color:${theme.disabled};cursor:not-allowed;}`, `.button.__jsx-style-dynamic-selector:disabled svg{color:${theme.disabled};}`]));
45
+ }, [".wrapper.__jsx-style-dynamic-selector{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;}", `.button.__jsx-style-dynamic-selector{all:unset;cursor:pointer;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;gap:${spacers.dp4};-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:12px;line-height:14px;color:${colors.grey700};}`, ".viewOnly.__jsx-style-dynamic-selector{cursor:default;}", `.button.selected.__jsx-style-dynamic-selector{color:${colors.teal600};font-weight:500;}`, `.button.__jsx-style-dynamic-selector:hover{color:${colors.grey900};}`, `.button.selected.__jsx-style-dynamic-selector:hover{color:${colors.teal800};}`, `.button.selected.__jsx-style-dynamic-selector svg{color:${colors.teal500};}`, `.button.selected.__jsx-style-dynamic-selector:hover svg{color:${colors.teal700};}`, `.button.__jsx-style-dynamic-selector:disabled{color:${theme.disabled};cursor:not-allowed;}`, `.button.__jsx-style-dynamic-selector:disabled svg{color:${theme.disabled};}`]));
44
46
  });
45
47
  };
46
48
  MessageIconButton.propTypes = {
@@ -50,6 +52,7 @@ MessageIconButton.propTypes = {
50
52
  dataTest: PropTypes.string,
51
53
  disabled: PropTypes.bool,
52
54
  selected: PropTypes.bool,
55
+ viewOnly: PropTypes.bool,
53
56
  onClick: PropTypes.func
54
57
  };
55
58
  export { MessageIconButton };
@@ -0,0 +1,150 @@
1
+ import { getInterpretationAccess, getCommentAccess } from '../getInterpretationAccess.js';
2
+ const superuser = {
3
+ id: 'iamsuper',
4
+ authorities: new Set(['ALL'])
5
+ };
6
+ const userJoe = {
7
+ id: 'johndoe',
8
+ authorities: new Set(['Some'])
9
+ };
10
+ const userJane = {
11
+ id: 'jane',
12
+ authorities: new Set(['Some'])
13
+ };
14
+ describe('interpretation and comment access', () => {
15
+ describe('getInterpretationAccess', () => {
16
+ it('returns true for all accesses for superuser', () => {
17
+ const interpretation = {
18
+ access: {
19
+ write: true,
20
+ manage: true
21
+ },
22
+ createdBy: userJoe
23
+ };
24
+ expect(getInterpretationAccess(interpretation, superuser)).toMatchObject({
25
+ share: true,
26
+ comment: true,
27
+ edit: true,
28
+ delete: true
29
+ });
30
+ });
31
+ it('returns true for all accesses for creator', () => {
32
+ const interpretation = {
33
+ access: {
34
+ write: true,
35
+ manage: true
36
+ },
37
+ createdBy: userJoe
38
+ };
39
+ expect(getInterpretationAccess(interpretation, userJoe)).toMatchObject({
40
+ share: true,
41
+ comment: true,
42
+ edit: true,
43
+ delete: true
44
+ });
45
+ });
46
+ it('returns false for edit/delete if user is not creator/superuser', () => {
47
+ const interpretation = {
48
+ access: {
49
+ write: true,
50
+ manage: true
51
+ },
52
+ createdBy: userJane
53
+ };
54
+ expect(getInterpretationAccess(interpretation, userJoe)).toMatchObject({
55
+ share: true,
56
+ comment: true,
57
+ edit: false,
58
+ delete: false
59
+ });
60
+ });
61
+ it('returns false for comment/edit/delete if user is not creator/superuser and no write access', () => {
62
+ const interpretation = {
63
+ access: {
64
+ write: false,
65
+ manage: true
66
+ },
67
+ createdBy: userJane
68
+ };
69
+ expect(getInterpretationAccess(interpretation, userJoe)).toMatchObject({
70
+ share: true,
71
+ comment: false,
72
+ edit: false,
73
+ delete: false
74
+ });
75
+ });
76
+ it('returns false for share/comment/edit/delete if user is not creator/superuser and no write or manage access', () => {
77
+ const interpretation = {
78
+ access: {
79
+ write: false,
80
+ manage: false
81
+ },
82
+ createdBy: userJane
83
+ };
84
+ expect(getInterpretationAccess(interpretation, userJoe)).toMatchObject({
85
+ share: false,
86
+ comment: false,
87
+ edit: false,
88
+ delete: false
89
+ });
90
+ });
91
+ });
92
+ describe('getCommentAccess', () => {
93
+ it('returns true for all accesses for superuser', () => {
94
+ const interpretation = {
95
+ access: {
96
+ write: true
97
+ }
98
+ };
99
+ const comment = {
100
+ createdBy: userJoe
101
+ };
102
+ expect(getCommentAccess(comment, interpretation.access.write, superuser)).toMatchObject({
103
+ edit: true,
104
+ delete: true
105
+ });
106
+ });
107
+ it('returns true for all accesses for creator when interpretation has write access', () => {
108
+ const interpretation = {
109
+ access: {
110
+ write: true
111
+ }
112
+ };
113
+ const comment = {
114
+ createdBy: userJoe
115
+ };
116
+ expect(getCommentAccess(comment, interpretation.access.write, userJoe)).toMatchObject({
117
+ edit: true,
118
+ delete: true
119
+ });
120
+ });
121
+ it('returns true for edit and false for delete for creator when interpretation does not have write access', () => {
122
+ const interpretation = {
123
+ access: {
124
+ write: false
125
+ }
126
+ };
127
+ const comment = {
128
+ createdBy: userJoe
129
+ };
130
+ expect(getCommentAccess(comment, interpretation.access.write, userJoe)).toMatchObject({
131
+ edit: true,
132
+ delete: false
133
+ });
134
+ });
135
+ it('returns false for edit/delete for user who is not creator or superuser', () => {
136
+ const interpretation = {
137
+ access: {
138
+ write: true
139
+ }
140
+ };
141
+ const comment = {
142
+ createdBy: userJane
143
+ };
144
+ expect(getCommentAccess(comment, interpretation.access.write, userJoe)).toMatchObject({
145
+ edit: false,
146
+ delete: false
147
+ });
148
+ });
149
+ });
150
+ });
@@ -0,0 +1,17 @@
1
+ const isCreatorOrSuperuser = (object, currentUser) => (object === null || object === void 0 ? void 0 : object.createdBy.id) === (currentUser === null || currentUser === void 0 ? void 0 : currentUser.id) || (currentUser === null || currentUser === void 0 ? void 0 : currentUser.authorities.has('ALL'));
2
+ export const getInterpretationAccess = (interpretation, currentUser) => {
3
+ const canEditDelete = isCreatorOrSuperuser(interpretation, currentUser);
4
+ return {
5
+ share: interpretation.access.manage,
6
+ comment: interpretation.access.write,
7
+ edit: canEditDelete,
8
+ delete: canEditDelete
9
+ };
10
+ };
11
+ export const getCommentAccess = (comment, hasInterpretationReplyAccess, currentUser) => {
12
+ const canEditDelete = isCreatorOrSuperuser(comment, currentUser);
13
+ return {
14
+ edit: canEditDelete,
15
+ delete: canEditDelete && hasInterpretationReplyAccess
16
+ };
17
+ };
@@ -1,3 +1,4 @@
1
1
  export * from './Interpretation/index.js';
2
2
  export * from './Message/index.js';
3
- export * from './RichTextEditor/index.js';
3
+ export * from './RichTextEditor/index.js';
4
+ export * from './getInterpretationAccess.js';
@@ -107,6 +107,7 @@
107
107
  "Edit": "Edit",
108
108
  "Write a reply": "Write a reply",
109
109
  "Post reply": "Post reply",
110
+ "Delete failed": "Delete failed",
110
111
  "Could not update comment": "Could not update comment",
111
112
  "Enter comment text": "Enter comment text",
112
113
  "Update": "Update",
@@ -117,9 +118,12 @@
117
118
  "Write an interpretation": "Write an interpretation",
118
119
  "Post interpretation": "Post interpretation",
119
120
  "Interpretations": "Interpretations",
121
+ "Reply": "Reply",
122
+ "{{count}} replies": "{{count}} reply",
123
+ "{{count}} replies_plural": "{{count}} replies",
124
+ "View replies": "View replies",
120
125
  "Unlike": "Unlike",
121
126
  "Like": "Like",
122
- "Reply": "Reply",
123
127
  "Share": "Share",
124
128
  "See interpretation": "See interpretation",
125
129
  "Manage sharing": "Manage sharing",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhis2/analytics",
3
- "version": "26.0.11",
3
+ "version": "26.0.12",
4
4
  "main": "./build/cjs/index.js",
5
5
  "module": "./build/es/index.js",
6
6
  "exports": {