@dhis2/analytics 24.10.0 → 25.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 (86) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/build/cjs/__demo__/CalculationModal.stories.js +448 -0
  3. package/build/cjs/api/analytics/AnalyticsRequest.js +12 -1
  4. package/build/cjs/api/dimensions.js +1 -1
  5. package/build/cjs/api/expression.js +67 -0
  6. package/build/cjs/assets/DimensionItemIcons/CalculationIcon.js +25 -0
  7. package/build/cjs/assets/FormulaIcon.js +40 -0
  8. package/build/cjs/components/DataDimension/Calculation/CalculationModal.js +447 -0
  9. package/build/cjs/components/DataDimension/Calculation/DataElementOption.js +77 -0
  10. package/build/cjs/components/DataDimension/Calculation/DataElementSelector.js +306 -0
  11. package/build/cjs/components/DataDimension/Calculation/DndContext.js +213 -0
  12. package/build/cjs/components/DataDimension/Calculation/DragHandleIcon.js +23 -0
  13. package/build/cjs/components/DataDimension/Calculation/DraggingItem.js +58 -0
  14. package/build/cjs/components/DataDimension/Calculation/DropZone.js +58 -0
  15. package/build/cjs/components/DataDimension/Calculation/FormulaField.js +121 -0
  16. package/build/cjs/components/DataDimension/Calculation/FormulaItem.js +232 -0
  17. package/build/cjs/components/DataDimension/Calculation/MathOperatorSelector.js +57 -0
  18. package/build/cjs/components/DataDimension/Calculation/Operator.js +81 -0
  19. package/build/cjs/components/DataDimension/Calculation/styles/CalculationModal.style.js +13 -0
  20. package/build/cjs/components/DataDimension/Calculation/styles/DataElementOption.style.js +13 -0
  21. package/build/cjs/components/DataDimension/Calculation/styles/DataElementSelector.style.js +13 -0
  22. package/build/cjs/components/DataDimension/Calculation/styles/DraggingItem.style.js +13 -0
  23. package/build/cjs/components/DataDimension/Calculation/styles/DropZone.style.js +13 -0
  24. package/build/cjs/components/DataDimension/Calculation/styles/FormulaField.style.js +13 -0
  25. package/build/cjs/components/DataDimension/Calculation/styles/FormulaItem.style.js +13 -0
  26. package/build/cjs/components/DataDimension/Calculation/styles/MathOperatorSelector.style.js +13 -0
  27. package/build/cjs/components/DataDimension/Calculation/styles/Operator.style.js +13 -0
  28. package/build/cjs/components/DataDimension/DataDimension.js +22 -6
  29. package/build/cjs/components/DataDimension/DataTypeSelector.js +5 -3
  30. package/build/cjs/components/DataDimension/ItemSelector.js +111 -73
  31. package/build/cjs/components/TransferOption.js +13 -4
  32. package/build/cjs/components/styles/DimensionSelector.style.js +2 -2
  33. package/build/cjs/components/styles/TransferOption.style.js +2 -2
  34. package/build/cjs/index.js +6 -0
  35. package/build/cjs/locales/en/translations.json +32 -7
  36. package/build/cjs/modules/__tests__/expressions.spec.js +139 -0
  37. package/build/cjs/modules/__tests__/hash.spec.js +92 -0
  38. package/build/cjs/modules/__tests__/parseExpression.spec.js +46 -0
  39. package/build/cjs/modules/dataTypes.js +8 -1
  40. package/build/cjs/modules/dimensionListItem.js +82 -0
  41. package/build/cjs/modules/expressions.js +164 -0
  42. package/build/cjs/modules/hash.js +28 -0
  43. package/build/cjs/visualizations/config/generators/dhis/singleValue.js +2 -2
  44. package/build/es/__demo__/CalculationModal.stories.js +440 -0
  45. package/build/es/api/analytics/AnalyticsRequest.js +11 -1
  46. package/build/es/api/dimensions.js +1 -1
  47. package/build/es/api/expression.js +57 -0
  48. package/build/es/assets/DimensionItemIcons/CalculationIcon.js +13 -0
  49. package/build/es/assets/FormulaIcon.js +30 -0
  50. package/build/es/components/DataDimension/Calculation/CalculationModal.js +418 -0
  51. package/build/es/components/DataDimension/Calculation/DataElementOption.js +60 -0
  52. package/build/es/components/DataDimension/Calculation/DataElementSelector.js +280 -0
  53. package/build/es/components/DataDimension/Calculation/DndContext.js +194 -0
  54. package/build/es/components/DataDimension/Calculation/DragHandleIcon.js +11 -0
  55. package/build/es/components/DataDimension/Calculation/DraggingItem.js +40 -0
  56. package/build/es/components/DataDimension/Calculation/DropZone.js +43 -0
  57. package/build/es/components/DataDimension/Calculation/FormulaField.js +98 -0
  58. package/build/es/components/DataDimension/Calculation/FormulaItem.js +207 -0
  59. package/build/es/components/DataDimension/Calculation/MathOperatorSelector.js +41 -0
  60. package/build/es/components/DataDimension/Calculation/Operator.js +64 -0
  61. package/build/es/components/DataDimension/Calculation/styles/CalculationModal.style.js +4 -0
  62. package/build/es/components/DataDimension/Calculation/styles/DataElementOption.style.js +4 -0
  63. package/build/es/components/DataDimension/Calculation/styles/DataElementSelector.style.js +4 -0
  64. package/build/es/components/DataDimension/Calculation/styles/DraggingItem.style.js +4 -0
  65. package/build/es/components/DataDimension/Calculation/styles/DropZone.style.js +4 -0
  66. package/build/es/components/DataDimension/Calculation/styles/FormulaField.style.js +4 -0
  67. package/build/es/components/DataDimension/Calculation/styles/FormulaItem.style.js +4 -0
  68. package/build/es/components/DataDimension/Calculation/styles/MathOperatorSelector.style.js +4 -0
  69. package/build/es/components/DataDimension/Calculation/styles/Operator.style.js +4 -0
  70. package/build/es/components/DataDimension/DataDimension.js +21 -6
  71. package/build/es/components/DataDimension/DataTypeSelector.js +6 -4
  72. package/build/es/components/DataDimension/ItemSelector.js +111 -73
  73. package/build/es/components/TransferOption.js +14 -5
  74. package/build/es/components/styles/DimensionSelector.style.js +2 -2
  75. package/build/es/components/styles/TransferOption.style.js +2 -2
  76. package/build/es/index.js +1 -1
  77. package/build/es/locales/en/translations.json +32 -7
  78. package/build/es/modules/__tests__/expressions.spec.js +136 -0
  79. package/build/es/modules/__tests__/hash.spec.js +88 -0
  80. package/build/es/modules/__tests__/parseExpression.spec.js +43 -0
  81. package/build/es/modules/dataTypes.js +6 -0
  82. package/build/es/modules/dimensionListItem.js +61 -0
  83. package/build/es/modules/expressions.js +131 -0
  84. package/build/es/modules/hash.js +12 -0
  85. package/build/es/visualizations/config/generators/dhis/singleValue.js +2 -2
  86. package/package.json +6 -1
@@ -0,0 +1,280 @@
1
+ import _JSXStyle from "styled-jsx/style";
2
+ import { useDataEngine } from '@dhis2/app-runtime';
3
+ import { CircularLoader, InputField, IntersectionDetector, SingleSelectField, SingleSelectOption } from '@dhis2/ui';
4
+ import { useSortable } from '@dnd-kit/sortable';
5
+ import { useDebounceCallback } from '@react-hook/debounce';
6
+ import cx from 'classnames';
7
+ import PropTypes from 'prop-types';
8
+ import React, { useEffect, useRef, useState } from 'react';
9
+ import { apiFetchOptions, apiFetchGroups } from '../../../api/dimensions.js';
10
+ import i18n from '../../../locales/index.js';
11
+ import { TOTALS, DETAIL, DIMENSION_TYPE_ALL, DIMENSION_TYPE_DATA_ELEMENT, dataTypeMap as dataTypes } from '../../../modules/dataTypes.js';
12
+ import DataElementOption from './DataElementOption.js';
13
+ import styles from './styles/DataElementSelector.style.js';
14
+
15
+ const getOptions = () => ({
16
+ [TOTALS]: i18n.t('Totals only'),
17
+ [DETAIL]: i18n.t('Details only')
18
+ });
19
+
20
+ const GroupSelector = _ref => {
21
+ var _dataTypes$DIMENSION_;
22
+
23
+ let {
24
+ currentValue,
25
+ onChange,
26
+ displayNameProp
27
+ } = _ref;
28
+ const dataEngine = useDataEngine();
29
+ const [loading, setLoading] = useState(true);
30
+ const [groups, setGroups] = useState([]);
31
+ const defaultGroup = (_dataTypes$DIMENSION_ = dataTypes[DIMENSION_TYPE_DATA_ELEMENT]) === null || _dataTypes$DIMENSION_ === void 0 ? void 0 : _dataTypes$DIMENSION_.defaultGroup;
32
+ useEffect(() => {
33
+ const fetchGroups = async () => {
34
+ setLoading(true);
35
+ const result = await apiFetchGroups(dataEngine, DIMENSION_TYPE_DATA_ELEMENT, displayNameProp);
36
+ setGroups(result);
37
+ setLoading(false);
38
+ };
39
+
40
+ fetchGroups();
41
+ }, [dataEngine, displayNameProp]);
42
+ return /*#__PURE__*/React.createElement("div", {
43
+ className: "jsx-".concat(styles.__hash) + " " + "group-select"
44
+ }, /*#__PURE__*/React.createElement(SingleSelectField, {
45
+ selected: currentValue,
46
+ onChange: ref => onChange(ref.selected),
47
+ dense: true,
48
+ loading: loading,
49
+ loadingText: i18n.t('Loading'),
50
+ dataTest: 'data-element-group-select'
51
+ }, defaultGroup ? /*#__PURE__*/React.createElement(SingleSelectOption, {
52
+ value: defaultGroup.id,
53
+ key: defaultGroup.id,
54
+ label: defaultGroup.getName()
55
+ }) : null, !loading ? groups.map(group => /*#__PURE__*/React.createElement(SingleSelectOption, {
56
+ value: group.id,
57
+ key: group.id,
58
+ label: group.name
59
+ })) : null), /*#__PURE__*/React.createElement(_JSXStyle, {
60
+ id: styles.__hash
61
+ }, styles));
62
+ };
63
+
64
+ GroupSelector.propTypes = {
65
+ currentValue: PropTypes.string.isRequired,
66
+ displayNameProp: PropTypes.string.isRequired,
67
+ onChange: PropTypes.func.isRequired
68
+ };
69
+
70
+ const DisaggregationSelector = _ref2 => {
71
+ let {
72
+ currentValue,
73
+ onChange
74
+ } = _ref2;
75
+ const options = getOptions();
76
+ return /*#__PURE__*/React.createElement("div", {
77
+ className: "jsx-".concat(styles.__hash) + " " + "group-select"
78
+ }, /*#__PURE__*/React.createElement(SingleSelectField, {
79
+ selected: currentValue,
80
+ onChange: ref => onChange(ref.selected),
81
+ dense: true,
82
+ dataTest: 'data-element-disaggregation-select'
83
+ }, Object.entries(options).map(option => /*#__PURE__*/React.createElement(SingleSelectOption, {
84
+ value: option[0],
85
+ key: option[0],
86
+ label: option[1]
87
+ }))), /*#__PURE__*/React.createElement(_JSXStyle, {
88
+ id: styles.__hash
89
+ }, styles));
90
+ };
91
+
92
+ DisaggregationSelector.propTypes = {
93
+ currentValue: PropTypes.string.isRequired,
94
+ onChange: PropTypes.func.isRequired
95
+ };
96
+
97
+ const DataElementSelector = _ref3 => {
98
+ let {
99
+ displayNameProp,
100
+ onDoubleClick
101
+ } = _ref3;
102
+ const dataEngine = useDataEngine();
103
+ const [searchTerm, setSearchTerm] = useState('');
104
+ const [group, setGroup] = useState(DIMENSION_TYPE_ALL);
105
+ const [subGroup, setSubGroup] = useState(TOTALS);
106
+ const [options, setOptions] = useState([]);
107
+ const [loading, setLoading] = useState(true);
108
+ const {
109
+ isSorting
110
+ } = useSortable({});
111
+ const rootRef = useRef();
112
+ const hasNextPageRef = useRef(false);
113
+ const searchTermRef = useRef(searchTerm);
114
+ const pageRef = useRef(0);
115
+ const filterRef = useRef({
116
+ dataType: DIMENSION_TYPE_DATA_ELEMENT,
117
+ group,
118
+ subGroup
119
+ });
120
+
121
+ const fetchData = async scrollToTop => {
122
+ try {
123
+ setLoading(true);
124
+ const result = await apiFetchOptions({
125
+ dataEngine,
126
+ nameProp: displayNameProp,
127
+ filter: filterRef.current,
128
+ searchTerm: searchTermRef.current,
129
+ page: pageRef.current
130
+ });
131
+
132
+ if (result !== null && result !== void 0 && result.dimensionItems) {
133
+ const newOptions = result.dimensionItems.map(item => ({
134
+ label: item.name,
135
+ value: item.id,
136
+ type: item.dimensionItemType,
137
+ expression: item.expression
138
+ }));
139
+ setOptions(prevOptions => pageRef.current > 1 ? [...prevOptions, ...newOptions] : newOptions);
140
+ setLoading(false);
141
+ }
142
+
143
+ hasNextPageRef.current = result !== null && result !== void 0 && result.nextPage ? true : false;
144
+ } catch (error) {
145
+ // TODO handle errors
146
+ console.log('apiFetchOptions error: ', error);
147
+ } finally {
148
+ if (scrollToTop) {
149
+ rootRef.current.scrollTo({
150
+ top: 0
151
+ });
152
+ }
153
+ }
154
+ };
155
+
156
+ const debouncedFetchData = useDebounceCallback(newSearchTerm => {
157
+ hasNextPageRef.current = false;
158
+ pageRef.current = 1;
159
+ searchTermRef.current = newSearchTerm;
160
+ fetchData(true);
161
+ }, 500);
162
+
163
+ const onSearchChange = _ref4 => {
164
+ let {
165
+ value
166
+ } = _ref4;
167
+ const newSearchTerm = value;
168
+ setSearchTerm(newSearchTerm); // debounce the fetch
169
+
170
+ debouncedFetchData(newSearchTerm);
171
+ };
172
+
173
+ const onFilterChange = newFilter => {
174
+ if (newFilter.group) {
175
+ setGroup(newFilter.group);
176
+ filterRef.current.group = newFilter.group;
177
+ }
178
+
179
+ if (newFilter.subGroup) {
180
+ setSubGroup(newFilter.subGroup);
181
+ filterRef.current.subGroup = newFilter.subGroup;
182
+ }
183
+
184
+ hasNextPageRef.current = false;
185
+ pageRef.current = 1;
186
+ fetchData(true);
187
+ };
188
+
189
+ const onEndReached = _ref5 => {
190
+ let {
191
+ isIntersecting
192
+ } = _ref5;
193
+
194
+ if (isIntersecting) {
195
+ // if hasNextPage is set it means at least 1 request already happened and there is
196
+ // another page, fetch the next page
197
+ if (hasNextPageRef.current) {
198
+ pageRef.current += 1;
199
+ fetchData();
200
+ } else if (pageRef.current === 0) {
201
+ // this is for fetching the initial page
202
+ pageRef.current = 1;
203
+ fetchData();
204
+ }
205
+ }
206
+ };
207
+
208
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
209
+ className: "jsx-".concat(styles.__hash) + " " + "filter-wrapper"
210
+ }, /*#__PURE__*/React.createElement("h4", {
211
+ className: "jsx-".concat(styles.__hash) + " " + "sub-header"
212
+ }, i18n.t('Data elements')), /*#__PURE__*/React.createElement(InputField, {
213
+ value: searchTerm,
214
+ onChange: onSearchChange,
215
+ placeholder: i18n.t('Search by data element name'),
216
+ dense: true,
217
+ type: 'search',
218
+ dataTest: 'data-element-search'
219
+ }), /*#__PURE__*/React.createElement("div", {
220
+ className: "jsx-".concat(styles.__hash) + " " + "selector-wrapper"
221
+ }, /*#__PURE__*/React.createElement(GroupSelector, {
222
+ currentValue: group,
223
+ onChange: group => onFilterChange({
224
+ group
225
+ }),
226
+ displayNameProp: displayNameProp
227
+ }), /*#__PURE__*/React.createElement(DisaggregationSelector, {
228
+ currentValue: subGroup,
229
+ onChange: subGroup => onFilterChange({
230
+ subGroup
231
+ })
232
+ }))), /*#__PURE__*/React.createElement("div", {
233
+ className: "jsx-".concat(styles.__hash) + " " + "dimension-list-container"
234
+ }, loading && /*#__PURE__*/React.createElement("div", {
235
+ className: "jsx-".concat(styles.__hash) + " " + "dimension-list-overlay"
236
+ }, /*#__PURE__*/React.createElement(CircularLoader, null)), /*#__PURE__*/React.createElement("div", {
237
+ ref: rootRef,
238
+ onScroll: () => {
239
+ if (isSorting) {
240
+ rootRef.current.scrollTo({
241
+ top: 0
242
+ });
243
+ }
244
+ },
245
+ className: "jsx-".concat(styles.__hash) + " " + "dimension-list-scrollbox"
246
+ }, /*#__PURE__*/React.createElement("div", {
247
+ "data-test": "dimension-list",
248
+ className: "jsx-".concat(styles.__hash) + " " + (cx('dimension-list-scroller', {
249
+ loading
250
+ }) || "")
251
+ }, Boolean(options.length) && options.map(_ref6 => {
252
+ let {
253
+ label,
254
+ value
255
+ } = _ref6;
256
+ return /*#__PURE__*/React.createElement(DataElementOption, {
257
+ key: value,
258
+ label: label,
259
+ value: value,
260
+ onDoubleClick: onDoubleClick
261
+ });
262
+ }), !loading && !options.length && /*#__PURE__*/React.createElement("div", {
263
+ className: "jsx-".concat(styles.__hash) + " " + "empty-list"
264
+ }, searchTermRef.current ? i18n.t('No data elements found for "{{- searchTerm}}"', {
265
+ searchTerm: searchTermRef.current
266
+ }) : i18n.t('No data elements found')), /*#__PURE__*/React.createElement("div", {
267
+ className: "jsx-".concat(styles.__hash) + " " + "scroll-detector"
268
+ }, /*#__PURE__*/React.createElement(IntersectionDetector, {
269
+ onChange: onEndReached,
270
+ rootRef: rootRef
271
+ }))))), /*#__PURE__*/React.createElement(_JSXStyle, {
272
+ id: styles.__hash
273
+ }, styles));
274
+ };
275
+
276
+ DataElementSelector.propTypes = {
277
+ displayNameProp: PropTypes.string.isRequired,
278
+ onDoubleClick: PropTypes.func.isRequired
279
+ };
280
+ export default DataElementSelector;
@@ -0,0 +1,194 @@
1
+ function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
2
+
3
+ import { DndContext, DragOverlay, useSensor, useSensors, PointerSensor as DndKitPointerSensor } from '@dnd-kit/core';
4
+ import PropTypes from 'prop-types';
5
+ import React, { useState } from 'react';
6
+ import DraggingItem from './DraggingItem.js';
7
+ export const OPTIONS_PANEL = 'Sortable';
8
+
9
+ const getIntersectionRatio = (entry, target) => {
10
+ const top = Math.max(target.top, entry.top);
11
+ const left = Math.max(target.left, entry.left);
12
+ const right = Math.min(target.left + target.width, entry.left + entry.width);
13
+ const bottom = Math.min(target.top + target.height, entry.top + entry.height);
14
+ const width = right - left;
15
+ const height = bottom - top;
16
+
17
+ if (left < right && top < bottom) {
18
+ const targetArea = target.width * target.height;
19
+ const entryArea = entry.width * entry.height;
20
+ const intersectionArea = width * height;
21
+ const intersectionRatio = intersectionArea / (targetArea + entryArea - intersectionArea);
22
+ return Number(intersectionRatio.toFixed(4));
23
+ } // Rectangles do not overlap, or overlap has an area of zero (edge/corner overlap)
24
+
25
+
26
+ return 0;
27
+ };
28
+
29
+ const sortCollisionsDesc = (_ref, _ref2) => {
30
+ let {
31
+ data: {
32
+ value: a
33
+ }
34
+ } = _ref;
35
+ let {
36
+ data: {
37
+ value: b
38
+ }
39
+ } = _ref2;
40
+ return b - a;
41
+ };
42
+
43
+ const rectIntersectionCustom = _ref3 => {
44
+ let {
45
+ pointerCoordinates,
46
+ droppableContainers
47
+ } = _ref3;
48
+ // create a rect around the pointerCoords for calculating the intersection
49
+ const pointerRectWidth = 40;
50
+ const pointerRectHeight = 40;
51
+ const pointerRect = {
52
+ width: pointerRectWidth,
53
+ height: pointerRectHeight,
54
+ top: pointerCoordinates.y - pointerRectHeight / 2,
55
+ bottom: pointerCoordinates.y + pointerRectHeight / 2,
56
+ left: pointerCoordinates.x - pointerRectWidth / 2,
57
+ right: pointerCoordinates.x + pointerRectWidth / 2
58
+ };
59
+ const collisions = [];
60
+
61
+ for (const droppableContainer of droppableContainers) {
62
+ const {
63
+ id,
64
+ rect: {
65
+ current: rect
66
+ }
67
+ } = droppableContainer;
68
+
69
+ if (rect) {
70
+ const intersectionRatio = getIntersectionRatio(rect, pointerRect);
71
+
72
+ if (intersectionRatio > 0) {
73
+ collisions.push({
74
+ id,
75
+ data: {
76
+ droppableContainer,
77
+ value: intersectionRatio
78
+ }
79
+ });
80
+ }
81
+ }
82
+ }
83
+
84
+ return collisions.sort(sortCollisionsDesc);
85
+ };
86
+
87
+ const isInteractiveElement = el => {
88
+ const interactiveElements = ['button', 'input', 'textarea', 'select', 'option'];
89
+
90
+ if (interactiveElements.includes(el.tagName.toLowerCase())) {
91
+ return true;
92
+ }
93
+
94
+ return false;
95
+ }; // disable dragging if user is in an input
96
+
97
+
98
+ class PointerSensor extends DndKitPointerSensor {}
99
+
100
+ _defineProperty(PointerSensor, "activators", [{
101
+ eventName: 'onPointerDown',
102
+ handler: _ref7 => {
103
+ let {
104
+ nativeEvent: event
105
+ } = _ref7;
106
+
107
+ if (!event.isPrimary || event.button !== 0 || isInteractiveElement(event.target)) {
108
+ return false;
109
+ }
110
+
111
+ return true;
112
+ }
113
+ }]);
114
+
115
+ const OuterDndContext = _ref4 => {
116
+ let {
117
+ children,
118
+ onDragEnd,
119
+ onDragStart
120
+ } = _ref4;
121
+ const [draggingItem, setDraggingItem] = useState(null);
122
+ const sensor = useSensor(PointerSensor, {
123
+ activationConstraint: {
124
+ distance: 15
125
+ }
126
+ });
127
+ const sensors = useSensors(sensor);
128
+
129
+ const handleDragStart = _ref5 => {
130
+ let {
131
+ active
132
+ } = _ref5;
133
+ setDraggingItem(active.data.current);
134
+ onDragStart && onDragStart();
135
+ };
136
+
137
+ const handleDragCancel = () => {
138
+ setDraggingItem(null);
139
+ };
140
+
141
+ const handleDragEnd = _ref6 => {
142
+ var _over$data, _over$data$current, _over$data$current$so, _over$data$current2, _over$data$current3;
143
+
144
+ let {
145
+ active,
146
+ over
147
+ } = _ref6;
148
+
149
+ if (!(over !== null && over !== void 0 && over.id) || (over === null || over === void 0 ? void 0 : (_over$data = over.data) === null || _over$data === void 0 ? void 0 : (_over$data$current = _over$data.current) === null || _over$data$current === void 0 ? void 0 : (_over$data$current$so = _over$data$current.sortable) === null || _over$data$current$so === void 0 ? void 0 : _over$data$current$so.containerId) === OPTIONS_PANEL || !active.data.current) {
150
+ // dropped over non-droppable or over options panel
151
+ handleDragCancel();
152
+ return;
153
+ }
154
+
155
+ const item = {
156
+ id: active.id,
157
+ sourceContainerId: active.data.current.sortable.containerId,
158
+ sourceIndex: active.data.current.sortable.index,
159
+ data: {
160
+ label: active.data.current.label,
161
+ value: active.data.current.value,
162
+ type: active.data.current.type
163
+ }
164
+ };
165
+ const destination = {
166
+ containerId: ((_over$data$current2 = over.data.current) === null || _over$data$current2 === void 0 ? void 0 : _over$data$current2.sortable.containerId) || over.id,
167
+ index: (_over$data$current3 = over.data.current) === null || _over$data$current3 === void 0 ? void 0 : _over$data$current3.sortable.index
168
+ };
169
+ onDragEnd({
170
+ item,
171
+ destination
172
+ });
173
+ setDraggingItem(null);
174
+ };
175
+
176
+ return /*#__PURE__*/React.createElement(DndContext, {
177
+ collisionDetection: rectIntersectionCustom,
178
+ onDragStart: handleDragStart,
179
+ onDragEnd: handleDragEnd,
180
+ onDragCancel: handleDragCancel,
181
+ sensors: sensors
182
+ }, children, /*#__PURE__*/React.createElement(DragOverlay, {
183
+ dropAnimation: null
184
+ }, draggingItem ? /*#__PURE__*/React.createElement("span", {
185
+ className: "dragOverlay"
186
+ }, /*#__PURE__*/React.createElement(DraggingItem, draggingItem)) : null));
187
+ };
188
+
189
+ OuterDndContext.propTypes = {
190
+ onDragEnd: PropTypes.func.isRequired,
191
+ children: PropTypes.node,
192
+ onDragStart: PropTypes.func
193
+ };
194
+ export default OuterDndContext;
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ export default /*#__PURE__*/React.createElement("svg", {
3
+ height: "16",
4
+ viewBox: "0 0 16 16",
5
+ width: "16",
6
+ xmlns: "http://www.w3.org/2000/svg"
7
+ }, /*#__PURE__*/React.createElement("path", {
8
+ d: "M6 11a1 1 0 110 2 1 1 0 010-2zm4 0a1 1 0 110 2 1 1 0 010-2zM6 7a1 1 0 110 2 1 1 0 010-2zm4 0a1 1 0 110 2 1 1 0 010-2zM6 3a1 1 0 110 2 1 1 0 010-2zm4 0a1 1 0 110 2 1 1 0 010-2z",
9
+ fill: "#A0ADBA",
10
+ fillRule: "evenodd"
11
+ }));
@@ -0,0 +1,40 @@
1
+ import _JSXStyle from "styled-jsx/style";
2
+ import cx from 'classnames';
3
+ import PropTypes from 'prop-types';
4
+ import React from 'react';
5
+ import { DIMENSION_TYPE_DATA_ELEMENT } from '../../../modules/dataTypes.js';
6
+ import { getIcon } from '../../../modules/dimensionListItem.js';
7
+ import { EXPRESSION_TYPE_DATA, EXPRESSION_TYPE_NUMBER, EXPRESSION_TYPE_OPERATOR } from '../../../modules/expressions.js';
8
+ import styles from './styles/DraggingItem.style.js';
9
+ import formulaItemStyles from './styles/FormulaItem.style.js';
10
+
11
+ const DraggingItem = _ref => {
12
+ let {
13
+ label,
14
+ type,
15
+ value
16
+ } = _ref;
17
+ const displayLabel = type === EXPRESSION_TYPE_NUMBER ? value || label : label;
18
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
19
+ className: "jsx-".concat(styles.__hash, " jsx-").concat(formulaItemStyles.__hash) + " " + (cx('dragging', 'content', {
20
+ operator: type === EXPRESSION_TYPE_OPERATOR,
21
+ number: type === EXPRESSION_TYPE_NUMBER,
22
+ data: type === EXPRESSION_TYPE_DATA
23
+ }) || "")
24
+ }, type === EXPRESSION_TYPE_DATA && /*#__PURE__*/React.createElement("span", {
25
+ className: "jsx-".concat(styles.__hash, " jsx-").concat(formulaItemStyles.__hash) + " " + "icon"
26
+ }, getIcon(DIMENSION_TYPE_DATA_ELEMENT)), /*#__PURE__*/React.createElement("span", {
27
+ className: "jsx-".concat(styles.__hash, " jsx-").concat(formulaItemStyles.__hash) + " " + "label"
28
+ }, displayLabel)), /*#__PURE__*/React.createElement(_JSXStyle, {
29
+ id: styles.__hash
30
+ }, styles), /*#__PURE__*/React.createElement(_JSXStyle, {
31
+ id: formulaItemStyles.__hash
32
+ }, formulaItemStyles));
33
+ };
34
+
35
+ DraggingItem.propTypes = {
36
+ label: PropTypes.string,
37
+ type: PropTypes.string,
38
+ value: PropTypes.string
39
+ };
40
+ export default DraggingItem;
@@ -0,0 +1,43 @@
1
+ import _JSXStyle from "styled-jsx/style";
2
+ import { useDroppable } from '@dnd-kit/core';
3
+ import cx from 'classnames';
4
+ import PropTypes from 'prop-types';
5
+ import React from 'react';
6
+ import styles from './styles/DropZone.style.js';
7
+
8
+ const DropZone = _ref => {
9
+ let {
10
+ firstElementId,
11
+ overLastDropZone
12
+ } = _ref;
13
+ const {
14
+ isOver,
15
+ setNodeRef,
16
+ active
17
+ } = useDroppable({
18
+ id: 'firstdropzone'
19
+ });
20
+ let draggingOver = false;
21
+
22
+ if (overLastDropZone && !firstElementId) {
23
+ draggingOver = true;
24
+ } else {
25
+ draggingOver = firstElementId === (active === null || active === void 0 ? void 0 : active.id) ? false : isOver;
26
+ }
27
+
28
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
29
+ ref: setNodeRef,
30
+ className: "jsx-".concat(styles.__hash) + " " + (cx('first-dropzone', {
31
+ 'dragging-over': draggingOver,
32
+ empty: !firstElementId
33
+ }) || "")
34
+ }), /*#__PURE__*/React.createElement(_JSXStyle, {
35
+ id: styles.__hash
36
+ }, styles));
37
+ };
38
+
39
+ DropZone.propTypes = {
40
+ firstElementId: PropTypes.string,
41
+ overLastDropZone: PropTypes.bool
42
+ };
43
+ export default DropZone;
@@ -0,0 +1,98 @@
1
+ import _JSXStyle from "styled-jsx/style";
2
+ import i18n from '@dhis2/d2-i18n';
3
+ import { Center, CircularLoader } from '@dhis2/ui';
4
+ import { useDroppable } from '@dnd-kit/core';
5
+ import { SortableContext } from '@dnd-kit/sortable';
6
+ import PropTypes from 'prop-types';
7
+ import React from 'react';
8
+ import FormulaIcon from '../../../assets/FormulaIcon.js';
9
+ import DropZone from './DropZone.js';
10
+ import FormulaItem from './FormulaItem.js';
11
+ import styles from './styles/FormulaField.style.js';
12
+ export const LAST_DROPZONE_ID = 'lastdropzone';
13
+ export const FORMULA_BOX_ID = 'formulabox';
14
+
15
+ const Placeholder = () => /*#__PURE__*/React.createElement("div", {
16
+ "data-test": 'placeholder',
17
+ className: "jsx-".concat(styles.__hash) + " " + "placeholder"
18
+ }, /*#__PURE__*/React.createElement(FormulaIcon, null), /*#__PURE__*/React.createElement("span", {
19
+ className: "jsx-".concat(styles.__hash) + " " + "help-text"
20
+ }, i18n.t('Drag items here, or double click in the list, to start building a calculation formula')), /*#__PURE__*/React.createElement(_JSXStyle, {
21
+ id: styles.__hash
22
+ }, styles));
23
+
24
+ const FormulaField = _ref => {
25
+ let {
26
+ items = [],
27
+ selectedItemId,
28
+ focusItemId,
29
+ onChange,
30
+ onClick,
31
+ onDoubleClick,
32
+ loading
33
+ } = _ref;
34
+ const {
35
+ over,
36
+ setNodeRef: setLastDropzoneRef
37
+ } = useDroppable({
38
+ id: LAST_DROPZONE_ID
39
+ });
40
+ const itemIds = items.map(item => item.id);
41
+ const overLastDropZone = (over === null || over === void 0 ? void 0 : over.id) === LAST_DROPZONE_ID;
42
+ return /*#__PURE__*/React.createElement("div", {
43
+ className: "jsx-".concat(styles.__hash) + " " + "container"
44
+ }, /*#__PURE__*/React.createElement("div", {
45
+ className: "jsx-".concat(styles.__hash) + " " + "border"
46
+ }), /*#__PURE__*/React.createElement("div", {
47
+ ref: setLastDropzoneRef,
48
+ "data-test": 'formula-field',
49
+ className: "jsx-".concat(styles.__hash) + " " + "formula-field"
50
+ }, loading && /*#__PURE__*/React.createElement(Center, null, /*#__PURE__*/React.createElement(CircularLoader, {
51
+ small: true
52
+ })), !loading && itemIds && /*#__PURE__*/React.createElement(SortableContext, {
53
+ id: FORMULA_BOX_ID,
54
+ items: itemIds
55
+ }, /*#__PURE__*/React.createElement(DropZone, {
56
+ firstElementId: itemIds[0],
57
+ overLastDropZone: overLastDropZone
58
+ }), !items.length && /*#__PURE__*/React.createElement(Placeholder, null), Boolean(items.length) && items.map((_ref2, index) => {
59
+ let {
60
+ id,
61
+ label,
62
+ type,
63
+ value
64
+ } = _ref2;
65
+ return /*#__PURE__*/React.createElement(FormulaItem, {
66
+ key: id,
67
+ id: id,
68
+ label: label,
69
+ type: type,
70
+ value: value,
71
+ hasFocus: focusItemId === id,
72
+ isHighlighted: selectedItemId === id,
73
+ isLast: index === items.length - 1,
74
+ onChange: onChange,
75
+ onClick: onClick,
76
+ onDoubleClick: onDoubleClick,
77
+ overLastDropZone: overLastDropZone
78
+ });
79
+ }))), /*#__PURE__*/React.createElement(_JSXStyle, {
80
+ id: styles.__hash
81
+ }, styles));
82
+ };
83
+
84
+ FormulaField.propTypes = {
85
+ onChange: PropTypes.func.isRequired,
86
+ onClick: PropTypes.func.isRequired,
87
+ onDoubleClick: PropTypes.func.isRequired,
88
+ focusItemId: PropTypes.string,
89
+ items: PropTypes.arrayOf(PropTypes.shape({
90
+ id: PropTypes.string,
91
+ label: PropTypes.string,
92
+ type: PropTypes.string,
93
+ value: PropTypes.string
94
+ })),
95
+ loading: PropTypes.bool,
96
+ selectedItemId: PropTypes.string
97
+ };
98
+ export default FormulaField;