From cdc2aa36cd83c18886dd17f9f17054b57e4ed6ec Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Wed, 6 Jul 2022 01:51:08 +0200 Subject: [PATCH 1/6] refactor: to category-combo-table-header --- .../category-combo-table-header.js | 130 ++++++++++++++++++ .../category-combo-table.js | 121 +++------------- .../category-combo-table/data-element-cell.js | 32 +++++ .../category-combo-table/padding-cell.js | 9 ++ 4 files changed, 187 insertions(+), 105 deletions(-) create mode 100644 src/data-workspace/category-combo-table/category-combo-table-header.js create mode 100644 src/data-workspace/category-combo-table/data-element-cell.js create mode 100644 src/data-workspace/category-combo-table/padding-cell.js 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..b9e91b4a7 --- /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 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' + +export const CategoryComboTableHeader = ({ + dataElements, + renderRowTotals, + paddingCells, + categoryOptionCombos, + categories, +}) => { + const { data: metadata } = useMetadata() + const { deId: activeDeId, cocId: activeCocId } = useActiveCell() + + // 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 = categoryOptionCombos.length + const rowToColumnsMap = 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 = + categoryOptionCombos.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 + }) + + // 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 = categoryOptionCombos.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} + + ) + })} + {paddingCells.map((_, i) => ( + + ))} + {renderRowTotals && colInfoIndex === 0 && ( + + )} + + ) + }) +} + +CategoryComboTableHeader.propTypes = { + categoryCombo: PropTypes.shape({ + id: PropTypes.string.isRequired, + categoryOptionCombos: PropTypes.array, + }).isRequired, + categories: PropTypes.array, + categoryOptionCombos: PropTypes.array, + dataElements: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + categoryCombo: PropTypes.shape({ + id: PropTypes.string, + }), + displayFormName: PropTypes.string, + valueType: PropTypes.string, + }) + ), + 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..08d31ba4c 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 { 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,8 @@ 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 renderPaddedCells = + const paddingCells = maxColumnsInSection > 0 ? new Array(maxColumnsInSection - sortedCOCs.length).fill(0) : [] @@ -108,69 +69,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 +108,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 From 44b34f5020e894b479afed111984308476cc5730 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Wed, 6 Jul 2022 14:15:13 +0200 Subject: [PATCH 2/6] fix: separate setCurrentItem-context --- .../data-entry-cell/entry-field-input.js | 6 +++--- .../current-item-provider/current-item-context.js | 6 ++++-- .../current-item-provider/current-item-provider.js | 9 +++++++-- src/shared/current-item-provider/index.js | 2 +- .../current-item-provider/use-current-item-context.js | 11 +++++++++-- 5 files changed, 24 insertions(+), 10 deletions(-) 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/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) +} From 711293f31862a171308472465925fe13b528f5b8 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Wed, 6 Jul 2022 14:32:25 +0200 Subject: [PATCH 3/6] refactor: align paddingCells naming --- .../category-combo-table/category-combo-table.js | 2 +- src/data-workspace/category-combo-table/total-cells.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 08d31ba4c..af92b2f3b 100644 --- a/src/data-workspace/category-combo-table/category-combo-table.js +++ b/src/data-workspace/category-combo-table/category-combo-table.js @@ -108,7 +108,7 @@ export const CategoryComboTable = ({ })} {renderColumnTotals && ( { @@ -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, } From 39faadb230638261a69bab597cebc31568be3ca4 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Wed, 6 Jul 2022 15:34:40 +0200 Subject: [PATCH 4/6] refactor: use hook for useCategoryColumns --- .../category-combo-table-header.js | 110 +++++++++--------- .../category-combo-table.js | 8 +- 2 files changed, 62 insertions(+), 56 deletions(-) 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 index b9e91b4a7..38ccd9e54 100644 --- a/src/data-workspace/category-combo-table/category-combo-table-header.js +++ b/src/data-workspace/category-combo-table/category-combo-table-header.js @@ -2,62 +2,66 @@ import i18n from '@dhis2/d2-i18n' import { TableRowHead, TableCellHead } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' -import React from 'react' +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 = ({ - dataElements, renderRowTotals, paddingCells, categoryOptionCombos, categories, + checkTableActive, }) => { - const { data: metadata } = useMetadata() const { deId: activeDeId, cocId: activeCocId } = useActiveCell() - // 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 = categoryOptionCombos.length - const rowToColumnsMap = 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 = - categoryOptionCombos.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 - }) - - // 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) + const rowToColumnsMap = useCategoryColumns( + categories, + categoryOptionCombos.length + ) // Find if this column header includes the active cell's column const isHeaderActive = (headerIdx, headerColSpan) => { @@ -65,7 +69,11 @@ export const CategoryComboTableHeader = ({ (coc) => activeCocId === coc.id ) const idxDiff = activeCellColIdx - headerIdx * headerColSpan - return isThisTableActive && idxDiff < headerColSpan && idxDiff >= 0 + return ( + checkTableActive(activeDeId) && + idxDiff < headerColSpan && + idxDiff >= 0 + ) } return rowToColumnsMap.map((colInfo, colInfoIndex) => { @@ -109,22 +117,14 @@ export const CategoryComboTableHeader = ({ } CategoryComboTableHeader.propTypes = { - categoryCombo: PropTypes.shape({ - id: PropTypes.string.isRequired, - categoryOptionCombos: PropTypes.array, - }).isRequired, categories: PropTypes.array, - categoryOptionCombos: PropTypes.array, - dataElements: PropTypes.arrayOf( + // 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.isRequired, - categoryCombo: PropTypes.shape({ - id: PropTypes.string, - }), - displayFormName: PropTypes.string, - valueType: PropTypes.string, + 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 af92b2f3b..c1b0a819b 100644 --- a/src/data-workspace/category-combo-table/category-combo-table.js +++ b/src/data-workspace/category-combo-table/category-combo-table.js @@ -1,7 +1,7 @@ import i18n from '@dhis2/d2-i18n' 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 } from '../data-entry-cell/index.js' @@ -55,6 +55,11 @@ export const CategoryComboTable = ({ ) } + const checkTableActive = useCallback( + (activeDeId) => dataElements.some(({ id }) => id === activeDeId), + [dataElements] + ) + const paddingCells = maxColumnsInSection > 0 ? new Array(maxColumnsInSection - sortedCOCs.length).fill(0) @@ -77,6 +82,7 @@ export const CategoryComboTable = ({ dataElements={dataElements} renderRowTotals={renderRowTotals} paddingCells={paddingCells} + checkTableActive={checkTableActive} /> {filteredDataElements.map((de, i) => { return ( From a05fc3b852f9869be33adfd4ec40aac0e9f7510d Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Wed, 6 Jul 2022 16:55:36 +0200 Subject: [PATCH 5/6] fix: calculate maxColumnsInSection using actual categoryOptions --- src/data-workspace/section-form/section.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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( From 8c8b6377b4b0b8fc1bb7cfbbe4bb40ec4d91ee43 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Wed, 6 Jul 2022 17:07:43 +0200 Subject: [PATCH 6/6] refactor: some cleanup --- src/data-workspace/category-combo-table/category-combo-table.js | 1 - src/data-workspace/category-combo-table/total-cells.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) 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 c1b0a819b..2c04117f5 100644 --- a/src/data-workspace/category-combo-table/category-combo-table.js +++ b/src/data-workspace/category-combo-table/category-combo-table.js @@ -79,7 +79,6 @@ export const CategoryComboTable = ({ ( - + {i18n.t('Totals')} )