@dhis2/analytics 26.13.4 → 27.0.1

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.
@@ -1,75 +1,25 @@
1
1
  import _JSXStyle from "styled-jsx/style";
2
- import { useDataMutation } from '@dhis2/app-runtime';
3
2
  import { Modal, ModalTitle, ModalContent, ModalActions, ButtonStrip, Button, InputField, TextAreaField } from '@dhis2/ui';
4
3
  import PropTypes from 'prop-types';
5
- import React, { useMemo, useState } from 'react';
4
+ import React, { useState } from 'react';
6
5
  import i18n from '../../locales/index.js';
7
6
  import { modalStyles } from './FileMenu.styles.js';
8
- import { supportedFileTypes, endpointFromFileType, labelForFileType } from './utils.js';
9
- const formatPayload = (name, description) => {
10
- const payload = [{
11
- op: 'add',
12
- path: '/name',
13
- value: name
14
- }];
15
- if (description) {
16
- payload.push({
17
- op: 'add',
18
- path: '/description',
19
- value: description
20
- });
21
- }
22
- return payload;
23
- };
24
- const getMutation = type => ({
25
- resource: endpointFromFileType(type),
26
- id: _ref => {
27
- let {
28
- id
29
- } = _ref;
30
- return id;
31
- },
32
- type: 'json-patch',
33
- data: _ref2 => {
34
- let {
35
- name,
36
- description
37
- } = _ref2;
38
- return formatPayload(name, description);
39
- }
40
- });
41
- export const RenameDialog = _ref3 => {
7
+ import { supportedFileTypes, labelForFileType } from './utils.js';
8
+ export const RenameDialog = _ref => {
42
9
  let {
43
10
  type,
44
11
  object,
45
12
  onClose,
46
- onRename,
47
- onError
48
- } = _ref3;
13
+ onRename
14
+ } = _ref;
49
15
  const [name, setName] = useState(object.name);
50
16
  const [description, setDescription] = useState(object.description);
51
- const mutation = useMemo(() => getMutation(type), [type]);
52
- const [mutate, {
53
- loading
54
- }] = useDataMutation(mutation, {
55
- onError: error => {
56
- onError(error);
57
- onClose();
58
- },
59
- onComplete: () => {
60
- onRename({
61
- name,
62
- description
63
- });
64
- onClose();
65
- }
66
- });
67
17
  const renameObject = () => {
68
- mutate({
69
- id: object.id,
18
+ onRename({
70
19
  name,
71
20
  description
72
21
  });
22
+ onClose();
73
23
  };
74
24
  return /*#__PURE__*/React.createElement(Modal, {
75
25
  onClose: onClose,
@@ -82,49 +32,43 @@ export const RenameDialog = _ref3 => {
82
32
  className: `jsx-${modalStyles.__hash}` + " " + "modal-content"
83
33
  }, /*#__PURE__*/React.createElement(InputField, {
84
34
  label: i18n.t('Name'),
85
- disabled: loading,
86
35
  required: true,
87
36
  value: name,
88
- onChange: _ref4 => {
37
+ onChange: _ref2 => {
89
38
  let {
90
39
  value
91
- } = _ref4;
40
+ } = _ref2;
92
41
  return setName(value);
93
42
  },
94
43
  dataTest: "file-menu-rename-modal-name"
95
44
  }), /*#__PURE__*/React.createElement(TextAreaField, {
96
45
  label: i18n.t('Description'),
97
- disabled: loading,
98
46
  value: description,
99
47
  rows: 3,
100
- onChange: _ref5 => {
48
+ onChange: _ref3 => {
101
49
  let {
102
50
  value
103
- } = _ref5;
51
+ } = _ref3;
104
52
  return setDescription(value);
105
53
  },
106
54
  dataTest: "file-menu-rename-modal-description"
107
55
  }))), /*#__PURE__*/React.createElement(ModalActions, null, /*#__PURE__*/React.createElement(ButtonStrip, null, /*#__PURE__*/React.createElement(Button, {
108
56
  onClick: onClose,
109
- disabled: loading,
110
57
  secondary: true,
111
58
  dataTest: "file-menu-rename-modal-cancel"
112
59
  }, i18n.t('Cancel')), /*#__PURE__*/React.createElement(Button, {
113
60
  onClick: renameObject,
114
- disabled: loading,
115
61
  primary: true,
116
62
  dataTest: "file-menu-rename-modal-rename"
117
63
  }, i18n.t('Rename')))));
118
64
  };
119
65
  RenameDialog.propTypes = {
120
- id: PropTypes.string,
121
66
  object: PropTypes.shape({
122
- id: PropTypes.string.isRequired,
123
67
  description: PropTypes.string,
68
+ id: PropTypes.string,
124
69
  name: PropTypes.string
125
70
  }),
126
71
  type: PropTypes.oneOf(supportedFileTypes),
127
72
  onClose: PropTypes.func,
128
- onError: PropTypes.func,
129
73
  onRename: PropTypes.func
130
74
  };
@@ -1,51 +1,83 @@
1
- import { Button, Modal, ModalTitle } from '@dhis2/ui';
2
- import { shallow } from 'enzyme';
1
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
2
+ import '@testing-library/jest-dom';
3
+ import { render, fireEvent, screen, within } from '@testing-library/react';
3
4
  import React from 'react';
4
5
  import { RenameDialog } from '../RenameDialog.js';
5
6
  describe('The FileMenu - RenameDialog component', () => {
6
- let shallowRenameDialog;
7
- let props;
8
7
  const onClose = jest.fn();
9
- const getRenameDialogComponent = props => {
10
- if (!shallowRenameDialog) {
11
- shallowRenameDialog = shallow(/*#__PURE__*/React.createElement(RenameDialog, props));
12
- }
13
- return shallowRenameDialog;
8
+ const onRename = jest.fn();
9
+ const props = {
10
+ type: 'visualization',
11
+ object: {
12
+ id: 'rename-test'
13
+ },
14
+ onClose,
15
+ onRename
14
16
  };
15
17
  beforeEach(() => {
16
- shallowRenameDialog = undefined;
17
- props = {
18
- type: 'visualization',
19
- object: {
20
- id: 'rename-test'
21
- },
22
- onClose
23
- };
24
- });
25
- it('renders a Modal component', () => {
26
- expect(getRenameDialogComponent(props).find(Modal)).toHaveLength(1);
18
+ jest.resetAllMocks();
19
+ jest.clearAllMocks();
27
20
  });
28
- it('renders a ModalTitle containing the type prop', () => {
29
- expect(getRenameDialogComponent(props).find(ModalTitle).childAt(0).text()).toEqual(`Rename ${props.type}`);
21
+ it('renders a Modal component with the correct heading', () => {
22
+ render(/*#__PURE__*/React.createElement(RenameDialog, props));
23
+ expect(screen.getAllByTestId('file-menu-rename-modal')).toHaveLength(1);
24
+ expect(screen.getByRole('heading')).toHaveTextContent('Rename visualization');
30
25
  });
31
26
  it('renders a InputField for name', () => {
32
- expect(getRenameDialogComponent(props).findWhere(n => n.prop('label') === 'Name')).toHaveLength(1);
27
+ render(/*#__PURE__*/React.createElement(RenameDialog, props));
28
+ expect(screen.getByTestId('file-menu-rename-modal-name')).toBeInTheDocument();
29
+ expect(screen.getByText('Name')).toBeInTheDocument();
30
+ expect(screen.getByText('Name')).toBeVisible();
33
31
  });
34
32
  it('renders a InputField for name with prefilled value if name is in object prop', () => {
35
- props.object.name = 'Name test';
36
- const nameInputField = getRenameDialogComponent(props).findWhere(n => n.prop('label') === 'Name');
37
- expect(nameInputField.prop('value')).toEqual(props.object.name);
33
+ render(/*#__PURE__*/React.createElement(RenameDialog, _extends({}, props, {
34
+ object: {
35
+ ...props.object,
36
+ name: 'Vis test'
37
+ }
38
+ })));
39
+ const ancestorElement = screen.getByTestId('file-menu-rename-modal-name');
40
+ const inputElement = within(ancestorElement).getByRole('textbox');
41
+ expect(inputElement).toBeInTheDocument();
42
+ expect(inputElement).toHaveValue('Vis test');
38
43
  });
39
44
  it('renders a TextAreaField for description', () => {
40
- expect(getRenameDialogComponent(props).findWhere(n => n.prop('label') === 'Description')).toHaveLength(1);
45
+ render(/*#__PURE__*/React.createElement(RenameDialog, props));
46
+
47
+ // Locate the label by its text
48
+ const labelElement = screen.getByText('Description');
49
+
50
+ // Find the textarea element within the same container as the label
51
+ const descriptionField = labelElement.closest('div').querySelector('textarea');
52
+ expect(descriptionField).toBeInTheDocument();
53
+ expect(descriptionField).toBeVisible();
41
54
  });
42
55
  it('renders a TextAreaField for description with prefilled value if description is in object prop', () => {
43
- props.object.description = 'Description test';
44
- const descriptionInputField = getRenameDialogComponent(props).findWhere(n => n.prop('label') === 'Description');
45
- expect(descriptionInputField.prop('value')).toEqual(props.object.description);
56
+ render(/*#__PURE__*/React.createElement(RenameDialog, _extends({}, props, {
57
+ object: {
58
+ ...props.object,
59
+ description: 'Long explanation of the visualization'
60
+ }
61
+ })));
62
+
63
+ // Locate the label by its text
64
+ const labelElement = screen.getByText('Description');
65
+
66
+ // Find the textarea element within the same container as the label
67
+ const descriptionField = labelElement.closest('div').querySelector('textarea');
68
+ expect(descriptionField).toBeInTheDocument();
69
+ expect(descriptionField).toHaveValue('Long explanation of the visualization');
70
+ });
71
+ it('calls the onClose callback when the Cancel button is clicked', async () => {
72
+ render(/*#__PURE__*/React.createElement(RenameDialog, props));
73
+ await fireEvent.click(screen.getByTestId('file-menu-rename-modal-cancel'));
74
+ expect(onClose).toHaveBeenCalled();
75
+ expect(onRename).not.toHaveBeenCalled();
46
76
  });
47
- it('calls the onClose callback when the Cancel button is clicked', () => {
48
- getRenameDialogComponent(props).find(Button).first().simulate('click');
77
+ it('calls the onRename callback when the Rename button is clicked', async () => {
78
+ render(/*#__PURE__*/React.createElement(RenameDialog, props));
79
+ await fireEvent.click(screen.getByTestId('file-menu-rename-modal-rename'));
80
+ expect(onRename).toHaveBeenCalled();
49
81
  expect(onClose).toHaveBeenCalled();
50
82
  });
51
83
  });
@@ -0,0 +1,237 @@
1
+ import { preparePayloadForSaveAs, preparePayloadForSave } from '../utils.js';
2
+ describe('utils', () => {
3
+ describe('preparePayloadForSaveAs', () => {
4
+ it('removes unnecessary properties from the visualization object', () => {
5
+ const visualization = {
6
+ id: '123',
7
+ created: '2023-01-01',
8
+ createdBy: 'user1',
9
+ user: 'user2',
10
+ name: 'Existing Name',
11
+ description: 'Existing Description',
12
+ type: 'PIVOT_TABLE'
13
+ };
14
+ const result = preparePayloadForSaveAs({
15
+ visualization
16
+ });
17
+ expect(result).not.toHaveProperty('id');
18
+ expect(result).not.toHaveProperty('created');
19
+ expect(result).not.toHaveProperty('createdBy');
20
+ expect(result).not.toHaveProperty('user');
21
+ });
22
+ it('sets the name to the provided name', () => {
23
+ const visualization = {
24
+ type: 'PIVOT_TABLE'
25
+ };
26
+ const name = 'New Name';
27
+ const result = preparePayloadForSaveAs({
28
+ visualization,
29
+ name
30
+ });
31
+ expect(result.name).toBe(name);
32
+ });
33
+ it('sets the name to the existing name if no new name is provided', () => {
34
+ const visualization = {
35
+ name: 'Existing Name',
36
+ type: 'MAP'
37
+ };
38
+ const result = preparePayloadForSaveAs({
39
+ visualization
40
+ });
41
+ expect(result.name).toBe('Existing Name');
42
+ });
43
+ it('sets the name to a default value if no name is provided', () => {
44
+ const visualization = {
45
+ type: 'LINE_LIST'
46
+ };
47
+ const result = preparePayloadForSaveAs({
48
+ visualization
49
+ });
50
+ const expectedName = `Untitled Line list, ${new Date().toLocaleDateString(undefined, {
51
+ year: 'numeric',
52
+ month: 'short',
53
+ day: '2-digit'
54
+ })}`;
55
+ expect(result.name).toBe(expectedName);
56
+ });
57
+ it('sets the description to the provided description', () => {
58
+ const visualization = {
59
+ description: 'Existing Description',
60
+ type: 'PIVOT_TABLE'
61
+ };
62
+ const description = 'New Description';
63
+ const result = preparePayloadForSaveAs({
64
+ visualization,
65
+ description
66
+ });
67
+ expect(result.description).toBe(description);
68
+ });
69
+ it('keeps the existing description if no new description is provided', () => {
70
+ const visualization = {
71
+ description: 'Existing Description',
72
+ type: 'PIVOT_TABLE'
73
+ };
74
+ const result = preparePayloadForSaveAs({
75
+ visualization
76
+ });
77
+ expect(result.description).toBe('Existing Description');
78
+ });
79
+ it('sets the description to undefined if no description is provided and none exists', () => {
80
+ const visualization = {
81
+ type: 'PIVOT_TABLE'
82
+ };
83
+ const result = preparePayloadForSaveAs({
84
+ visualization
85
+ });
86
+ expect(result.description).toBeUndefined();
87
+ });
88
+ });
89
+ describe('preparePayloadForSave', () => {
90
+ const mockEngine = {
91
+ query: jest.fn()
92
+ };
93
+ beforeEach(() => {
94
+ jest.clearAllMocks();
95
+ });
96
+ it('fetches subscribers and adds them to the visualization', async () => {
97
+ const visualization = {
98
+ id: '123',
99
+ type: 'BAR',
100
+ name: 'Existing Name',
101
+ description: 'Existing Description'
102
+ };
103
+ mockEngine.query.mockResolvedValue({
104
+ ao: {
105
+ subscribers: ['user1', 'user2']
106
+ }
107
+ });
108
+ const result = await preparePayloadForSave({
109
+ visualization,
110
+ engine: mockEngine
111
+ });
112
+ expect(mockEngine.query).toHaveBeenCalledWith({
113
+ ao: {
114
+ resource: 'visualizations',
115
+ id: expect.any(Function),
116
+ params: {
117
+ fields: 'subscribers'
118
+ }
119
+ }
120
+ }, {
121
+ variables: {
122
+ id: '123'
123
+ }
124
+ });
125
+ expect(result.subscribers).toEqual(['user1', 'user2']);
126
+ });
127
+ it('sets the name to the provided name', async () => {
128
+ const visualization = {
129
+ id: '123',
130
+ type: 'MAP',
131
+ name: 'Existing name'
132
+ };
133
+ const name = 'New Name';
134
+ mockEngine.query.mockResolvedValue({
135
+ ao: {
136
+ subscribers: []
137
+ }
138
+ });
139
+ const result = await preparePayloadForSave({
140
+ visualization,
141
+ name,
142
+ engine: mockEngine
143
+ });
144
+ expect(result.name).toBe(name);
145
+ });
146
+ it('sets the name to the existing name if no new name is provided', async () => {
147
+ const visualization = {
148
+ id: '123',
149
+ type: 'LINE_LIST',
150
+ name: 'Existing Name'
151
+ };
152
+ mockEngine.query.mockResolvedValue({
153
+ ao: {
154
+ subscribers: []
155
+ }
156
+ });
157
+ const result = await preparePayloadForSave({
158
+ visualization,
159
+ engine: mockEngine
160
+ });
161
+ expect(result.name).toBe('Existing Name');
162
+ });
163
+ it('sets the name to a default value if no name is provided', async () => {
164
+ const visualization = {
165
+ id: '123',
166
+ type: 'BAR'
167
+ };
168
+ mockEngine.query.mockResolvedValue({
169
+ ao: {
170
+ subscribers: []
171
+ }
172
+ });
173
+ const result = await preparePayloadForSave({
174
+ visualization,
175
+ engine: mockEngine
176
+ });
177
+ const expectedName = `Untitled Bar, ${new Date().toLocaleDateString(undefined, {
178
+ year: 'numeric',
179
+ month: 'short',
180
+ day: '2-digit'
181
+ })}`;
182
+ expect(result.name).toBe(expectedName);
183
+ });
184
+ it('sets the description to the provided description', async () => {
185
+ const visualization = {
186
+ id: '123',
187
+ type: 'YEAR_OVER_YEAR_LINE',
188
+ description: 'Existing Description'
189
+ };
190
+ const description = 'New Description';
191
+ mockEngine.query.mockResolvedValue({
192
+ ao: {
193
+ subscribers: []
194
+ }
195
+ });
196
+ const result = await preparePayloadForSave({
197
+ visualization,
198
+ description,
199
+ engine: mockEngine
200
+ });
201
+ expect(result.description).toBe(description);
202
+ });
203
+ it('keeps the existing description if no new description is provided', async () => {
204
+ const visualization = {
205
+ id: '123',
206
+ type: 'COLUMN',
207
+ description: 'Existing Description'
208
+ };
209
+ mockEngine.query.mockResolvedValue({
210
+ ao: {
211
+ subscribers: []
212
+ }
213
+ });
214
+ const result = await preparePayloadForSave({
215
+ visualization,
216
+ engine: mockEngine
217
+ });
218
+ expect(result.description).toBe('Existing Description');
219
+ });
220
+ it('sets the description to undefined if no description is provided and none exists', async () => {
221
+ const visualization = {
222
+ id: '123',
223
+ type: 'BAR'
224
+ };
225
+ mockEngine.query.mockResolvedValue({
226
+ ao: {
227
+ subscribers: []
228
+ }
229
+ });
230
+ const result = await preparePayloadForSave({
231
+ visualization,
232
+ engine: mockEngine
233
+ });
234
+ expect(result.description).toBeUndefined();
235
+ });
236
+ });
237
+ });
@@ -1,4 +1,5 @@
1
1
  import i18n from '@dhis2/d2-i18n';
2
+ import { getDisplayNameByVisType, getApiEndpointByVisType } from '../../modules/visTypes.js';
2
3
  export const FILE_TYPE_EVENT_REPORT = 'eventReport';
3
4
  export const FILE_TYPE_VISUALIZATION = 'visualization';
4
5
  export const FILE_TYPE_MAP = 'map';
@@ -34,11 +35,65 @@ export const appPathFor = (fileType, id, apiVersion) => {
34
35
  };
35
36
  export const preparePayloadForSaveAs = _ref => {
36
37
  let {
37
- ...visualization
38
+ visualization,
39
+ name,
40
+ description
38
41
  } = _ref;
39
42
  delete visualization.id;
40
43
  delete visualization.created;
41
44
  delete visualization.createdBy;
42
45
  delete visualization.user;
46
+ visualization.name = name || visualization.name || i18n.t('Untitled {{visualizationType}}, {{date}}', {
47
+ visualizationType: getDisplayNameByVisType(visualization.type),
48
+ date: new Date().toLocaleDateString(undefined, {
49
+ year: 'numeric',
50
+ month: 'short',
51
+ day: '2-digit'
52
+ })
53
+ });
54
+ visualization.description = description !== undefined ? description : visualization.description;
55
+ return visualization;
56
+ };
57
+ const getSubscriberQuery = type => ({
58
+ ao: {
59
+ resource: getApiEndpointByVisType(type),
60
+ id: _ref2 => {
61
+ let {
62
+ id
63
+ } = _ref2;
64
+ return id;
65
+ },
66
+ params: {
67
+ fields: 'subscribers'
68
+ }
69
+ }
70
+ });
71
+ const apiFetchAOSubscribers = (dataEngine, id, type) => {
72
+ return dataEngine.query(getSubscriberQuery(type), {
73
+ variables: {
74
+ id
75
+ }
76
+ });
77
+ };
78
+ export const preparePayloadForSave = async _ref3 => {
79
+ let {
80
+ visualization,
81
+ name,
82
+ description,
83
+ engine
84
+ } = _ref3;
85
+ const {
86
+ ao
87
+ } = await apiFetchAOSubscribers(engine, visualization.id, visualization.type);
88
+ visualization.subscribers = ao.subscribers;
89
+ visualization.name = name || visualization.name || i18n.t('Untitled {{visualizationType}}, {{date}}', {
90
+ visualizationType: getDisplayNameByVisType(visualization.type),
91
+ date: new Date().toLocaleDateString(undefined, {
92
+ year: 'numeric',
93
+ month: 'short',
94
+ day: '2-digit'
95
+ })
96
+ });
97
+ visualization.description = description !== undefined ? description : visualization.description;
43
98
  return visualization;
44
99
  };
package/build/es/index.js CHANGED
@@ -12,7 +12,7 @@ export { default as DimensionFilter } from './components/Filter/Filter.js';
12
12
  export { default as DimensionMenu } from './components/DimensionMenu.js';
13
13
  export { default as PivotTable } from './components/PivotTable/PivotTable.js';
14
14
  export { default as FileMenu } from './components/FileMenu/FileMenu.js';
15
- export { preparePayloadForSaveAs } from './components/FileMenu/utils.js';
15
+ export { preparePayloadForSaveAs, preparePayloadForSave } from './components/FileMenu/utils.js';
16
16
  export { default as VisTypeIcon } from './components/VisTypeIcon.js';
17
17
  export { default as LegendKey } from './components/LegendKey/LegendKey.js';
18
18
  export { default as AboutAOUnit } from './components/AboutAOUnit/AboutAOUnit.js';
@@ -99,7 +99,7 @@ export { getLayoutTypeByVisType } from './modules/visTypeToLayoutType.js';
99
99
 
100
100
  // Modules: visTypes
101
101
 
102
- export { VIS_TYPE_GROUP_ALL, VIS_TYPE_GROUP_CHARTS, VIS_TYPE_COLUMN, VIS_TYPE_STACKED_COLUMN, VIS_TYPE_BAR, VIS_TYPE_STACKED_BAR, VIS_TYPE_LINE, VIS_TYPE_AREA, VIS_TYPE_STACKED_AREA, VIS_TYPE_PIE, VIS_TYPE_RADAR, VIS_TYPE_GAUGE, VIS_TYPE_BUBBLE, VIS_TYPE_YEAR_OVER_YEAR_LINE, VIS_TYPE_YEAR_OVER_YEAR_COLUMN, VIS_TYPE_SINGLE_VALUE, VIS_TYPE_PIVOT_TABLE, VIS_TYPE_SCATTER, VIS_TYPE_LINE_LIST, VIS_TYPE_OUTLIER_TABLE, visTypeDisplayNames, visTypeIcons, getDisplayNameByVisType, defaultVisType, isStacked, isMultiType, isYearOverYear, isDualAxisType, isSingleValue, isOutlierTable, isTwoCategoryChartType, isLegendSetType, isColumnBasedType, isVerticalType } from './modules/visTypes.js';
102
+ export { VIS_TYPE_GROUP_ALL, VIS_TYPE_GROUP_CHARTS, VIS_TYPE_COLUMN, VIS_TYPE_STACKED_COLUMN, VIS_TYPE_BAR, VIS_TYPE_STACKED_BAR, VIS_TYPE_LINE, VIS_TYPE_AREA, VIS_TYPE_STACKED_AREA, VIS_TYPE_PIE, VIS_TYPE_RADAR, VIS_TYPE_GAUGE, VIS_TYPE_BUBBLE, VIS_TYPE_YEAR_OVER_YEAR_LINE, VIS_TYPE_YEAR_OVER_YEAR_COLUMN, VIS_TYPE_SINGLE_VALUE, VIS_TYPE_PIVOT_TABLE, VIS_TYPE_SCATTER, VIS_TYPE_LINE_LIST, VIS_TYPE_OUTLIER_TABLE, VIS_TYPE_MAP, visTypeDisplayNames, visTypeIcons, getDisplayNameByVisType, defaultVisType, isStacked, isMultiType, isYearOverYear, isDualAxisType, isSingleValue, isOutlierTable, isTwoCategoryChartType, isLegendSetType, isColumnBasedType, isVerticalType } from './modules/visTypes.js';
103
103
 
104
104
  // Modules: layoutTypes
105
105
 
@@ -162,6 +162,7 @@
162
162
  "line list": "line list",
163
163
  "map": "map",
164
164
  "visualization": "visualization",
165
+ "Untitled {{visualizationType}}, {{date}}": "Untitled {{visualizationType}}, {{date}}",
165
166
  "Edit": "Edit",
166
167
  "Write a reply": "Write a reply",
167
168
  "Post reply": "Post reply",
@@ -451,6 +452,7 @@
451
452
  "Single value": "Single value",
452
453
  "Outlier table": "Outlier table",
453
454
  "All charts": "All charts",
455
+ "Map": "Map",
454
456
  "{{seriesName}} (trend)": "{{seriesName}} (trend)",
455
457
  "Trend": "Trend",
456
458
  "No legend for this series": "No legend for this series",
@@ -243,10 +243,10 @@ export class PivotTableEngine {
243
243
  rawValue = parseValue(rawValue);
244
244
  switch (this.visualization.numberType) {
245
245
  case NUMBER_TYPE_ROW_PERCENTAGE:
246
- renderedValue = rawValue / this.percentageTotals[row].value;
246
+ rawCell.pctValue = renderedValue = rawValue / this.percentageTotals[row].value;
247
247
  break;
248
248
  case NUMBER_TYPE_COLUMN_PERCENTAGE:
249
- renderedValue = rawValue / this.percentageTotals[column].value;
249
+ rawCell.pctValue = renderedValue = rawValue / this.percentageTotals[column].value;
250
250
  break;
251
251
  default:
252
252
  break;
@@ -258,6 +258,19 @@ export class PivotTableEngine {
258
258
  rawCell.rawValue = rawValue;
259
259
  rawCell.renderedValue = renderedValue;
260
260
  }
261
+
262
+ // show the original value in the tooltip
263
+ if ([NUMBER_TYPE_COLUMN_PERCENTAGE, NUMBER_TYPE_ROW_PERCENTAGE].includes(this.visualization.numberType)) {
264
+ rawCell.titleValue = i18n.t('Value: {{value}}', {
265
+ value: renderValue(rawCell.rawValue, valueType,
266
+ // force VALUE for formatting the original value
267
+ {
268
+ ...this.visualization,
269
+ numberType: NUMBER_TYPE_VALUE
270
+ }),
271
+ nsSeparator: '^^'
272
+ });
273
+ }
261
274
  if ([CELL_TYPE_TOTAL, CELL_TYPE_SUBTOTAL].includes(rawCell.cellType) && rawCell.rawValue === AGGREGATE_TYPE_NA) {
262
275
  rawCell.titleValue = i18n.t('Not applicable');
263
276
  }
@@ -950,7 +963,11 @@ export class PivotTableEngine {
950
963
  if (!valueB || valueB.empty) {
951
964
  return 1 * order;
952
965
  }
953
- if (valueA.valueType === VALUE_TYPE_NUMBER && valueB.valueType === VALUE_TYPE_NUMBER) {
966
+ if (
967
+ // for percentage strings, use the pctValue (percentage value) in the sort comparison
968
+ [NUMBER_TYPE_ROW_PERCENTAGE, NUMBER_TYPE_COLUMN_PERCENTAGE].includes(this.visualization.numberType)) {
969
+ return (valueA.pctValue - valueB.pctValue) * order;
970
+ } else if (valueA.valueType === VALUE_TYPE_NUMBER && valueB.valueType === VALUE_TYPE_NUMBER) {
954
971
  return (valueA.rawValue - valueB.rawValue) * order;
955
972
  }
956
973
  return valueA.renderedValue.localeCompare(valueB.renderedValue) * order;