@dhis2/analytics 28.1.3 → 29.0.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.
Files changed (57) hide show
  1. package/build/cjs/__demo__/InterpretationsUnit.stories.js +9 -6
  2. package/build/cjs/__fixtures__/interpretationsMockData.js +204 -0
  3. package/build/cjs/components/Interpretations/DashboardItemInterpretations/DashboardInterpretationThread.js +56 -0
  4. package/build/cjs/components/Interpretations/DashboardItemInterpretations/DashboardItemInterpretations.js +54 -0
  5. package/build/cjs/components/Interpretations/DashboardItemInterpretations/index.js +12 -0
  6. package/build/cjs/components/Interpretations/InterpretationModal/Comment.js +12 -17
  7. package/build/cjs/components/Interpretations/InterpretationModal/CommentAddForm.js +20 -34
  8. package/build/cjs/components/Interpretations/InterpretationModal/CommentDeleteButton.js +11 -36
  9. package/build/cjs/components/Interpretations/InterpretationModal/CommentUpdateForm.js +11 -27
  10. package/build/cjs/components/Interpretations/InterpretationModal/InterpretationModal.js +11 -68
  11. package/build/cjs/components/Interpretations/InterpretationModal/InterpretationThread.js +11 -24
  12. package/build/cjs/components/Interpretations/InterpretationsProvider/InterpretationsManager.js +275 -0
  13. package/build/cjs/components/Interpretations/InterpretationsProvider/InterpretationsProvider.js +28 -0
  14. package/build/cjs/components/Interpretations/InterpretationsProvider/__tests__/groupInterpretationIdsByDate.spec.js +37 -0
  15. package/build/cjs/components/Interpretations/InterpretationsProvider/__tests__/hooks.spec.js +565 -0
  16. package/build/cjs/components/Interpretations/InterpretationsProvider/groupInterpretationIdsByDate.js +16 -0
  17. package/build/cjs/components/Interpretations/InterpretationsProvider/hooks.js +278 -0
  18. package/build/cjs/components/Interpretations/InterpretationsProvider/index.js +12 -0
  19. package/build/cjs/components/Interpretations/InterpretationsUnit/InterpretationForm.js +25 -30
  20. package/build/cjs/components/Interpretations/InterpretationsUnit/InterpretationList.js +8 -38
  21. package/build/cjs/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js +22 -73
  22. package/build/cjs/components/Interpretations/common/Interpretation/Interpretation.js +20 -34
  23. package/build/cjs/components/Interpretations/common/Interpretation/InterpretationDeleteButton.js +10 -12
  24. package/build/cjs/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js +13 -24
  25. package/build/cjs/components/Interpretations/common/Message/MessageEditorContainer.js +3 -3
  26. package/build/cjs/index.js +72 -63
  27. package/build/cjs/locales/en/translations.json +10 -1
  28. package/build/es/__demo__/InterpretationsUnit.stories.js +9 -6
  29. package/build/es/__fixtures__/interpretationsMockData.js +198 -0
  30. package/build/es/components/Interpretations/DashboardItemInterpretations/DashboardInterpretationThread.js +48 -0
  31. package/build/es/components/Interpretations/DashboardItemInterpretations/DashboardItemInterpretations.js +45 -0
  32. package/build/es/components/Interpretations/DashboardItemInterpretations/index.js +1 -0
  33. package/build/es/components/Interpretations/InterpretationModal/Comment.js +14 -19
  34. package/build/es/components/Interpretations/InterpretationModal/CommentAddForm.js +21 -35
  35. package/build/es/components/Interpretations/InterpretationModal/CommentDeleteButton.js +11 -35
  36. package/build/es/components/Interpretations/InterpretationModal/CommentUpdateForm.js +12 -28
  37. package/build/es/components/Interpretations/InterpretationModal/InterpretationModal.js +12 -69
  38. package/build/es/components/Interpretations/InterpretationModal/InterpretationThread.js +11 -24
  39. package/build/es/components/Interpretations/InterpretationsProvider/InterpretationsManager.js +268 -0
  40. package/build/es/components/Interpretations/InterpretationsProvider/InterpretationsProvider.js +19 -0
  41. package/build/es/components/Interpretations/InterpretationsProvider/__tests__/groupInterpretationIdsByDate.spec.js +35 -0
  42. package/build/es/components/Interpretations/InterpretationsProvider/__tests__/hooks.spec.js +561 -0
  43. package/build/es/components/Interpretations/InterpretationsProvider/groupInterpretationIdsByDate.js +9 -0
  44. package/build/es/components/Interpretations/InterpretationsProvider/hooks.js +258 -0
  45. package/build/es/components/Interpretations/InterpretationsProvider/index.js +1 -0
  46. package/build/es/components/Interpretations/InterpretationsUnit/InterpretationForm.js +26 -31
  47. package/build/es/components/Interpretations/InterpretationsUnit/InterpretationList.js +8 -38
  48. package/build/es/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js +23 -75
  49. package/build/es/components/Interpretations/common/Interpretation/Interpretation.js +21 -35
  50. package/build/es/components/Interpretations/common/Interpretation/InterpretationDeleteButton.js +11 -13
  51. package/build/es/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js +14 -25
  52. package/build/es/components/Interpretations/common/Message/MessageEditorContainer.js +3 -3
  53. package/build/es/index.js +3 -1
  54. package/build/es/locales/en/translations.json +10 -1
  55. package/package.json +1 -1
  56. package/build/cjs/components/Interpretations/common/Interpretation/useLike.js +0 -56
  57. package/build/es/components/Interpretations/common/Interpretation/useLike.js +0 -50
@@ -1,10 +1,10 @@
1
1
  import _JSXStyle from "styled-jsx/style";
2
- import { useDataQuery } from '@dhis2/app-runtime';
3
2
  import i18n from '@dhis2/d2-i18n';
4
3
  import { Modal, ModalActions, ModalContent, NoticeBox, Button, spacers, colors, Layer, CenteredContent, CircularLoader } from '@dhis2/ui';
5
4
  import cx from 'classnames';
6
5
  import PropTypes from 'prop-types';
7
- import React, { useEffect, useState, useMemo } from 'react';
6
+ import React, { useMemo } from 'react';
7
+ import { useActiveInterpretation, useInterpretationsCurrentUser } from '../InterpretationsProvider/hooks.js';
8
8
  import { InterpretationThread } from './InterpretationThread.js';
9
9
  import { useModalContentWidth } from './useModalContentWidth.js';
10
10
  const modalCSS = {
@@ -22,25 +22,12 @@ function getModalContentCSS(width) {
22
22
  className: _JSXStyle.dynamic([["2099285089", [width]]])
23
23
  };
24
24
  }
25
- const query = {
26
- interpretation: {
27
- resource: 'interpretations',
28
- id: ({
29
- id
30
- }) => id,
31
- params: {
32
- fields: ['access[write,manage]', 'id', 'text', 'created', 'createdBy[id,displayName]', 'likes', 'likedBy', 'comments[id,text,created,createdBy[id,displayName]]']
33
- }
34
- }
35
- };
36
25
  const InterpretationModal = ({
37
- currentUser,
38
26
  isVisualizationLoading,
39
27
  visualization,
40
28
  onResponsesReceived,
41
29
  downloadMenuComponent,
42
30
  onClose,
43
- onInterpretationUpdate,
44
31
  interpretationId,
45
32
  initialFocus,
46
33
  pluginComponent: VisualizationPlugin
@@ -48,53 +35,14 @@ const InterpretationModal = ({
48
35
  var _currentUser$settings;
49
36
  const modalContentWidth = useModalContentWidth();
50
37
  const modalContentCSS = getModalContentCSS(modalContentWidth);
51
- const [isDirty, setIsDirty] = useState(false);
38
+ const currentUser = useInterpretationsCurrentUser();
52
39
  const {
53
- data,
54
- error,
40
+ data: interpretation,
55
41
  loading,
56
- fetching,
57
- refetch
58
- } = useDataQuery(query, {
59
- lazy: true
60
- });
61
- const interpretation = data === null || data === void 0 ? void 0 : data.interpretation;
42
+ error
43
+ } = useActiveInterpretation(interpretationId);
62
44
  const shouldRenderModalContent = !error && interpretation;
63
45
  const loadingInProgress = loading || isVisualizationLoading;
64
- const handleClose = () => {
65
- if (isDirty) {
66
- onInterpretationUpdate();
67
- setIsDirty(false);
68
- }
69
- onClose();
70
- };
71
- const onThreadUpdated = affectsInterpretation => {
72
- if (affectsInterpretation) {
73
- setIsDirty(true);
74
- }
75
- refetch({
76
- id: interpretationId
77
- });
78
- };
79
- const onLikeToggled = ({
80
- likedBy
81
- }) => {
82
- setIsDirty(true);
83
- interpretation.likedBy = likedBy;
84
- interpretation.likes = likedBy.length;
85
- };
86
- const onInterpretationDeleted = () => {
87
- setIsDirty(false);
88
- onInterpretationUpdate();
89
- onClose();
90
- };
91
- useEffect(() => {
92
- if (interpretationId) {
93
- refetch({
94
- id: interpretationId
95
- });
96
- }
97
- }, [interpretationId, refetch]);
98
46
  const filters = useMemo(() => {
99
47
  return {
100
48
  relativePeriodDate: interpretation === null || interpretation === void 0 ? void 0 : interpretation.created
@@ -102,7 +50,7 @@ const InterpretationModal = ({
102
50
  }, [interpretation === null || interpretation === void 0 ? void 0 : interpretation.created]);
103
51
  return /*#__PURE__*/React.createElement(React.Fragment, null, loadingInProgress && /*#__PURE__*/React.createElement(Layer, null, /*#__PURE__*/React.createElement(CenteredContent, null, /*#__PURE__*/React.createElement(CircularLoader, null))), /*#__PURE__*/React.createElement(Modal, {
104
52
  fluid: true,
105
- onClose: handleClose,
53
+ onClose: onClose,
106
54
  className: cx(modalCSS.className, {
107
55
  hidden: loadingInProgress
108
56
  }),
@@ -135,24 +83,20 @@ const InterpretationModal = ({
135
83
  })), /*#__PURE__*/React.createElement("div", {
136
84
  className: _JSXStyle.dynamic([["2014146191", [colors.grey900, spacers.dp24, spacers.dp4, spacers.dp4]]]) + " " + "thread-wrap"
137
85
  }, /*#__PURE__*/React.createElement(InterpretationThread, {
138
- currentUser: currentUser,
139
- fetching: fetching,
86
+ loading: loading,
140
87
  interpretation: interpretation,
141
- onInterpretationDeleted: onInterpretationDeleted,
142
- onThreadUpdated: onThreadUpdated,
143
88
  initialFocus: initialFocus,
144
89
  downloadMenuComponent: downloadMenuComponent,
145
- onLikeToggled: onLikeToggled
90
+ onInterpretationDeleted: onClose
146
91
  }))))), /*#__PURE__*/React.createElement(ModalActions, null, /*#__PURE__*/React.createElement(Button, {
147
- disabled: fetching,
148
- onClick: handleClose
92
+ disabled: loading,
93
+ onClick: onClose
149
94
  }, i18n.t('Hide interpretation'))), modalCSS.styles, modalContentCSS.styles, /*#__PURE__*/React.createElement(_JSXStyle, {
150
95
  id: "2014146191",
151
96
  dynamic: [colors.grey900, spacers.dp24, spacers.dp4, spacers.dp4]
152
97
  }, [`.title.__jsx-style-dynamic-selector{color:${colors.grey900};margin:0px;padding:${spacers.dp24} 0 ${spacers.dp4};}`, ".ellipsis.__jsx-style-dynamic-selector{display:inline-block;font-size:20px;font-weight:500;line-height:24px;white-space:nowrap;width:100%;overflow:hidden;text-overflow:ellipsis;}", ".container.__jsx-style-dynamic-selector{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;height:100%;}", ".row.__jsx-style-dynamic-selector{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;gap:16px;height:100%;}", ".visualisation-wrap.__jsx-style-dynamic-selector{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;min-width:0;}", `.thread-wrap.__jsx-style-dynamic-selector{padding-right:${spacers.dp4};-webkit-flex-basis:300px;-ms-flex-preferred-size:300px;flex-basis:300px;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;}`])));
153
98
  };
154
99
  InterpretationModal.propTypes = {
155
- currentUser: PropTypes.object.isRequired,
156
100
  interpretationId: PropTypes.string.isRequired,
157
101
  isVisualizationLoading: PropTypes.bool.isRequired,
158
102
  pluginComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
@@ -160,7 +104,6 @@ InterpretationModal.propTypes = {
160
104
  onClose: PropTypes.func.isRequired,
161
105
  onResponsesReceived: PropTypes.func.isRequired,
162
106
  downloadMenuComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
163
- initialFocus: PropTypes.bool,
164
- onInterpretationUpdate: PropTypes.func
107
+ initialFocus: PropTypes.bool
165
108
  };
166
109
  export { InterpretationModal };
@@ -6,34 +6,33 @@ import moment from 'moment';
6
6
  import PropTypes from 'prop-types';
7
7
  import React, { useRef, useEffect } from 'react';
8
8
  import { Interpretation, getInterpretationAccess } from '../common/index.js';
9
+ import { useInterpretationsCurrentUser } from '../InterpretationsProvider/hooks.js';
9
10
  import { Comment } from './Comment.js';
10
11
  import { CommentAddForm } from './CommentAddForm.js';
11
12
  const InterpretationThread = ({
12
- currentUser,
13
- fetching,
13
+ loading,
14
14
  interpretation,
15
15
  onInterpretationDeleted,
16
- onLikeToggled,
17
16
  initialFocus,
18
- onThreadUpdated,
19
17
  downloadMenuComponent: DownloadMenu,
20
18
  dashboardRedirectUrl
21
19
  }) => {
20
+ const currentUser = useInterpretationsCurrentUser();
22
21
  const {
23
22
  fromServerDate
24
23
  } = useTimeZoneConversion();
25
24
  const focusRef = useRef();
26
25
  useEffect(() => {
27
26
  if (initialFocus && focusRef.current) {
28
- window.requestAnimationFrame(() => {
27
+ window.setTimeout(() => {
29
28
  focusRef.current.focus();
30
- });
29
+ }, 25);
31
30
  }
32
31
  }, [initialFocus]);
33
32
  const interpretationAccess = getInterpretationAccess(interpretation, currentUser);
34
33
  return /*#__PURE__*/React.createElement("div", {
35
34
  className: "jsx-3292109121" + " " + (cx('container', {
36
- fetching,
35
+ fetching: loading,
37
36
  dashboard: !!dashboardRedirectUrl
38
37
  }) || "")
39
38
  }, /*#__PURE__*/React.createElement("div", {
@@ -46,44 +45,32 @@ const InterpretationThread = ({
46
45
  }), /*#__PURE__*/React.createElement("div", {
47
46
  className: "jsx-3292109121" + " " + 'thread'
48
47
  }, /*#__PURE__*/React.createElement(Interpretation, {
49
- currentUser: currentUser,
50
- interpretation: interpretation,
51
- onLikeToggled: onLikeToggled,
48
+ id: interpretation.id,
52
49
  onReplyIconClick: interpretationAccess.comment ? () => {
53
50
  var _focusRef$current;
54
51
  return (_focusRef$current = focusRef.current) === null || _focusRef$current === void 0 ? void 0 : _focusRef$current.focus();
55
52
  } : null,
56
- onUpdated: () => onThreadUpdated(true),
57
- onDeleted: onInterpretationDeleted,
58
53
  dashboardRedirectUrl: dashboardRedirectUrl,
59
- isInThread: true
54
+ isInThread: true,
55
+ onDeleted: onInterpretationDeleted
60
56
  }), /*#__PURE__*/React.createElement("div", {
61
57
  className: "jsx-3292109121" + " " + 'comments'
62
58
  }, interpretation.comments.map(comment => /*#__PURE__*/React.createElement(Comment, {
63
59
  key: comment.id,
64
60
  comment: comment,
65
- currentUser: currentUser,
66
- interpretationId: interpretation.id,
67
- onThreadUpdated: onThreadUpdated,
68
61
  canComment: interpretationAccess.comment
69
62
  })))), interpretationAccess.comment && /*#__PURE__*/React.createElement(CommentAddForm, {
70
- currentUser: currentUser,
71
- interpretationId: interpretation.id,
72
- onSave: () => onThreadUpdated(true),
73
63
  focusRef: focusRef
74
64
  }), /*#__PURE__*/React.createElement(_JSXStyle, {
75
65
  id: "3292109121"
76
66
  }, [".thread.jsx-3292109121{margin-top:var(--spacers-dp16);overflow-y:auto;-webkit-scroll-behavior:smooth;-moz-scroll-behavior:smooth;-ms-scroll-behavior:smooth;scroll-behavior:smooth;}", ".dashboard.jsx-3292109121 .thread.jsx-3292109121{overflow-y:hidden;}", ".container.jsx-3292109121{position:relative;overflow:auto;max-height:calc(100vh - 285px);display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;}", ".container.dashboard.jsx-3292109121{max-height:none;}", ".container.fetching.jsx-3292109121::before{content:'';position:absolute;inset:0px;background-color:rgba(255,255,255,0.8);}", ".container.fetching.jsx-3292109121::after{content:'';position:absolute;top:calc(50% - 12px);left:calc(50% - 12px);width:24px;height:24px;border-width:4px;border-style:solid;border-color:rgba(110,122,138,0.15) rgba(110,122,138,0.15) rgb(20,124,215);border-image:initial;border-radius:50%;-webkit-animation:1s linear 0s infinite normal none running rotation-jsx-3292109121;animation:1s linear 0s infinite normal none running rotation-jsx-3292109121;}", ".title.jsx-3292109121{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:var(--spacers-dp8);color:var(--colors-grey900);font-size:14px;line-height:18px;}", ".comments.jsx-3292109121{padding-left:16px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-top:var(--spacers-dp4);gap:var(--spacers-dp4);}", "@-webkit-keyframes rotation-jsx-3292109121{0%{-webkit-transform:rotate(0);-ms-transform:rotate(0);transform:rotate(0);}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}", "@keyframes rotation-jsx-3292109121{0%{-webkit-transform:rotate(0);-ms-transform:rotate(0);transform:rotate(0);}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}"]));
77
67
  };
78
68
  InterpretationThread.propTypes = {
79
- currentUser: PropTypes.object.isRequired,
80
- fetching: PropTypes.bool.isRequired,
81
69
  interpretation: PropTypes.object.isRequired,
70
+ loading: PropTypes.bool.isRequired,
82
71
  onInterpretationDeleted: PropTypes.func.isRequired,
83
- onLikeToggled: PropTypes.func.isRequired,
84
72
  dashboardRedirectUrl: PropTypes.string,
85
73
  downloadMenuComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
86
- initialFocus: PropTypes.bool,
87
- onThreadUpdated: PropTypes.func
74
+ initialFocus: PropTypes.bool
88
75
  };
89
76
  export { InterpretationThread };
@@ -0,0 +1,268 @@
1
+ import { groupInterpretationIdsByDate } from './groupInterpretationIdsByDate.js';
2
+ export class InterpretationsManager {
3
+ constructor(dataEngine, currentUser) {
4
+ if (!dataEngine || !currentUser) {
5
+ throw new Error('Initialised InterpretationsManager without dataEngine or currentUser');
6
+ }
7
+ this.query = dataEngine.query.bind(dataEngine);
8
+ this.mutate = dataEngine.mutate.bind(dataEngine);
9
+ this.currentUser = currentUser;
10
+ this.currentVisualizationId = null;
11
+ this.currentType = null;
12
+ this.interpretations = new Map();
13
+ this.activeInterpretationId = null;
14
+ this.interpretationsListCallback = null;
15
+ this.interpretationObservers = new Map();
16
+ }
17
+ getInterpretation(id) {
18
+ const interpretation = this.interpretations.get(id);
19
+ if (!interpretation) {
20
+ throw new Error(`Could not get interpretation with id ${id}`);
21
+ }
22
+ return interpretation;
23
+ }
24
+ getCurrentUser() {
25
+ return this.currentUser;
26
+ }
27
+ getActiveInterpretation() {
28
+ const activeInterpretation = this.interpretations.get(this.activeInterpretationId);
29
+ if (!activeInterpretation) {
30
+ throw new Error('There currently is no active interpretation');
31
+ }
32
+ return activeInterpretation;
33
+ }
34
+ subscribeToInterpretationsListUpdates(callback) {
35
+ this.interpretationsListCallback = callback;
36
+
37
+ // return cleanup function for useEffect hooks
38
+ return () => {
39
+ this.interpretationsListCallback = null;
40
+ };
41
+ }
42
+ subscribeToInterpretationUpdates(id, callback) {
43
+ // create callback Set if needed on the fly
44
+ if (!this.interpretationObservers.has(id)) {
45
+ this.interpretationObservers.set(id, new Set());
46
+ }
47
+ this.interpretationObservers.get(id).add(callback);
48
+
49
+ // return cleanup function for useEffect hooks
50
+ return () => {
51
+ this.interpretationObservers.get(id).delete(callback);
52
+ };
53
+ }
54
+ notifyInterpretationsListObserver() {
55
+ if (this.interpretationsListCallback) {
56
+ this.interpretationsListCallback(groupInterpretationIdsByDate(Array.from(this.interpretations.values())));
57
+ }
58
+ }
59
+ notifyInterpretationObservers(id) {
60
+ const callbacks = this.interpretationObservers.get(id);
61
+ if (callbacks) {
62
+ for (const callback of callbacks) {
63
+ callback(this.getInterpretation(id));
64
+ }
65
+ }
66
+ }
67
+ clearActiveInterpretation() {
68
+ this.activeInterpretationId = null;
69
+ }
70
+ clearInterpretations() {
71
+ this.clearActiveInterpretation();
72
+ this.currentVisualizationId = null;
73
+ this.currentType = null;
74
+ this.interpretations.clear();
75
+ }
76
+ resetInterpretations(newInterpretations) {
77
+ this.interpretations.clear();
78
+ for (const interpretation of newInterpretations) {
79
+ this.interpretations.set(interpretation.id, interpretation);
80
+ }
81
+ }
82
+ async fetchInterpretationDetails(id) {
83
+ const result = await this.query({
84
+ interpretation: {
85
+ resource: 'interpretations',
86
+ id,
87
+ params: {
88
+ fields: ['access[write,manage]', 'comments[id,text,created,createdBy[id,displayName]]', 'created', 'createdBy[id,displayName]', 'id', 'likedBy', 'likes', 'text']
89
+ }
90
+ }
91
+ });
92
+ return result.interpretation;
93
+ }
94
+ async fetchInterpretationsList() {
95
+ if (!this.currentType || !this.currentVisualizationId) {
96
+ throw new Error('Called fetchInterpretationsList before currentType or currentVisualizationId was set');
97
+ }
98
+ const result = await this.query({
99
+ interpretations: {
100
+ resource: 'interpretations',
101
+ params: {
102
+ fields: ['access[write,manage]', 'comments[id]', 'created', 'createdBy[id,displayName]', 'id', 'likedBy[id]', 'likes', 'text'],
103
+ filter: `${this.currentType}.id:eq:${this.currentVisualizationId}`,
104
+ paging: false
105
+ }
106
+ }
107
+ });
108
+ return result.interpretations.interpretations;
109
+ }
110
+ async loadActiveInterpretation(id) {
111
+ const interpretation = await this.fetchInterpretationDetails(id);
112
+ this.interpretations.set(id, interpretation);
113
+ this.activeInterpretationId = id;
114
+ return this.getActiveInterpretation();
115
+ }
116
+ async loadInterpretationsForVisualization(type, id) {
117
+ this.currentType = type;
118
+ this.currentVisualizationId = id;
119
+ const interpretations = await this.fetchInterpretationsList();
120
+ this.resetInterpretations(interpretations);
121
+ return groupInterpretationIdsByDate(interpretations);
122
+ }
123
+ async createInterpretation({
124
+ type,
125
+ id,
126
+ text,
127
+ onComplete
128
+ }) {
129
+ await this.mutate({
130
+ resource: `interpretations/${type}/${id}`,
131
+ type: 'create',
132
+ data: text
133
+ }, {
134
+ onComplete
135
+ });
136
+ /* since the create request does not return an ID we must refetch the list
137
+ * and cannot return the created interpretation */
138
+ const interpretations = await this.fetchInterpretationsList();
139
+ this.resetInterpretations(interpretations);
140
+ this.notifyInterpretationsListObserver();
141
+ return null;
142
+ }
143
+ async deleteInterpretation({
144
+ id,
145
+ onComplete,
146
+ onError
147
+ }) {
148
+ await this.mutate({
149
+ resource: 'interpretations',
150
+ id,
151
+ type: 'delete'
152
+ }, {
153
+ onComplete,
154
+ onError
155
+ });
156
+ // This happens when deleting the interpretation from the modal
157
+ if (this.activeInterpretationId && id === this.activeInterpretationId) {
158
+ this.clearActiveInterpretation();
159
+ }
160
+ this.interpretations.delete(id);
161
+ this.notifyInterpretationsListObserver();
162
+ return null;
163
+ }
164
+ async updateInterpretationText({
165
+ id,
166
+ text,
167
+ onComplete,
168
+ onError
169
+ }) {
170
+ await this.mutate({
171
+ resource: 'interpretations',
172
+ type: 'update',
173
+ partial: false,
174
+ id,
175
+ data: text
176
+ }, {
177
+ onComplete,
178
+ onError
179
+ });
180
+ const updatedInterpretation = {
181
+ ...this.getInterpretation(id),
182
+ text
183
+ };
184
+ this.interpretations.set(id, updatedInterpretation);
185
+ this.notifyInterpretationObservers(id);
186
+ return updatedInterpretation;
187
+ }
188
+ async toggleInterpretationLike(id) {
189
+ const interpretation = this.getInterpretation(id);
190
+ const wasLikedByCurrentUser = interpretation.likedBy.some(likedBy => likedBy.id === this.currentUser.id);
191
+ await this.mutate({
192
+ resource: `interpretations/${id}/like`,
193
+ type: wasLikedByCurrentUser ? 'delete' : 'create'
194
+ });
195
+ const isLikedByCurrentUser = !wasLikedByCurrentUser;
196
+ const updatedInterpretation = {
197
+ ...interpretation,
198
+ likedBy: isLikedByCurrentUser ? interpretation.likedBy.concat({
199
+ id: this.currentUser.id
200
+ }) : interpretation.likedBy.filter(lb => lb.id !== this.currentUser.id),
201
+ likes: isLikedByCurrentUser ? interpretation.likes + 1 : interpretation.likes - 1
202
+ };
203
+ this.interpretations.set(id, updatedInterpretation);
204
+ this.notifyInterpretationObservers(id);
205
+ return updatedInterpretation;
206
+ }
207
+ async addCommentToActiveInterpretation({
208
+ text,
209
+ onComplete
210
+ }) {
211
+ const {
212
+ id
213
+ } = this.getActiveInterpretation();
214
+ await this.mutate({
215
+ resource: `interpretations/${id}/comments`,
216
+ type: 'create',
217
+ data: text
218
+ }, {
219
+ onComplete
220
+ });
221
+ const interpretation = await this.fetchInterpretationDetails(id);
222
+ this.interpretations.set(id, interpretation);
223
+ this.notifyInterpretationObservers(id);
224
+ return interpretation;
225
+ }
226
+ async deleteCommentFromActiveInterpretation({
227
+ id
228
+ }) {
229
+ const activeInterpretation = this.getActiveInterpretation();
230
+ await this.mutate({
231
+ resource: `interpretations/${activeInterpretation.id}/comments/${id}`,
232
+ type: 'delete'
233
+ });
234
+ const updatedInterpretation = {
235
+ ...activeInterpretation,
236
+ comments: activeInterpretation.comments.filter(({
237
+ id: commentId
238
+ }) => commentId !== id)
239
+ };
240
+ this.interpretations.set(activeInterpretation.id, updatedInterpretation);
241
+ this.notifyInterpretationObservers(activeInterpretation.id);
242
+ return updatedInterpretation;
243
+ }
244
+ async updateCommentForActiveInterpretation({
245
+ id,
246
+ text,
247
+ onComplete
248
+ }) {
249
+ const activeInterpretation = this.getActiveInterpretation();
250
+ this.mutate({
251
+ resource: `interpretations/${activeInterpretation.id}/comments/${id}`,
252
+ type: 'update',
253
+ partial: false,
254
+ data: text
255
+ }, {
256
+ onComplete: () => onComplete(text)
257
+ });
258
+ const updatedInterpretation = {
259
+ ...activeInterpretation,
260
+ comments: activeInterpretation.comments.map(comment => comment.id === id ? {
261
+ ...comment,
262
+ text
263
+ } : comment)
264
+ };
265
+ this.interpretations.set(activeInterpretation.id, updatedInterpretation);
266
+ return updatedInterpretation;
267
+ }
268
+ }
@@ -0,0 +1,19 @@
1
+ import { useDataEngine } from '@dhis2/app-runtime';
2
+ import PropTypes from 'prop-types';
3
+ import React, { createContext, useState } from 'react';
4
+ import { InterpretationsManager } from './InterpretationsManager.js';
5
+ export const InterpretationsContext = /*#__PURE__*/createContext(null);
6
+ export const InterpretationsProvider = ({
7
+ currentUser,
8
+ children
9
+ }) => {
10
+ const dataEngine = useDataEngine();
11
+ const [interpretationsManager] = useState(() => new InterpretationsManager(dataEngine, currentUser));
12
+ return /*#__PURE__*/React.createElement(InterpretationsContext.Provider, {
13
+ value: interpretationsManager
14
+ }, children);
15
+ };
16
+ InterpretationsProvider.propTypes = {
17
+ children: PropTypes.node,
18
+ currentUser: PropTypes.object
19
+ };
@@ -0,0 +1,35 @@
1
+ import { groupInterpretationIdsByDate } from '../groupInterpretationIdsByDate.js';
2
+ describe('groupInterpretationIdsByDate', () => {
3
+ it('should group interpretations by date and sort them correctly', () => {
4
+ const interpretations = [{
5
+ id: 'id1',
6
+ created: '2025-09-04T07:47:12.477'
7
+ }, {
8
+ id: 'id2',
9
+ created: '2025-09-04T15:30:45.123'
10
+ }, {
11
+ id: 'id3',
12
+ created: '2025-09-03T10:20:30.456'
13
+ }, {
14
+ id: 'id4',
15
+ created: '2025-09-05T09:15:22.789'
16
+ }, {
17
+ id: 'id5',
18
+ created: '2025-09-03T18:45:15.012'
19
+ }];
20
+ const result = groupInterpretationIdsByDate(interpretations);
21
+
22
+ // Check that dates are grouped correctly
23
+ expect(Object.keys(result)).toEqual(['2025-09-05', '2025-09-04', '2025-09-03']);
24
+
25
+ // Check that within each date group, items are sorted from most recent to oldest
26
+ expect(result['2025-09-04']).toEqual(['id2', 'id1']); // 15:30 before 07:47
27
+ expect(result['2025-09-03']).toEqual(['id5', 'id3']); // 18:45 before 10:20
28
+ expect(result['2025-09-05']).toEqual(['id4']);
29
+ });
30
+ it('should handle empty array', () => {
31
+ const interpretations = [];
32
+ const result = groupInterpretationIdsByDate(interpretations);
33
+ expect(result).toEqual({});
34
+ });
35
+ });