diff --git a/package.json b/package.json
index 40dc629d..0331e630 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"history": "^5.0.1",
"prop-types": "^15.7.2",
"query-string": "^7.0.1",
+ "react-html-parser": "^2.0.2",
"use-debounce": "^7.0.0"
}
}
diff --git a/src/app-context/app-provider.js b/src/app-context/app-provider.js
index e4b201e4..f0868dd3 100644
--- a/src/app-context/app-provider.js
+++ b/src/app-context/app-provider.js
@@ -23,7 +23,7 @@ const query = {
'displayName',
'dataApprovalLevels',
'periodType',
- 'dataSets[id,displayName,periodType]',
+ 'dataSets[id,displayName,periodType,formType]',
],
},
},
diff --git a/src/data-workspace/display/display.js b/src/data-workspace/display/display.js
index bc1d22c2..1d892dcb 100644
--- a/src/data-workspace/display/display.js
+++ b/src/data-workspace/display/display.js
@@ -9,6 +9,7 @@ import {
RetryButton,
} from '../../shared/index.js'
import styles from './display.module.css'
+import { TableCustomDataSet } from './table-custom-data-set.js'
import { Table } from './table.js'
const query = {
@@ -123,6 +124,20 @@ const Display = ({ dataSetId }) => {
)
}
+ if (selectedDataSet.formType === 'CUSTOM') {
+ return (
+
+ {tables.map((table) => (
+
h.name)}
+ rows={table.rows}
+ />
+ ))}
+
+ )
+ }
return (
{tables.map((table) => (
diff --git a/src/data-workspace/display/display.test.js b/src/data-workspace/display/display.test.js
index 916b917c..1c8e60d4 100644
--- a/src/data-workspace/display/display.test.js
+++ b/src/data-workspace/display/display.test.js
@@ -23,12 +23,6 @@ describe('
', () => {
periodType: 'Monthly',
}
- const dataSetThree = {
- displayName: 'Another',
- id: 'custom',
- periodType: 'Monthly',
- }
-
it('asks the user to select a data set if none is selected', () => {
render(
@@ -223,64 +217,163 @@ describe('', () => {
).toBeInTheDocument()
})
- it('renders one table per data set in the report', async () => {
- const data = {
- dataSetReport: [
- {
- title: 'Data set 1',
- headers: [{ name: 'Header 1' }, { name: 'Header 2' }],
- rows: [],
- },
- {
- title: 'Data set 2',
- headers: [{ name: 'Header 1' }, { name: 'Header 2' }],
- rows: [],
- },
- {
- title: 'Data set 3',
- headers: [{ name: 'Header 1' }, { name: 'Header 2' }],
- rows: [],
- },
- ],
- }
- render(
-
-
-
-
-
- )
+ describe('display for custom datasets', () => {
+ it('renders a table for a custom dataset with safely sanitised HTML and CSS', async () => {
+ const data = {
+ dataSetReport: [
+ {
+ title: 'Custom Data set',
+ headers: [
+ {
+ name: '2024/25',
+ column: '2024/25',
+ type: 'java.lang.String',
+ hidden: false,
+ meta: false,
+ },
+ {
+ name: 'NATIONAL DEPARTMENT OF HEALTH',
+ column: 'NATIONAL DEPARTMENT OF HEALTH',
+ type: 'java.lang.String',
+ hidden: false,
+ meta: false,
+ },
+ ],
+ rows: [
+ [
+ 'Programme 6: Performance Indicator',
+ ],
+ ],
+ },
+ ],
+ }
+ render(
+
+
+
+
+
+ )
- await waitForElementToBeRemoved(() => screen.getByRole('progressbar'))
+ await waitForElementToBeRemoved(() =>
+ screen.getByRole('progressbar')
+ )
- expect(await screen.findAllByRole('table')).toHaveLength(3)
- expect(
- await screen.queryByText(
- /This data set does not use a default form. The data is displayed as a simple grid below, but this might not be a suitable representation..*/
+ expect(screen.getByText('2024/25')).toHaveStyle({
+ color: 'rgb(0, 176, 80)',
+ })
+ expect(screen.getByText('2024/25').parentElement.tagName).toBe('B')
+
+ expect(
+ screen.getByText('NATIONAL DEPARTMENT OF HEALTH')
+ ).toHaveStyle({
+ color: 'black',
+ })
+
+ expect(
+ screen.getByText('Programme 6: Performance Indicator')
+ ).toHaveStyle({
+ color: 'black',
+ })
+ })
+
+ it('renders HTML and CSS encoded for non-custom dataset', async () => {
+ const data = {
+ dataSetReport: [
+ {
+ title: 'Custom Data set',
+ headers: [
+ {
+ name: 'NATIONAL DEPARTMENT OF HEALTH',
+ column: 'NATIONAL DEPARTMENT OF HEALTH',
+ type: 'java.lang.String',
+ hidden: false,
+ meta: false,
+ },
+ ],
+ rows: [
+ [
+ 'Programme 6: Performance Indicator',
+ ],
+ ],
+ },
+ ],
+ }
+ render(
+
+
+
+
+
+ )
+
+ await waitForElementToBeRemoved(() =>
+ screen.getByRole('progressbar')
)
- ).not.toBeInTheDocument()
+
+ expect(screen.getByRole('table')).toContainHTML(
+ '<span style="color:black">Programme 6: Performance Indicator</span>'
+ )
+ })
})
})
diff --git a/src/data-workspace/display/table-custom-data-set.js b/src/data-workspace/display/table-custom-data-set.js
new file mode 100644
index 00000000..c364a73f
--- /dev/null
+++ b/src/data-workspace/display/table-custom-data-set.js
@@ -0,0 +1,89 @@
+import {
+ DataTable,
+ TableHead,
+ DataTableRow,
+ DataTableColumnHeader,
+ TableBody,
+ DataTableCell,
+} from '@dhis2/ui'
+import PropTypes from 'prop-types'
+import React from 'react'
+import ReactHtmlParser from 'react-html-parser'
+import styles from './table.module.css'
+
+// Needs to have the same width as the table, so can't use the one from
+// @dhis2/ui
+const DataTableToolbar = ({ children, columns }) => (
+
+
+ {children}
+ |
+
+)
+
+DataTableToolbar.propTypes = {
+ children: PropTypes.any.isRequired,
+ columns: PropTypes.number.isRequired,
+}
+
+const TableCustomDataSet = ({ title, columns, rows }) => (
+ <>
+
+
+
+ {ReactHtmlParser(title)}
+
+
+
+
+ {ReactHtmlParser(columns[0])}
+
+
+
+ {columns.slice(1).map((column) => {
+ return (
+
+ {ReactHtmlParser(column)}
+
+ )
+ })}
+
+
+
+ {rows.map((row, index) => {
+ const [firstCell, ...cells] = row
+
+ return (
+
+
+
+ {ReactHtmlParser(firstCell)}
+
+
+
+ {cells.map((value, index) => (
+
+ {ReactHtmlParser(value)}
+
+ ))}
+
+ )
+ })}
+
+
+ >
+)
+
+TableCustomDataSet.propTypes = {
+ columns: PropTypes.array.isRequired,
+ rows: PropTypes.array.isRequired,
+ title: PropTypes.string.isRequired,
+}
+
+export { TableCustomDataSet }
diff --git a/yarn.lock b/yarn.lock
index d8f745c3..64e1f624 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7795,7 +7795,7 @@ domain-browser@^1.1.1, domain-browser@^1.2.0:
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
-domelementtype@1:
+domelementtype@1, domelementtype@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
@@ -7812,6 +7812,13 @@ domexception@^2.0.1:
dependencies:
webidl-conversions "^5.0.0"
+domhandler@^2.3.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+ integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+ dependencies:
+ domelementtype "1"
+
domhandler@^4.0.0, domhandler@^4.2.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f"
@@ -7819,7 +7826,7 @@ domhandler@^4.0.0, domhandler@^4.2.0:
dependencies:
domelementtype "^2.2.0"
-domutils@^1.7.0:
+domutils@^1.5.1, domutils@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
@@ -8047,6 +8054,11 @@ ensure-array@^1.0.0:
resolved "https://registry.yarnpkg.com/ensure-array/-/ensure-array-1.0.0.tgz#317e9fc632c656bb849eb649133528e205b23abc"
integrity sha512-A+3Ntl5WS+GjDnHtC67dKIjw+IoGoeFdNvjn3ZfKEmZgWUz0nxBPE4W52QMCbGZsat0VwWskD5T6AEpe3T2d1g==
+entities@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+ integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
entities@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
@@ -9898,6 +9910,18 @@ htmlescape@^1.1.0:
resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
integrity sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=
+htmlparser2@^3.9.0:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+ integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+ dependencies:
+ domelementtype "^1.3.1"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^3.1.1"
+
htmlparser2@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
@@ -14870,6 +14894,13 @@ react-final-form@^6.5.3:
dependencies:
"@babel/runtime" "^7.12.1"
+react-html-parser@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/react-html-parser/-/react-html-parser-2.0.2.tgz#6dbe1ddd2cebc1b34ca15215158021db5fc5685e"
+ integrity sha512-XeerLwCVjTs3njZcgCOeDUqLgNIt/t+6Jgi5/qPsO/krUWl76kWKXMeVs2LhY2gwM6X378DkhLjur0zUQdpz0g==
+ dependencies:
+ htmlparser2 "^3.9.0"
+
react-is@^16.13.1, react-is@^16.8.1, react-is@^16.8.6:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"