diff --git a/src/__demo__/PivotTable.aggregate.stories.js b/src/__demo__/PivotTable.aggregate.stories.js index d3b558eaf..3602c89d5 100644 --- a/src/__demo__/PivotTable.aggregate.stories.js +++ b/src/__demo__/PivotTable.aggregate.stories.js @@ -470,6 +470,54 @@ DeepFilter.story = { name: 'deep - filter', } +/* This layout has a filter the lib can read, so it derives "Eastern Area". + * A caller that would rather format the line itself passes `filterText`. + * Whether there is a row at all stays the layout's call either way, which + * the third mode shows. */ +const filterTextModes = [ + { label: 'Derived by the lib' }, + { label: 'Supplied by the caller', filterText: 'Age: 5 - 10, Female' }, + { + label: 'Supplied, but the layout has no filters', + filterText: 'Age: 5 - 10, Female', + filters: [], + }, +] + +export const DeepSuppliedFilterText = (_, { pivotTableOptions }) => { + const [modeIndex, setModeIndex] = useState(0) + const mode = filterTextModes[modeIndex] + const visualization = { + ...deepWithFiltersVisualization, + ...visualizationReset, + ...pivotTableOptions, + showDimensionLabels: false, + filters: mode.filters ?? deepWithFiltersVisualization.filters, + } + return ( +
+ {filterTextModes.map((each, index) => ( + + ))} +
+ +
+
+ ) +} + +DeepSuppliedFilterText.storyName = 'deep - filter text supplied by the caller' + export const DeepTitleSubtitleFilter = (_, { pivotTableOptions }) => { const visualization = { ...deepVisualization, diff --git a/src/components/PivotTable/PivotTable.js b/src/components/PivotTable/PivotTable.js index ab5c74ca0..b480bc784 100644 --- a/src/components/PivotTable/PivotTable.js +++ b/src/components/PivotTable/PivotTable.js @@ -13,6 +13,7 @@ const PivotTable = ({ visualization, data, legendSets, + filterText, renderCounter, onToggleContextualMenu, }) => { @@ -20,8 +21,14 @@ const PivotTable = ({ const { width, height } = useParentSize(containerRef, renderCounter) const engine = useMemo( - () => new PivotTableEngine(visualization, data, legendSets), - [visualization, data, legendSets] + () => + new PivotTableEngine({ + visualization, + data, + legendSets, + filterText, + }), + [visualization, data, legendSets, filterText] ) const { sortBy, onSortByColumn } = useSortableColumns(engine) @@ -58,6 +65,7 @@ const PivotTable = ({ PivotTable.propTypes = { data: PropTypes.object.isRequired, visualization: PropTypes.object.isRequired, + filterText: PropTypes.string, legendSets: PropTypes.arrayOf(PropTypes.object), renderCounter: PropTypes.number, onToggleContextualMenu: PropTypes.func, diff --git a/src/components/PivotTable/PivotTableTitleRows.js b/src/components/PivotTable/PivotTableTitleRows.js index b29439261..6376a7e95 100644 --- a/src/components/PivotTable/PivotTableTitleRows.js +++ b/src/components/PivotTable/PivotTableTitleRows.js @@ -25,10 +25,13 @@ export const PivotTableTitleRows = ({ clippingResult, width }) => { ) : null} {engine.visualization.filters?.length ? ( diff --git a/src/components/PivotTable/__tests__/PivotTableTitleRows.spec.js b/src/components/PivotTable/__tests__/PivotTableTitleRows.spec.js new file mode 100644 index 000000000..b045d9f0f --- /dev/null +++ b/src/components/PivotTable/__tests__/PivotTableTitleRows.spec.js @@ -0,0 +1,101 @@ +import { render, screen } from '@testing-library/react' +import React from 'react' +import deepWithFiltersData from '../../../__demo__/data/aggregate/deepWithFilters.data.json' +import deepWithFiltersVisualization from '../../../__demo__/data/aggregate/deepWithFilters.visualization.json' +import PivotTable from '../PivotTable.js' + +/* The engine measures every cell against a canvas 2d context, which jsdom + * does not implement. */ +jest.mock('../../../modules/pivotTable/measureText.js', () => ({ + measureTextWithWrapping: () => ({ width: 100, height: 20 }), +})) + +/* The container renders nothing at all until it has a size, and jsdom reports + * every element as 0 x 0. */ +jest.mock('../../../modules/pivotTable/useParentSize.js', () => ({ + useParentSize: () => ({ width: 800, height: 600 }), +})) + +/* `deepWithFilters` filters on an org unit group set, which `getFilterText` + * resolves through `metaData.dimensions` to this name. */ +const DERIVED_FILTER_TEXT = 'Eastern Area' + +const visualization = { + ...deepWithFiltersVisualization, + title: 'Hello', + subtitle: 'Goodbye', +} + +const renderTable = (props) => + render( + + ) + +const titleRowTexts = () => + screen + .queryAllByTestId('visualization-title') + .map((element) => element.textContent) + +describe('PivotTable title rows', () => { + it('derives the filter row from the layout when no text is supplied', () => { + renderTable() + + expect(titleRowTexts()).toEqual([ + 'Hello', + 'Goodbye', + DERIVED_FILTER_TEXT, + ]) + }) + + it('renders no filter row when the layout has no filters', () => { + renderTable({ visualization: { ...visualization, filters: [] } }) + + expect(titleRowTexts()).toEqual(['Hello', 'Goodbye']) + }) + + it('renders the supplied text in place of the derived one', () => { + renderTable({ filterText: 'Age 5 - 10, Female' }) + + expect(titleRowTexts()).toEqual([ + 'Hello', + 'Goodbye', + 'Age 5 - 10, Female', + ]) + }) + + /* Whether there is a row remains the layout's call - supplying the text + * says what goes in the row, not whether there is one. */ + it('does not render a supplied text the layout has no row for', () => { + renderTable({ + visualization: { ...visualization, filters: [] }, + filterText: 'Age 5 - 10, Female', + }) + + expect(titleRowTexts()).toEqual(['Hello', 'Goodbye']) + }) + + /* An empty string is a caller with nothing to say, not a caller asking + * for an empty row. */ + it('derives the filter row for an empty supplied text', () => { + renderTable({ filterText: '' }) + + expect(titleRowTexts()).toEqual([ + 'Hello', + 'Goodbye', + DERIVED_FILTER_TEXT, + ]) + }) + + it('leaves the title and subtitle to the visualization', () => { + renderTable({ + visualization: { ...visualization, hideSubtitle: true }, + filterText: 'Age 5 - 10', + }) + + expect(titleRowTexts()).toEqual(['Hello', 'Age 5 - 10']) + }) +}) diff --git a/src/modules/pivotTable/PivotTableEngine.js b/src/modules/pivotTable/PivotTableEngine.js index cdcc8257c..d2125c7e3 100644 --- a/src/modules/pivotTable/PivotTableEngine.js +++ b/src/modules/pivotTable/PivotTableEngine.js @@ -298,7 +298,7 @@ export class PivotTableEngine { columnMap = [] accumulators = { rows: {} } - constructor(visualization, data, legendSets) { + constructor({ visualization, data, legendSets, filterText }) { this.visualization = Object.assign( {}, defaultVisualizationProps, @@ -336,6 +336,9 @@ export class PivotTableEngine { ? visualization.fixRowHeaders : false, cumulativeValues: visualization.cumulativeValues, + /* If visualization.filters is populated and this option is present, + * this text is used instead of the text derived from `visualization.filters` */ + filterText, } this.adaptiveClippingController = new AdaptiveClippingController(this) diff --git a/src/modules/pivotTable/__tests__/pivotTableEngineHierarchy.spec.js b/src/modules/pivotTable/__tests__/pivotTableEngineHierarchy.spec.js index 56b5e8b3b..e854d2093 100644 --- a/src/modules/pivotTable/__tests__/pivotTableEngineHierarchy.spec.js +++ b/src/modules/pivotTable/__tests__/pivotTableEngineHierarchy.spec.js @@ -57,10 +57,10 @@ const cellValues = (engine) => describe('PivotTableEngine org unit hierarchy', () => { it('applies the hierarchy to a bare `ou` dimension', () => { - const engine = new PivotTableEngine( - buildVisualization('ou'), - buildData('ou') - ) + const engine = new PivotTableEngine({ + visualization: buildVisualization('ou'), + data: buildData('ou'), + }) expect(rowHierarchies(engine)).toEqual([ ['Sierra Leone', 'Northern Province', 'Bombali'], @@ -70,10 +70,10 @@ describe('PivotTableEngine org unit hierarchy', () => { it('applies the hierarchy to a stage-qualified `ou` dimension', () => { const dimension = `${STAGE}.ou` - const engine = new PivotTableEngine( - buildVisualization(dimension), - buildData(dimension) - ) + const engine = new PivotTableEngine({ + visualization: buildVisualization(dimension), + data: buildData(dimension), + }) expect(rowHierarchies(engine)).toEqual([ ['Sierra Leone', 'Northern Province', 'Bombali'], @@ -82,10 +82,10 @@ describe('PivotTableEngine org unit hierarchy', () => { }) it('applies the hierarchy to an `enrollmentou` dimension', () => { - const engine = new PivotTableEngine( - buildVisualization('enrollmentou'), - buildData('enrollmentou') - ) + const engine = new PivotTableEngine({ + visualization: buildVisualization('enrollmentou'), + data: buildData('enrollmentou'), + }) expect(rowHierarchies(engine)).toEqual([ ['Sierra Leone', 'Northern Province', 'Bombali'], @@ -94,10 +94,13 @@ describe('PivotTableEngine org unit hierarchy', () => { }) it('leaves items untouched when showHierarchy is off', () => { - const engine = new PivotTableEngine( - { ...buildVisualization(`${STAGE}.ou`), showHierarchy: false }, - buildData(`${STAGE}.ou`) - ) + const engine = new PivotTableEngine({ + visualization: { + ...buildVisualization(`${STAGE}.ou`), + showHierarchy: false, + }, + data: buildData(`${STAGE}.ou`), + }) expect(rowHierarchies(engine)).toEqual([undefined, undefined]) expect(engine.getRowHeader(0)[0].uid).toBe(OU_A) @@ -106,10 +109,10 @@ describe('PivotTableEngine org unit hierarchy', () => { /* Sorting rewrites itemIds, which the row lookup resolves data rows * through. If the two fall out of step every cell renders empty. */ it('keeps values aligned with the re-sorted rows', () => { - const engine = new PivotTableEngine( - buildVisualization(`${STAGE}.ou`), - buildData(`${STAGE}.ou`) - ) + const engine = new PivotTableEngine({ + visualization: buildVisualization(`${STAGE}.ou`), + data: buildData(`${STAGE}.ou`), + }) expect(rowNames(engine)).toEqual(['Bombali', 'Bo']) expect(cellValues(engine)).toEqual(['2', '1']) @@ -122,7 +125,10 @@ describe('PivotTableEngine org unit hierarchy', () => { const data = buildData(dimension) data.metaData.ouNameHierarchy = {} - const engine = new PivotTableEngine(buildVisualization(dimension), data) + const engine = new PivotTableEngine({ + visualization: buildVisualization(dimension), + data, + }) expect(rowNames(engine)).toEqual(['Bo', 'Bombali']) expect(rowHierarchies(engine)).toEqual([undefined, undefined]) @@ -131,10 +137,10 @@ describe('PivotTableEngine org unit hierarchy', () => { it('leaves non-org-unit dimensions untouched', () => { const dimension = `${STAGE}.de1` - const engine = new PivotTableEngine( - buildVisualization(dimension), - buildData(dimension) - ) + const engine = new PivotTableEngine({ + visualization: buildVisualization(dimension), + data: buildData(dimension), + }) expect(rowHierarchies(engine)).toEqual([undefined, undefined]) })