Skip to content

Commit 581faef

Browse files
authored
feat: support xlsx downloads (DHIS2-19748) (#3415)
1 parent 58c2d8b commit 581faef

File tree

8 files changed

+421
-19
lines changed

8 files changed

+421
-19
lines changed

i18n/en.pot

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ msgstr ""
55
"Content-Type: text/plain; charset=utf-8\n"
66
"Content-Transfer-Encoding: 8bit\n"
77
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
8-
"POT-Creation-Date: 2025-04-15T09:09:07.966Z\n"
9-
"PO-Revision-Date: 2025-04-15T09:09:07.966Z\n"
8+
"POT-Creation-Date: 2025-06-16T12:59:51.831Z\n"
9+
"PO-Revision-Date: 2025-06-16T12:59:51.831Z\n"
1010

1111
msgid "All items"
1212
msgstr "All items"
@@ -145,6 +145,9 @@ msgstr "Name"
145145
msgid "Table layout"
146146
msgstr "Table layout"
147147

148+
msgid "Excel (.xlsx)"
149+
msgstr "Excel (.xlsx)"
150+
148151
msgid "Excel (.xls)"
149152
msgstr "Excel (.xls)"
150153

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"@semantic-release/changelog": "^6",
2828
"@semantic-release/exec": "^6",
2929
"@semantic-release/git": "^10",
30+
"@testing-library/jest-dom": "^5.17.0",
31+
"@testing-library/react": "^12.1.5",
3032
"cypress": "^13.6.1",
3133
"cypress-tags": "^1.1.2",
3234
"enzyme": "^3.11.0",

src/components/DownloadMenu/DownloadMenu.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
VIS_TYPE_OUTLIER_TABLE,
44
HoverMenuList,
55
} from '@dhis2/analytics'
6+
import { useConfig } from '@dhis2/app-runtime'
67
import i18n from '@dhis2/d2-i18n'
78
import { FlyoutMenu, MenuSectionHeader } from '@dhis2/ui'
89
import PropTypes from 'prop-types'
@@ -11,6 +12,7 @@ import { AdvancedSubMenu } from './AdvancedSubMenu.js'
1112
import {
1213
FILE_FORMAT_CSV,
1314
FILE_FORMAT_XLS,
15+
FILE_FORMAT_XLSX,
1416
FILE_FORMAT_JSON,
1517
FILE_FORMAT_XML,
1618
} from './constants.js'
@@ -24,6 +26,7 @@ const DownloadMenu = ({
2426
onDownloadImage,
2527
hoverable,
2628
}) => {
29+
const config = useConfig()
2730
const MenuComponent = hoverable ? HoverMenuList : FlyoutMenu
2831

2932
return (
@@ -59,7 +62,12 @@ const DownloadMenu = ({
5962
hoverable={hoverable}
6063
onDownload={onDownloadData}
6164
label={i18n.t('Microsoft Excel')}
62-
format={FILE_FORMAT_XLS}
65+
format={
66+
// VERSION-TOGGLE: remove when 42 is the lowest supported version
67+
config.serverVersion.minor >= 42
68+
? FILE_FORMAT_XLSX
69+
: FILE_FORMAT_XLS
70+
}
6371
/>
6472
<PlainDataSourceSubMenu
6573
hoverable={hoverable}

src/components/DownloadMenu/TableMenu.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { HoverMenuListItem } from '@dhis2/analytics'
2+
import { useConfig } from '@dhis2/app-runtime'
23
import i18n from '@dhis2/d2-i18n'
34
import { MenuItem, MenuSectionHeader } from '@dhis2/ui'
45
import PropTypes from 'prop-types'
56
import React from 'react'
67
import {
78
DOWNLOAD_TYPE_TABLE,
89
FILE_FORMAT_XLS,
10+
FILE_FORMAT_XLSX,
911
FILE_FORMAT_CSV,
1012
FILE_FORMAT_HTML_CSS,
1113
} from './constants.js'
1214

1315
export const TableMenu = ({ hoverable, onDownload }) => {
16+
const config = useConfig()
1417
const MenuItemComponent = hoverable ? HoverMenuListItem : MenuItem
1518

1619
return (
@@ -23,11 +26,18 @@ export const TableMenu = ({ hoverable, onDownload }) => {
2326
/>
2427
<MenuItemComponent
2528
key="xls"
26-
label={i18n.t('Excel (.xls)')}
29+
label={
30+
config.serverVersion.minor >= 42
31+
? i18n.t('Excel (.xlsx)')
32+
: i18n.t('Excel (.xls)')
33+
}
2734
onClick={() =>
2835
onDownload({
2936
type: DOWNLOAD_TYPE_TABLE,
30-
format: FILE_FORMAT_XLS,
37+
format:
38+
config.serverVersion.minor >= 42
39+
? FILE_FORMAT_XLSX
40+
: FILE_FORMAT_XLS,
3141
})
3242
}
3343
/>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { VIS_TYPE_PIVOT_TABLE, VIS_TYPE_OUTLIER_TABLE } from '@dhis2/analytics'
2+
import { useConfig } from '@dhis2/app-runtime'
3+
import { render, fireEvent } from '@testing-library/react'
4+
import React from 'react'
5+
import { DOWNLOAD_TYPE_PLAIN, ID_SCHEME_UID } from '../constants.js'
6+
import { DownloadMenu } from '../DownloadMenu.js'
7+
8+
jest.mock('@dhis2/app-runtime', () => ({
9+
useConfig: jest.fn(() => ({
10+
serverVersion: { minor: 42 },
11+
})),
12+
useDhis2ConnectionStatus: jest.fn(() => ({
13+
isDisconnected: false,
14+
})),
15+
}))
16+
17+
describe('DownloadMenu component', () => {
18+
const downloadDataFn = jest.fn()
19+
const downloadImageFn = jest.fn()
20+
21+
it('renders the correct menu items for Pivot Table download', () => {
22+
const { getByText } = render(
23+
<DownloadMenu
24+
visType={VIS_TYPE_PIVOT_TABLE}
25+
onDownloadData={downloadDataFn}
26+
onDownloadImage={downloadImageFn}
27+
/>
28+
)
29+
30+
Array(
31+
'Excel (.xlsx)',
32+
'CSV (.csv)',
33+
'HTML (.html)',
34+
'JSON',
35+
'XML',
36+
'Microsoft Excel',
37+
'CSV',
38+
'Advanced'
39+
).forEach((label) => expect(getByText(label)).toBeTruthy())
40+
})
41+
42+
it('renders the correct menu items for Outlier Table download', () => {
43+
const { getByText } = render(
44+
<DownloadMenu
45+
visType={VIS_TYPE_OUTLIER_TABLE}
46+
onDownloadData={downloadDataFn}
47+
onDownloadImage={downloadImageFn}
48+
/>
49+
)
50+
51+
Array('JSON', 'Microsoft Excel', 'CSV').forEach((label) =>
52+
expect(getByText(label)).toBeTruthy()
53+
)
54+
})
55+
56+
test.each(
57+
[VIS_TYPE_PIVOT_TABLE, VIS_TYPE_OUTLIER_TABLE],
58+
'uses the correct format for Excel in 42',
59+
async (visType) => {
60+
const { getByText, findByText } = render(
61+
<DownloadMenu
62+
visType={visType}
63+
onDownloadData={downloadDataFn}
64+
onDownloadImage={downloadImageFn}
65+
/>
66+
)
67+
68+
visType === VIS_TYPE_PIVOT_TABLE &&
69+
expect(getByText('Excel (.xlsx)')).toBeTruthy()
70+
71+
fireEvent.click(getByText('Microsoft Excel'))
72+
73+
await findByText('ID')
74+
75+
expect(getByText('ID')).toBeTruthy()
76+
77+
fireEvent.click(getByText('ID'))
78+
79+
expect(downloadDataFn).toHaveBeenCalledWith({
80+
type: DOWNLOAD_TYPE_PLAIN,
81+
format: 'xlsx',
82+
idScheme: ID_SCHEME_UID,
83+
})
84+
}
85+
)
86+
87+
// VERSION-TOGGLE: remove when 42 is the lowest supported version
88+
test.each(
89+
[VIS_TYPE_PIVOT_TABLE, VIS_TYPE_OUTLIER_TABLE],
90+
'uses the correct format for Excel in 41',
91+
async (visType) => {
92+
useConfig.mockReturnValue({ serverVersion: { minor: 41 } })
93+
94+
const { getByText, findByText } = render(
95+
<DownloadMenu
96+
visType={visType}
97+
onDownloadData={downloadDataFn}
98+
onDownloadImage={downloadImageFn}
99+
/>
100+
)
101+
102+
visType === VIS_TYPE_PIVOT_TABLE &&
103+
expect(getByText('Excel (.xls)')).toBeTruthy()
104+
105+
fireEvent.click(getByText('Microsoft Excel'))
106+
107+
await findByText('ID')
108+
109+
expect(getByText('ID')).toBeTruthy()
110+
111+
fireEvent.click(getByText('ID'))
112+
113+
expect(downloadDataFn).toHaveBeenCalledWith({
114+
type: DOWNLOAD_TYPE_PLAIN,
115+
format: 'xls',
116+
idScheme: ID_SCHEME_UID,
117+
})
118+
}
119+
)
120+
})

src/components/DownloadMenu/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const FILE_FORMAT_JSON = 'json'
77
export const FILE_FORMAT_PDF = 'pdf'
88
export const FILE_FORMAT_PNG = 'png'
99
export const FILE_FORMAT_XLS = 'xls'
10+
export const FILE_FORMAT_XLSX = 'xlsx'
1011
export const FILE_FORMAT_XML = 'xml'
1112
export const FILE_FORMAT_SQL = 'sql'
1213
export const ID_SCHEME_UID = 'UID'

src/components/DownloadMenu/useDownload.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
FILE_FORMAT_HTML_CSS,
2424
FILE_FORMAT_CSV,
2525
FILE_FORMAT_XLS,
26+
FILE_FORMAT_XLSX,
2627
FILE_FORMAT_PDF,
2728
} from './constants.js'
2829

@@ -109,7 +110,11 @@ const useDownload = (relativePeriodDate) => {
109110
if (visType === VIS_TYPE_OUTLIER_TABLE) {
110111
// only DOWNLOAD_TYPE_PLAIN is enabled
111112
// open JSON in new tab
112-
target = [FILE_FORMAT_CSV, FILE_FORMAT_XLS].includes(format)
113+
target = [
114+
FILE_FORMAT_CSV,
115+
FILE_FORMAT_XLS,
116+
FILE_FORMAT_XLSX,
117+
].includes(format)
113118
? '_top'
114119
: '_blank'
115120

@@ -189,9 +194,11 @@ const useDownload = (relativePeriodDate) => {
189194
req = req.withOutputIdScheme(idScheme)
190195
}
191196

192-
target = [FILE_FORMAT_CSV, FILE_FORMAT_XLS].includes(
193-
format
194-
)
197+
target = [
198+
FILE_FORMAT_CSV,
199+
FILE_FORMAT_XLS,
200+
FILE_FORMAT_XLSX,
201+
].includes(format)
195202
? '_top'
196203
: '_blank'
197204
break

0 commit comments

Comments
 (0)