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) +}