diff --git a/src/data-workspace/category-combo-table/category-combo-table-header.js b/src/data-workspace/category-combo-table/category-combo-table-header.js
new file mode 100644
index 000000000..38ccd9e54
--- /dev/null
+++ b/src/data-workspace/category-combo-table/category-combo-table-header.js
@@ -0,0 +1,130 @@
+import i18n from '@dhis2/d2-i18n'
+import { TableRowHead, TableCellHead } from '@dhis2/ui'
+import cx from 'classnames'
+import PropTypes from 'prop-types'
+import React, { useMemo } from 'react'
+import { useMetadata, selectors } from '../../metadata/index.js'
+import { useActiveCell } from '../data-entry-cell/index.js'
+import styles from './category-combo-table.module.css'
+import { PaddingCell } from './padding-cell.js'
+import { TotalHeader } from './total-cells.js'
+
+// Computes the span and columns to render in each category-row
+// columns are category-options, and needs to be repeated in case of multiple categories
+const useCategoryColumns = (categories, numberOfCoCs) => {
+ const { data: metadata } = useMetadata()
+ return useMemo(() => {
+ let catColSpan = numberOfCoCs
+ return categories.map((c) => {
+ const categoryOptions = selectors.getCategoryOptionsByCategoryId(
+ metadata,
+ c.id
+ )
+ const nrOfOptions = c.categoryOptions.length
+ // catColSpan should always be equal to nrOfOptions in last iteration
+ // unless anomaly with categoryOptionCombo-generation server-side
+ if (nrOfOptions > 0 && catColSpan >= nrOfOptions) {
+ // calculate colSpan for current options
+ // this is the span for each option, not the "total" span of the row
+ catColSpan = catColSpan / nrOfOptions
+ // when table have multiple categories, options need to be repeated for each disaggregation "above" current-category
+ const repeat = numberOfCoCs / (catColSpan * nrOfOptions)
+
+ const columnsToRender = new Array(repeat)
+ .fill(0)
+ .flatMap(() => categoryOptions)
+
+ return {
+ span: catColSpan,
+ columns: columnsToRender,
+ category: c,
+ }
+ } else {
+ console.warn(
+ `Category ${c.displayFormName} malformed. Number of options: ${nrOfOptions}, span: ${catColSpan}`
+ )
+ }
+ return c
+ })
+ }, [metadata, categories, numberOfCoCs])
+}
+
+export const CategoryComboTableHeader = ({
+ renderRowTotals,
+ paddingCells,
+ categoryOptionCombos,
+ categories,
+ checkTableActive,
+}) => {
+ const { deId: activeDeId, cocId: activeCocId } = useActiveCell()
+
+ const rowToColumnsMap = useCategoryColumns(
+ categories,
+ categoryOptionCombos.length
+ )
+
+ // Find if this column header includes the active cell's column
+ const isHeaderActive = (headerIdx, headerColSpan) => {
+ const activeCellColIdx = categoryOptionCombos.findIndex(
+ (coc) => activeCocId === coc.id
+ )
+ const idxDiff = activeCellColIdx - headerIdx * headerColSpan
+ return (
+ checkTableActive(activeDeId) &&
+ idxDiff < headerColSpan &&
+ idxDiff >= 0
+ )
+ }
+
+ return rowToColumnsMap.map((colInfo, colInfoIndex) => {
+ const { span, columns, category } = colInfo
+ return (
+
+
+ {category.displayFormName !== 'default' &&
+ category.displayFormName}
+
+ {columns.map((co, columnIndex) => {
+ return (
+
+ {co.isDefault
+ ? i18n.t('Value')
+ : co.displayFormName}
+
+ )
+ })}
+ {paddingCells.map((_, i) => (
+
+ ))}
+ {renderRowTotals && colInfoIndex === 0 && (
+
+ )}
+
+ )
+ })
+}
+
+CategoryComboTableHeader.propTypes = {
+ categories: PropTypes.array,
+ // Note that this must be the sorted categoryoOptionCombos, eg. in the same order as they are rendered
+ categoryOptionCombos: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.string,
+ })
+ ),
+ checkTableActive: PropTypes.func,
+ paddingCells: PropTypes.array,
+ renderRowTotals: PropTypes.bool,
+}
diff --git a/src/data-workspace/category-combo-table/category-combo-table.js b/src/data-workspace/category-combo-table/category-combo-table.js
index 8e865f9da..2c04117f5 100644
--- a/src/data-workspace/category-combo-table/category-combo-table.js
+++ b/src/data-workspace/category-combo-table/category-combo-table.js
@@ -1,24 +1,15 @@
import i18n from '@dhis2/d2-i18n'
-import {
- TableRowHead,
- TableCellHead,
- TableBody,
- TableRow,
- TableCell,
-} from '@dhis2/ui'
-import cx from 'classnames'
+import { TableBody, TableRow, TableCell } from '@dhis2/ui'
import PropTypes from 'prop-types'
-import React, { useMemo } from 'react'
+import React, { useMemo, useCallback } from 'react'
import { useMetadata, selectors } from '../../metadata/index.js'
import { cartesian } from '../../shared/utils.js'
-import {
- DataEntryCell,
- DataEntryField,
- useActiveCell,
-} from '../data-entry-cell/index.js'
+import { DataEntryCell, DataEntryField } from '../data-entry-cell/index.js'
import { getFieldId } from '../get-field-id.js'
+import { CategoryComboTableHeader } from './category-combo-table-header.js'
import styles from './category-combo-table.module.css'
-import { TotalHeader, ColumnTotals, RowTotal } from './total-cells.js'
+import { DataElementCell } from './data-element-cell.js'
+import { ColumnTotals, RowTotal } from './total-cells.js'
export const CategoryComboTable = ({
categoryCombo,
@@ -31,7 +22,6 @@ export const CategoryComboTable = ({
renderColumnTotals,
}) => {
const { data: metadata } = useMetadata()
- const { deId: activeDeId, cocId: activeCocId } = useActiveCell()
const categories = selectors.getCategoriesByCategoryComboId(
metadata,
@@ -64,37 +54,13 @@ export const CategoryComboTable = ({
Server: ${categoryCombo.categoryOptionCombos.length})`
)
}
- // Computes the span and repeats for each columns in a category-row.
- // Repeats are the number of times the options in a category needs to be rendered per category-row
- let catColSpan = sortedCOCs.length
- const rowToColumnsMap = categories.map((c) => {
- const categoryOptions = selectors.getCategoryOptionsByCategoryId(
- metadata,
- c.id
- )
- const nrOfOptions = c.categoryOptions.length
- if (nrOfOptions > 0 && catColSpan >= nrOfOptions) {
- catColSpan = catColSpan / nrOfOptions
- const repeat = sortedCOCs.length / (catColSpan * nrOfOptions)
-
- const columnsToRender = new Array(repeat)
- .fill(0)
- .flatMap(() => categoryOptions)
- return {
- span: catColSpan,
- columns: columnsToRender,
- category: c,
- }
- } else {
- console.warn(
- `Category ${c.displayFormName} malformed. Number of options: ${nrOfOptions}, span: ${catColSpan}`
- )
- }
- return c
- })
+ const checkTableActive = useCallback(
+ (activeDeId) => dataElements.some(({ id }) => id === activeDeId),
+ [dataElements]
+ )
- const renderPaddedCells =
+ const paddingCells =
maxColumnsInSection > 0
? new Array(maxColumnsInSection - sortedCOCs.length).fill(0)
: []
@@ -108,69 +74,19 @@ export const CategoryComboTable = ({
})
const itemsHiddenCnt = dataElements.length - filteredDataElements.length
- // Is the active cell in this cat-combo table? Check to see if active
- // data element is in this CCT
- const isThisTableActive = dataElements.some(({ id }) => id === activeDeId)
-
- // Find if this column header includes the active cell's column
- const isHeaderActive = (headerIdx, headerColSpan) => {
- const activeCellColIdx = sortedCOCs.findIndex(
- (coc) => activeCocId === coc.id
- )
- const idxDiff = activeCellColIdx - headerIdx * headerColSpan
- return isThisTableActive && idxDiff < headerColSpan && idxDiff >= 0
- }
-
return (
- {rowToColumnsMap.map((colInfo, colInfoIndex) => {
- const { span, columns, category } = colInfo
- return (
-
-
- {category.displayFormName !== 'default' &&
- category.displayFormName}
-
- {columns.map((co, columnIndex) => {
- return (
-
- {co.isDefault
- ? i18n.t('Value')
- : co.displayFormName}
-
- )
- })}
- {renderPaddedCells.map((_, i) => (
-
- ))}
- {renderRowTotals && colInfoIndex === 0 && (
-
- )}
-
- )
- })}
+
{filteredDataElements.map((de, i) => {
return (
-
- {de.displayFormName}
-
+
{sortedCOCs.map((coc) => (
))}
- {renderPaddedCells.map((_, i) => (
+ {paddingCells.map((_, i) => (
))}
{renderRowTotals && (
@@ -197,7 +113,7 @@ export const CategoryComboTable = ({
})}
{renderColumnTotals && (
{
+ const { deId: activeDeId } = useActiveCell()
+ return (
+
+ {dataElement.displayFormName}
+
+ )
+}
+
+DataElementCell.propTypes = {
+ dataElement: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ categoryCombo: PropTypes.shape({
+ id: PropTypes.string,
+ }),
+ displayFormName: PropTypes.string,
+ valueType: PropTypes.string,
+ }),
+}
+
+export default DataElementCell
diff --git a/src/data-workspace/category-combo-table/padding-cell.js b/src/data-workspace/category-combo-table/padding-cell.js
new file mode 100644
index 000000000..48c7c7779
--- /dev/null
+++ b/src/data-workspace/category-combo-table/padding-cell.js
@@ -0,0 +1,9 @@
+import { TableCell } from '@dhis2/ui'
+import React from 'react'
+import styles from './category-combo-table.module.css'
+
+export const PaddingCell = () => (
+
+)
+
+export default PaddingCell
diff --git a/src/data-workspace/category-combo-table/total-cells.js b/src/data-workspace/category-combo-table/total-cells.js
index cfe397387..52c395c4b 100644
--- a/src/data-workspace/category-combo-table/total-cells.js
+++ b/src/data-workspace/category-combo-table/total-cells.js
@@ -15,7 +15,7 @@ TotalCell.propTypes = {
}
export const TotalHeader = ({ rowSpan }) => (
-
+
{i18n.t('Totals')}
)
@@ -42,7 +42,7 @@ RowTotal.propTypes = {
export const ColumnTotals = ({
renderTotalSum,
- paddedCells,
+ paddingCells,
dataElements,
categoryOptionCombos,
}) => {
@@ -57,7 +57,7 @@ export const ColumnTotals = ({
{columnTotals.map((v, i) => (
{v}
))}
- {paddedCells.map((_, i) => (
+ {paddingCells.map((_, i) => (
))}
{renderTotalSum && (
@@ -72,6 +72,6 @@ export const ColumnTotals = ({
ColumnTotals.propTypes = {
categoryOptionCombos: propTypes.array,
dataElements: propTypes.array,
- paddedCells: propTypes.array,
+ paddingCells: propTypes.array,
renderTotalSum: propTypes.bool,
}
diff --git a/src/data-workspace/data-entry-cell/entry-field-input.js b/src/data-workspace/data-entry-cell/entry-field-input.js
index 0e3d9c04b..129270bd7 100644
--- a/src/data-workspace/data-entry-cell/entry-field-input.js
+++ b/src/data-workspace/data-entry-cell/entry-field-input.js
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types'
import React from 'react'
import { useRightHandPanelContext } from '../../right-hand-panel/index.js'
-import { useCurrentItemContext } from '../../shared/index.js'
+import { useSetCurrentItemContext } from '../../shared/index.js'
import { focusNext, focusPrev } from '../focus-utils/index.js'
import {
GenericInput,
@@ -85,7 +85,7 @@ export function EntryFieldInput({
setSyncStatus,
disabled,
}) {
- const currentItemContext = useCurrentItemContext()
+ const setCurrentItem = useSetCurrentItemContext()
const rightHandPanel = useRightHandPanelContext()
const { id: deId } = de
const { id: cocId } = coc
@@ -113,7 +113,7 @@ export function EntryFieldInput({
}
const onFocus = () => {
- currentItemContext.setItem(currentItem)
+ setCurrentItem(currentItem)
rightHandPanel.hide()
}
diff --git a/src/data-workspace/section-form/section.js b/src/data-workspace/section-form/section.js
index 6f71ad364..4b59686f9 100644
--- a/src/data-workspace/section-form/section.js
+++ b/src/data-workspace/section-form/section.js
@@ -37,12 +37,17 @@ export const SectionFormSection = ({
? selectors.getGroupedDataElementsByCatComboInOrder(data, dataElements)
: selectors.getGroupedDataElementsByCatCombo(data, dataElements)
- const maxColumnsInSection = Math.max(
- ...groupedDataElements.map(
- (grp) => grp.categoryCombo.categoryOptionCombos?.length || 1
- )
+ // calculate how many columns in each group
+ const groupedTotalColumns = groupedDataElements.map((grp) =>
+ (
+ selectors
+ .getCategoriesByCategoryComboId(data, grp.categoryCombo.id)
+ ?.map((cat) => cat.categoryOptions.length) || [1]
+ ).reduce((total, curr) => total * curr)
)
+ const maxColumnsInSection = Math.max(...groupedTotalColumns)
+
const greyedFields = new Set(
section.greyedFields.map((greyedField) =>
getFieldId(
diff --git a/src/shared/current-item-provider/current-item-context.js b/src/shared/current-item-provider/current-item-context.js
index 801a11566..612fafd4c 100644
--- a/src/shared/current-item-provider/current-item-context.js
+++ b/src/shared/current-item-provider/current-item-context.js
@@ -1,10 +1,12 @@
import { createContext } from 'react'
-const CurrentItemContext = createContext({
+export const CurrentItemContext = createContext({
item: null,
setItem: () => {
throw new Error('Current item context has not been initialized yet')
},
})
-export default CurrentItemContext
+export const SetCurrentItemContext = createContext(() => {
+ throw new Error('Current item context has not been initialized yet')
+})
diff --git a/src/shared/current-item-provider/current-item-provider.js b/src/shared/current-item-provider/current-item-provider.js
index 92c98d1f9..9cd2f33d6 100644
--- a/src/shared/current-item-provider/current-item-provider.js
+++ b/src/shared/current-item-provider/current-item-provider.js
@@ -1,6 +1,9 @@
import PropTypes from 'prop-types'
import React, { useState } from 'react'
-import CurrentItemContext from './current-item-context.js'
+import {
+ CurrentItemContext,
+ SetCurrentItemContext,
+} from './current-item-context.js'
export default function CurrentItemProvider({ children }) {
const [item, setItem] = useState(null)
@@ -8,7 +11,9 @@ export default function CurrentItemProvider({ children }) {
return (
- {children}
+
+ {children}
+
)
}
diff --git a/src/shared/current-item-provider/index.js b/src/shared/current-item-provider/index.js
index f556dafec..d4a0e9930 100644
--- a/src/shared/current-item-provider/index.js
+++ b/src/shared/current-item-provider/index.js
@@ -1,2 +1,2 @@
-export { default as useCurrentItemContext } from './use-current-item-context.js'
+export * from './use-current-item-context.js'
export { default as CurrentItemProvider } from './current-item-provider.js'
diff --git a/src/shared/current-item-provider/use-current-item-context.js b/src/shared/current-item-provider/use-current-item-context.js
index e4c4f32f7..8b8efd56e 100644
--- a/src/shared/current-item-provider/use-current-item-context.js
+++ b/src/shared/current-item-provider/use-current-item-context.js
@@ -1,6 +1,13 @@
import { useContext } from 'react'
-import CurrentItemContext from './current-item-context.js'
+import {
+ CurrentItemContext,
+ SetCurrentItemContext,
+} from './current-item-context.js'
-export default function useCurrentItemContext() {
+export function useCurrentItemContext() {
return useContext(CurrentItemContext)
}
+
+export function useSetCurrentItemContext() {
+ return useContext(SetCurrentItemContext)
+}