diff --git a/README.md b/README.md index 33021bf40f..f5d0ed3437 100644 --- a/README.md +++ b/README.md @@ -165,4 +165,4 @@ In order to automatically format code in VS Code according to our style guide: 1. Install [Prettier for VS Code](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode). 2. Enable the **Editor: Format on Save** setting: `"editor.formatOnSave": true`. -Your files will now be formatted automatically when you save them. +Your files will now be formatted automatically when you save them diff --git a/packages/admin-panel/src/VizBuilderApp/constants.js b/packages/admin-panel/src/VizBuilderApp/constants.js index 8b28819305..3f85d83e15 100644 --- a/packages/admin-panel/src/VizBuilderApp/constants.js +++ b/packages/admin-panel/src/VizBuilderApp/constants.js @@ -171,7 +171,7 @@ export const DASHBOARD_ITEM_VIZ_TYPES = { // Matrix MATRIX: { name: 'Matrix', - schema: MatrixVizBuilderConfigSchema, + // schema: MatrixVizBuilderConfigSchema, vizMatchesType: viz => viz.type === 'matrix', initialConfig: { type: 'matrix', diff --git a/packages/report-server/src/__tests__/reportBuilder/output/matrix.test.ts b/packages/report-server/src/__tests__/reportBuilder/output/matrix.test.ts index e6bad3a0e0..82817a39e6 100644 --- a/packages/report-server/src/__tests__/reportBuilder/output/matrix.test.ts +++ b/packages/report-server/src/__tests__/reportBuilder/output/matrix.test.ts @@ -246,6 +246,77 @@ describe('matrix', () => { ); expect(result).toEqual(expectedData); }); + + it('can include entity columns', async () => { + const expectedData = { + columns: [ + { + entityCode: 'TO', + key: 'Tonga', + title: 'Tonga', + }, + { + key: 'InfrastructureType', + title: 'InfrastructureType', + }, + { + key: 'Laos', + title: 'Laos', + }, + ], + rows: [ + { + dataElement: 'hospital', + InfrastructureType: 'medical center', + Tonga: 0, + Laos: 3, + }, + { + InfrastructureType: 'medical center', + dataElement: 'clinic', + Tonga: 9, + Laos: 4, + }, + { + InfrastructureType: 'others', + Tonga: 0, + dataElement: 'park', + }, + { + InfrastructureType: 'others', + Tonga: 5, + dataElement: 'library', + }, + ], + }; + // Test setting fixed columns + const output1 = buildOutput( + { + type: 'matrix', + rowField: 'FacilityType', + columns: [{ entityLabel: 'Tonga', entityCode: 'TO' }, 'InfrastructureType', 'Laos'], + }, + reportServerAggregator, + ); + const result1 = await output1( + TransformTable.fromRows(MULTIPLE_TRANSFORMED_DATA_FOR_SPECIFIED_COLUMNS), + ); + expect(result1).toEqual(expectedData); + + // Test setting fixed & dynamic columns + const output2 = buildOutput( + { + type: 'matrix', + rowField: 'FacilityType', + columns: [{ entityLabel: 'Tonga', entityCode: 'TO' }, '*'], + }, + reportServerAggregator, + ); + const result2 = await output2( + TransformTable.fromRows(MULTIPLE_TRANSFORMED_DATA_FOR_SPECIFIED_COLUMNS), + ); + expect(result2).toEqual(expectedData); + }); }); describe('categoryField', () => { diff --git a/packages/report-server/src/reportBuilder/output/functions/matrix/matrix.ts b/packages/report-server/src/reportBuilder/output/functions/matrix/matrix.ts index 0a52f7816d..208a5ee786 100644 --- a/packages/report-server/src/reportBuilder/output/functions/matrix/matrix.ts +++ b/packages/report-server/src/reportBuilder/output/functions/matrix/matrix.ts @@ -13,7 +13,7 @@ const paramsValidator = yup.object().shape( { columns: yup.lazy((value: unknown) => Array.isArray(value) - ? yup.array().of(yup.string().required()) + ? yup.array() : yup.mixed<'*'>().oneOf(['*'], "columns must be either '*' or an array"), ), rowField: yup diff --git a/packages/report-server/src/reportBuilder/output/functions/matrix/matrixBuilder.ts b/packages/report-server/src/reportBuilder/output/functions/matrix/matrixBuilder.ts index abed4fff85..8d08c31385 100644 --- a/packages/report-server/src/reportBuilder/output/functions/matrix/matrixBuilder.ts +++ b/packages/report-server/src/reportBuilder/output/functions/matrix/matrixBuilder.ts @@ -1,9 +1,13 @@ import pick from 'lodash.pick'; +import { MatrixEntityCell } from '@tupaia/types'; import { TransformTable } from '../../../transform'; - import { Row } from '../../../types'; import { MatrixParams, Matrix } from './types'; +function isMatrixEntityCell(cell: unknown): cell is MatrixEntityCell { + return typeof cell === 'object' && cell !== null && 'entityLabel' in cell && 'entityCode' in cell; +} + /** TODO: currently we are using 'dataElement' as a key in rows to specify row field (name), * eventually we want to change Tupaia front end logic to use 'rowField' instead of 'dataElement' */ @@ -36,19 +40,46 @@ export class MatrixBuilder { * eventually we want to refactor Tupaia frontend logic to render columns with an array formatted as * '[ ${columnName} ]' */ - const assignColumnSetToMatrixData = (columns: string[]) => { - this.matrixData.columns = columns.map(c => ({ key: c, title: c })); + const assignColumnSetToMatrixData = (columns: (string | MatrixEntityCell)[]) => { + this.matrixData.columns = columns.map(c => { + if (isMatrixEntityCell(c)) { + return { + key: c.entityLabel, + title: c.entityLabel, + entityCode: c.entityCode, + }; + } + return { key: c as string, title: c as string }; + }); }; - const getRemainingFieldsFromRows = (includeFields: string[], excludeFields: string[]) => { - return this.table - .getColumns() - .filter( - columnName => !excludeFields.includes(columnName) && !includeFields.includes(columnName), + const getRemainingFieldsFromRows = ( + includeFields: (string | MatrixEntityCell)[], + excludeFields: string[], + ) => { + const entityFields = includeFields.filter(field => + isMatrixEntityCell(field), + ) as MatrixEntityCell[]; + const entityFieldNames = entityFields.map(field => field.entityLabel); + + return this.table.getColumns().filter((columnName: string) => { + return ( + !entityFieldNames.includes(columnName) && + !excludeFields.includes(columnName) && + !includeFields.includes(columnName) ); + }); }; - const { includeFields, excludeFields } = this.params.columns; + const { includeFields: originalIncludeFields, excludeFields } = this.params.columns; + + // If the include fields are a matrix entity cell not in the table columns, don't include them + const includeFields = originalIncludeFields.filter(field => { + if (isMatrixEntityCell(field)) { + return this.table.getColumns().includes(field.entityLabel); + } + return true; + }); const remainingFields = includeFields.includes('*') ? getRemainingFieldsFromRows(includeFields, excludeFields) @@ -74,12 +105,16 @@ export class MatrixBuilder { } rows.push(newRows); }); - this.matrixData.rows = rows; } private adjustRowsToUseIncludedColumns() { - const { includeFields } = this.params.columns; + const includeFields = this.params.columns.includeFields.map(field => { + if (isMatrixEntityCell(field)) { + return field.entityLabel; + } + return field; + }); const newRows: Row[] = []; if (includeFields.includes('*')) { diff --git a/packages/report-server/src/reportBuilder/output/functions/matrix/types.ts b/packages/report-server/src/reportBuilder/output/functions/matrix/types.ts index 9ea969352c..e5a09f4b9e 100644 --- a/packages/report-server/src/reportBuilder/output/functions/matrix/types.ts +++ b/packages/report-server/src/reportBuilder/output/functions/matrix/types.ts @@ -1,7 +1,8 @@ +import { MatrixEntityCell } from '@tupaia/types'; import { Row } from '../../../types'; export type MatrixParams = { - columns: { includeFields: string[]; excludeFields: string[] }; + columns: { includeFields: (string | MatrixEntityCell)[]; excludeFields: string[] }; rows: { rowField: string; categoryField: string | undefined }; }; @@ -9,6 +10,7 @@ export type Matrix = { columns: { key: string; title: string; + entityCode?: string; }[]; rows: Row[]; }; diff --git a/packages/report-server/src/reportBuilder/reportBuilder.ts b/packages/report-server/src/reportBuilder/reportBuilder.ts index 9f15ff37ee..b09571abb5 100644 --- a/packages/report-server/src/reportBuilder/reportBuilder.ts +++ b/packages/report-server/src/reportBuilder/reportBuilder.ts @@ -52,11 +52,9 @@ export class ReportBuilder { } const data = this.testData || []; - const context = await buildContext(this.config.transform, this.reqContext); const transform = buildTransform(this.config.transform, context); const transformedData = await transform(TransformTable.fromRows(data)); - const output = buildOutput(this.config.output, this.reqContext.aggregator); const outputData = await output(transformedData); diff --git a/packages/report-server/src/routes/FetchReportRoute.ts b/packages/report-server/src/routes/FetchReportRoute.ts index 5fe67f3053..4d8175bcd6 100644 --- a/packages/report-server/src/routes/FetchReportRoute.ts +++ b/packages/report-server/src/routes/FetchReportRoute.ts @@ -82,7 +82,6 @@ export class FetchReportRoute extends Route { }; const reportBuilder = new ReportBuilder(reqContext).setConfig(report.config); - const reportResponse = await reportBuilder.build(); const { results } = reportResponse; diff --git a/packages/tupaia-web/src/features/EntitySearch/EntityMenu.tsx b/packages/tupaia-web/src/features/EntitySearch/EntityMenu.tsx index 13203fd498..8dbf47faff 100644 --- a/packages/tupaia-web/src/features/EntitySearch/EntityMenu.tsx +++ b/packages/tupaia-web/src/features/EntitySearch/EntityMenu.tsx @@ -11,38 +11,38 @@ import { Button, IconButton, List as MuiList, ListItemProps } from '@material-ui import { useEntities } from '../../api/queries'; const FlexRow = styled.div` - display: flex; - align-items: center; - justify-content: space-between; + display: flex; + align-items: center; + justify-content: space-between; `; const List = styled(MuiList)` - margin-left: 1rem; + margin-left: 1rem; - .MuiIconButton-root, - .MuiSvgIcon-root { - font-size: 1.5rem; - } + .MuiIconButton-root, + .MuiSvgIcon-root { + font-size: 1.5rem; + } - // Hide expand icon when there are no children but keep the element on the page for spacing - .MuiButtonBase-root.MuiIconButton-root.Mui-disabled .MuiSvgIcon-root { - color: transparent; - } + // Hide expand icon when there are no children but keep the element on the page for spacing + .MuiButtonBase-root.MuiIconButton-root.Mui-disabled .MuiSvgIcon-root { + color: transparent; + } `; const MenuLink = styled(Button).attrs({ component: Link, })` - display: inline; - flex: 1; - justify-content: flex-start; - text-transform: none; - padding: 0.8rem; - font-size: 0.875rem; + display: inline; + flex: 1; + justify-content: flex-start; + text-transform: none; + padding: 0.8rem; + font-size: 0.875rem; - .MuiSvgIcon-root { - vertical-align: bottom; - } + .MuiSvgIcon-root { + vertical-align: bottom; + } `; interface EntityMenuProps { diff --git a/packages/tupaia-web/src/features/Visuals/Matrix/Matrix.tsx b/packages/tupaia-web/src/features/Visuals/Matrix/Matrix.tsx index 32a89672bd..924c42d358 100644 --- a/packages/tupaia-web/src/features/Visuals/Matrix/Matrix.tsx +++ b/packages/tupaia-web/src/features/Visuals/Matrix/Matrix.tsx @@ -5,7 +5,7 @@ import React, { useContext, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { useSearchParams } from 'react-router-dom'; +import { useSearchParams, useParams } from 'react-router-dom'; import { Typography } from '@material-ui/core'; import { Matrix as MatrixComponent, NoData, SearchFilter } from '@tupaia/ui-components'; import { DashboardItemType, isMatrixReport } from '@tupaia/types'; @@ -39,6 +39,7 @@ const NoResultsMessage = styled(Typography)` const MatrixVisual = () => { const context = useContext(DashboardItemContext); + const { projectCode } = useParams(); const [urlSearchParams, setUrlSearchParams] = useSearchParams(); const activeDrillDownId = urlSearchParams.get(URL_SEARCH_PARAMS.REPORT_DRILLDOWN_ID); const reportPeriod = urlSearchParams.get(URL_SEARCH_PARAMS.REPORT_PERIOD); @@ -50,8 +51,6 @@ const MatrixVisual = () => { const { columns = [], rows = [] } = report; const [searchFilters, setSearchFilters] = useState([]); - const { drillDown, valueType } = config; - // memoise the parsed rows and columns so that they don't get recalculated on every render, for performance reasons const parsedRows = useMemo( () => @@ -59,21 +58,24 @@ const MatrixVisual = () => { rows, undefined, searchFilters, - drillDown, - valueType, + config, urlSearchParams, setUrlSearchParams, + projectCode!, ), [ JSON.stringify(rows), JSON.stringify(searchFilters), - JSON.stringify(drillDown), - valueType, + JSON.stringify(config), JSON.stringify(urlSearchParams), setUrlSearchParams, + projectCode, ], ); - const parsedColumns = useMemo(() => parseColumns(columns), [JSON.stringify(columns)]); + const parsedColumns = useMemo( + () => parseColumns(columns, projectCode!), + [JSON.stringify(columns), projectCode], + ); const updateSearchFilter = ({ key, value }: SearchFilter) => { const filtersWithoutKey = searchFilters.filter(filter => filter.key !== key); diff --git a/packages/tupaia-web/src/features/Visuals/Matrix/parseData.ts b/packages/tupaia-web/src/features/Visuals/Matrix/parseData.tsx similarity index 77% rename from packages/tupaia-web/src/features/Visuals/Matrix/parseData.ts rename to packages/tupaia-web/src/features/Visuals/Matrix/parseData.tsx index f36fa9b3c1..4c07dc1750 100644 --- a/packages/tupaia-web/src/features/Visuals/Matrix/parseData.ts +++ b/packages/tupaia-web/src/features/Visuals/Matrix/parseData.tsx @@ -4,9 +4,13 @@ */ import { MatrixColumnType, MatrixRowType, SearchFilter } from '@tupaia/ui-components'; import { formatDataValueByType } from '@tupaia/utils'; -import { MatrixConfig, MatrixReportColumn, MatrixReportRow } from '@tupaia/types'; +import { MatrixConfig, MatrixReportColumn, MatrixReportRow, MatrixEntityCell } from '@tupaia/types'; import { URL_SEARCH_PARAMS } from '../../../constants'; +function isMatrixEntityCell(cell: unknown): cell is MatrixEntityCell { + return typeof cell === 'object' && cell !== null && 'entityLabel' in cell && 'entityCode' in cell; +} + const getValueMatchesSearchFilter = (value: any, searchTerm: SearchFilter['value']) => { if (typeof value !== 'string' && typeof value !== 'number') return false; const stringifiedValue = value.toString().toLowerCase(); @@ -24,7 +28,8 @@ const getValueMatchesSearchFilter = (value: any, searchTerm: SearchFilter['value const getRowMatchesSearchFilter = (row: MatrixReportRow, searchFilters: SearchFilter[]) => { return searchFilters.every(filter => { const rowValue = row[filter.key]; - return getValueMatchesSearchFilter(rowValue, filter.value); + const parsedRowValue = isMatrixEntityCell(rowValue) ? rowValue.entityLabel : rowValue; + return getValueMatchesSearchFilter(parsedRowValue, filter.value); }); }; @@ -33,11 +38,13 @@ export const parseRows = ( rows: MatrixReportRow[], categoryId: MatrixReportRow['categoryId'] | undefined, searchFilters: SearchFilter[], - drillDown: MatrixConfig['drillDown'] | undefined, - valueType: MatrixConfig['valueType'] | undefined, + config: MatrixConfig, urlSearchParams: URLSearchParams, setUrlSearchParams: (searchParams: URLSearchParams) => void, + projectCode: string, ): MatrixRowType[] => { + const { drillDown, valueType } = config; + const onDrillDown = row => { if (!drillDown) return; const { itemCode, parameterLink } = drillDown; @@ -60,18 +67,19 @@ export const parseRows = ( } // loop through the topLevelRows, and parse them into the format that the Matrix component can use return topLevelRows.reduce((result: MatrixRowType[], row: MatrixReportRow) => { - const { dataElement = '', category, valueType: rowValueType, ...rest } = row; + const { dataElement, category, valueType: rowValueType, ...rest } = row; const valueTypeToUse = rowValueType || valueType; // if the row has a category, then it has children, so we need to parse them using this same function + if (category) { const children = parseRows( rows, category, searchFilters, - drillDown, - valueTypeToUse, + { ...config, valueType: valueTypeToUse }, urlSearchParams, setUrlSearchParams, + projectCode, ); if (searchFilters.length > 0) { @@ -97,6 +105,7 @@ export const parseRows = ( // some items are objects, and we need to parse them to get the value if (typeof item === 'object' && item !== null) { const { value, metadata } = item as { value: any; metadata?: any }; + acc[key] = formatDataValueByType( { value, @@ -104,11 +113,13 @@ export const parseRows = ( }, valueTypeToUse, ); + return acc; } acc[key] = formatDataValueByType({ value: item }, valueTypeToUse); return acc; }, {}); + // if the row is a regular row, and there is a search filter, then we need to check if the row matches the search filter, and ignore this row if it doesn't. This filter only applies to standard rows, not category rows. if (searchFilters?.length > 0) { const matchesSearchFilter = getRowMatchesSearchFilter( @@ -121,27 +132,48 @@ export const parseRows = ( if (!matchesSearchFilter) return result; } - // otherwise, handle as a regular row - result.push({ - title: dataElement, + + const newResult = { + title: dataElement || '', onClick: drillDown ? () => onDrillDown(row) : undefined, ...formattedRowValues, - }); + } as MatrixRowType; + + // if the row is a matrix entity cell, then we need to add the entityLink to the row + if (isMatrixEntityCell(dataElement)) { + const entityLink = `/${projectCode}/${dataElement.entityCode}`; + newResult.title = dataElement.entityLabel; + newResult.entityLink = entityLink; + } + + result.push(newResult); return result; }, []); }; // This is a recursive function that parses the columns of the matrix into a format that the Matrix component can use. -export const parseColumns = (columns: MatrixReportColumn[]): MatrixColumnType[] => { +export const parseColumns = ( + columns: MatrixReportColumn[], + projectCode: string, +): MatrixColumnType[] => { return columns.map(column => { - const { category, key, title, columns: children } = column; + const { category, key, title, columns: children, entityCode } = column; // if a column has a category, then it has children, so we need to parse them using this same function - if (category) + if (category) { return { title: category, key: category, - children: parseColumns(children!), + children: parseColumns(children!, projectCode), + }; + } + + if (entityCode) { + return { + title, + key, + entityLink: `/${projectCode}/${entityCode}`, }; + } // otherwise, handle as a regular column return { title, diff --git a/packages/types/src/schemas/schemas.ts b/packages/types/src/schemas/schemas.ts index ed59234ae5..73bd76f07b 100644 --- a/packages/types/src/schemas/schemas.ts +++ b/packages/types/src/schemas/schemas.ts @@ -1533,6 +1533,23 @@ export const MatrixConfigSchema = { ] } +export const MatrixEntityCellSchema = { + "type": "object", + "properties": { + "entityCode": { + "type": "string" + }, + "entityLabel": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "entityCode", + "entityLabel" + ] +} + export const MatrixVizBuilderConfigSchema = { "additionalProperties": false, "type": "object", @@ -2266,7 +2283,7 @@ export const MatrixVizBuilderConfigSchema = { "type": "string" }, "columns": { - "description": "The columns of the data-table that should be included as columns in the matrix.\nCan be either a list of column names, or '*' to indicate all columns", + "description": "The columns of the data-table that should be included as columns in the matrix.\nCan be either:\na list of column names,\n'*' to indicate all columns\nor a list of objects with an entityCode and entityLabel to generate entity links", "anyOf": [ { "type": "array", @@ -2274,6 +2291,25 @@ export const MatrixVizBuilderConfigSchema = { "type": "string" } }, + { + "type": "array", + "items": { + "type": "object", + "properties": { + "entityCode": { + "type": "string" + }, + "entityLabel": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "entityCode", + "entityLabel" + ] + } + }, { "type": "string" } @@ -31884,7 +31920,27 @@ export const MatrixReportRowSchema = { "type": "object", "properties": { "dataElement": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "entityCode": { + "type": "string" + }, + "entityLabel": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "entityCode", + "entityLabel" + ] + }, + { + "type": "string" + } + ] }, "categoryId": { "type": "string" @@ -32159,7 +32215,27 @@ export const MatrixReportSchema = { "type": "object", "properties": { "dataElement": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "entityCode": { + "type": "string" + }, + "entityLabel": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "entityCode", + "entityLabel" + ] + }, + { + "type": "string" + } + ] }, "categoryId": { "type": "string" @@ -32416,7 +32492,27 @@ export const DashboardItemReportSchema = { "type": "object", "properties": { "dataElement": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "entityCode": { + "type": "string" + }, + "entityLabel": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "entityCode", + "entityLabel" + ] + }, + { + "type": "string" + } + ] }, "categoryId": { "type": "string" diff --git a/packages/types/src/types/index.ts b/packages/types/src/types/index.ts index fa6b9258c1..c144cda737 100644 --- a/packages/types/src/types/index.ts +++ b/packages/types/src/types/index.ts @@ -23,6 +23,8 @@ export { CartesianChartConfig, ValueType, MatrixConfig, + MatrixVizBuilderConfig, + MatrixEntityCell, PresentationOptionCondition, MatrixPresentationOptions, ConditionsObject, diff --git a/packages/types/src/types/models-extra/dashboard-item/index.ts b/packages/types/src/types/models-extra/dashboard-item/index.ts index db9779653e..b3a795e14e 100644 --- a/packages/types/src/types/models-extra/dashboard-item/index.ts +++ b/packages/types/src/types/models-extra/dashboard-item/index.ts @@ -13,6 +13,8 @@ import type { RangePresentationOptions, ConditionalPresentationOptions, PresentationOptionRange, + MatrixVizBuilderConfig, + MatrixEntityCell, } from './matricies'; import type { ComponentConfig } from './components'; import type { ChartConfig, ChartPresentationOptions } from './charts'; @@ -62,6 +64,8 @@ export type DashboardItemConfig = ChartConfig | ComponentConfig | MatrixConfig | export { ValueType, ExportPresentationOptions, DatePickerOffsetSpec } from './common'; export type { MatrixConfig, + MatrixEntityCell, + MatrixVizBuilderConfig, PresentationOptionCondition, MatrixPresentationOptions, ConditionsObject, diff --git a/packages/types/src/types/models-extra/dashboard-item/matricies.ts b/packages/types/src/types/models-extra/dashboard-item/matricies.ts index 745fdb920f..cfe9ecd605 100644 --- a/packages/types/src/types/models-extra/dashboard-item/matricies.ts +++ b/packages/types/src/types/models-extra/dashboard-item/matricies.ts @@ -45,6 +45,8 @@ export type MatrixConfig = BaseConfig & { enableSearch?: boolean; }; +export type MatrixEntityCell = { entityCode: string; entityLabel: string }; + export type MatrixVizBuilderConfig = MatrixConfig & { /** * @description Configuration for rows, columns, and categories of the matrix @@ -65,9 +67,12 @@ export type MatrixVizBuilderConfig = MatrixConfig & { /** * @description * The columns of the data-table that should be included as columns in the matrix. - * Can be either a list of column names, or '*' to indicate all columns + * Can be either: + * a list of column names, + * '*' to indicate all columns + * or a list of objects with an entityCode and entityLabel to generate entity links */ - columns?: string | string[]; + columns?: string | string[] | MatrixEntityCell[]; }; }; diff --git a/packages/types/src/types/models-extra/index.ts b/packages/types/src/types/models-extra/index.ts index 97270a8f69..a32ec85e49 100644 --- a/packages/types/src/types/models-extra/index.ts +++ b/packages/types/src/types/models-extra/index.ts @@ -43,6 +43,8 @@ export { CartesianChartConfig, ValueType, MatrixConfig, + MatrixVizBuilderConfig, + MatrixEntityCell, PresentationOptionCondition, MatrixPresentationOptions, ConditionsObject, diff --git a/packages/types/src/types/models-extra/report.ts b/packages/types/src/types/models-extra/report.ts index f300e0c502..3104c62b6b 100644 --- a/packages/types/src/types/models-extra/report.ts +++ b/packages/types/src/types/models-extra/report.ts @@ -3,7 +3,7 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ -import { ValueType, ViewConfig } from './dashboard-item'; +import { ValueType, ViewConfig, MatrixEntityCell } from './dashboard-item'; type Transform = string | Record; @@ -24,7 +24,7 @@ export type ReportConfig = StandardReportConfig | CustomReportConfig; // This is the row type in the response from the report endpoint when the report is a matrix. It will contain data for each column, keyed by the column key, as well as dataElement, categoryId and category export type MatrixReportRow = Record & { - dataElement?: string; // this is the data to display in the row header cell + dataElement?: string | MatrixEntityCell; // this is the data to display in the row header cell categoryId?: string; // this means the row is a child of a grouped row category?: string; // this means the row is a grouped row valueType?: ValueType; @@ -34,6 +34,7 @@ export type MatrixReportRow = Record & { export type MatrixReportColumn = { title: string; key: string; + entityCode: string; category?: string; // this means the column is a grouped column columns?: MatrixReportColumn[]; // these are the child columns of a grouped column }; diff --git a/packages/ui-components/src/components/Matrix/MatrixHeader.tsx b/packages/ui-components/src/components/Matrix/MatrixHeader.tsx index 7490b25237..cf32d37c59 100644 --- a/packages/ui-components/src/components/Matrix/MatrixHeader.tsx +++ b/packages/ui-components/src/components/Matrix/MatrixHeader.tsx @@ -5,6 +5,7 @@ import React, { useContext } from 'react'; import { darken, TableHead, TableRow } from '@material-ui/core'; import styled from 'styled-components'; +import { Link as RouterLink } from 'react-router-dom-v6'; import { MatrixColumnType } from '../../types'; import { MatrixContext } from './MatrixContext'; import { HeaderCell } from './Cell'; @@ -23,6 +24,22 @@ const THead = styled(TableHead)` top: 0; z-index: 3; `; + +const CellLink = styled(RouterLink)` + color: ${({ theme }) => theme.palette.text.primary}; + text-decoration: none; + &:hover { + text-decoration: underline; + font-weight: 700; + } +`; + +const TitleCell = ({ title, entityLink }: { title: string; entityLink?: string }) => { + if (entityLink) { + return {title}; + } + return <>{title}; +}; /** * This is a component that renders the header rows in the matrix. It renders the column groups and columns. */ @@ -76,17 +93,16 @@ export const MatrixHeader = ({ {/** If hasParents is true, then this row header column cell will have already been rendered. */} {!hasParents && RowHeaderColumn} - {flattenedColumns.map(({ title, key }) => ( + {flattenedColumns.map(({ title, key, entityLink }) => ( - {!hideColumnTitles && title} + {!hideColumnTitles && } ))} - diff --git a/packages/ui-components/src/components/Matrix/MatrixRow.tsx b/packages/ui-components/src/components/Matrix/MatrixRow.tsx index 3bbbfa39fd..837c20022a 100644 --- a/packages/ui-components/src/components/Matrix/MatrixRow.tsx +++ b/packages/ui-components/src/components/Matrix/MatrixRow.tsx @@ -7,6 +7,7 @@ import React, { useContext } from 'react'; import { ButtonProps, TableRow as MuiTableRow } from '@material-ui/core'; import { KeyboardArrowRight } from '@material-ui/icons'; import styled from 'styled-components'; +import { Link as RouterLink } from 'react-router-dom-v6'; import { MatrixRowType } from '../../types'; import { Button } from '../Button'; import { MatrixCell } from './MatrixCell'; @@ -170,6 +171,7 @@ interface MatrixRowProps { type MatrixRowHeaderProps = { depth: number; + entityLink?: string; isExpanded: boolean; rowTitle: string; hasChildren: boolean; @@ -215,12 +217,22 @@ const ClickableRowHeaderCell = ({ ); }; +const RowLink = styled(RouterLink)` + color: ${({ theme }) => theme.palette.text.primary}; + text-decoration: none; + &:hover { + font-weight: 700; + text-decoration: underline; + } +`; + /** * This component renders the first cell of a row. It renders a button to expand/collapse the row if * it has children, otherwise it renders a regular cell. */ const RowHeaderCell = ({ rowTitle, + entityLink, depth, isExpanded, hasChildren, @@ -237,6 +249,17 @@ const RowHeaderCell = ({ } }; + if (entityLink) { + return ( + + {rowTitle} + + ); + } + if (hasChildren) return ( { - const { children, title, onClick } = row; + const { children, title, entityLink, onClick } = row; const { columns, expandedRows, disableExpand = false, searchFilters } = useContext(MatrixContext); const flattenedColumns = getFlattenedColumns(columns); @@ -307,6 +330,7 @@ export const MatrixRow = ({ row, parents = [], index }: MatrixRowProps) => { isExpanded={isExpanded} depth={depth} rowTitle={title} + entityLink={entityLink} hasChildren={isCategory} disableExpandButton={disableExpand} onClick={onClick} diff --git a/packages/ui-components/src/types/types.ts b/packages/ui-components/src/types/types.ts index ede5bd9c5a..befb003d21 100644 --- a/packages/ui-components/src/types/types.ts +++ b/packages/ui-components/src/types/types.ts @@ -11,6 +11,7 @@ export type OverrideableComponentProps

= P & export type MatrixColumnType = { key: string; title: string; + entityLink?: string; children?: MatrixColumnType[]; }; diff --git a/packages/ui-components/stories/fixtures/matrix.js b/packages/ui-components/stories/fixtures/matrix.js new file mode 100644 index 0000000000..9250668822 --- /dev/null +++ b/packages/ui-components/stories/fixtures/matrix.js @@ -0,0 +1,517 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +export const basicColumns = [ + { + key: 'Col1', + title: 'Demo country 1', + }, + { + key: 'Col2', + title: 'Demo country 2', + }, + { + key: 'Col3', + title: 'Demo country 3', + }, + { + key: 'Col4', + title: 'Demo country 4', + }, +]; + +export const groupedColumns = [ + { + title: 'Column group 1', + children: [ + { + key: 'Col1', + title: 'Col1', + }, + { + key: 'Col2', + title: 'Col2', + }, + { + key: 'Col3', + title: 'Col3', + }, + ], + }, + { + title: 'Column group 2', + children: [ + { + key: 'Col4', + title: 'Col4', + }, + { + key: 'Col5', + title: 'Col5', + }, + { + key: 'Col6', + title: 'Col6', + }, + ], + }, +]; + +export const basicRows = [ + { + title: 'Data item 1', + Col4: 59.5, + Col2: 43.4, + }, + { + title: 'Data item 2', + Col1: 6.76, + Col4: 74.2, + Col3: 44.998, + }, + { + title: 'Data item 3', + Col1: 33.9, + Col4: 11.749, + Col2: 6.347, + Col3: 35.9, + }, + { + title: 'Data item 4', + Col1: 0.05, + Col4: 32.11, + Col2: 0, + Col3: 7.1, + }, + { + title: 'Data item 5', + Col1: 320, + Col4: 17, + Col2: 69.325, + Col3: 142.68, + }, + { + title: 'Data item 6', + Col1: 534, + Col3: 0, + Col5: 10, + Col6: 3, + }, +]; + +export const entityLinkColumns = [ + { + key: 'Col1', + title: 'Demo country 1', + entityLink: '/explore/demo-country-1', + }, + { + key: 'Col2', + title: 'Demo country 2', + entityLink: '/explore/demo-country-2', + }, + { + key: 'Col3', + title: 'Demo country 3', + entityLink: '/explore/demo-country-3', + }, + { + key: 'Col4', + title: 'Demo country 4', + entityLink: '/explore/demo-country-4', + }, +]; + +export const entityLinkRows = [ + { + title: 'Data item 1', + entityLink: '/explore/data-item-1', + Col4: 59.5, + Col2: 43.4, + }, + { + title: 'Data item 2', + entityLink: '/explore/data-item-2', + Col1: 6.76, + Col4: 74.2, + Col3: 44.998, + }, + { + title: 'Data item 3', + entityLink: '/explore/data-item-3', + Col1: 33.9, + Col4: 11.749, + Col2: 6.347, + Col3: 35.9, + }, + { + title: 'Data item 4', + entityLink: '/explore/data-item-4', + Col1: 0.05, + Col4: 32.11, + Col2: 0, + Col3: 7.1, + }, + { + title: 'Data item 5', + entityLink: '/explore/data-item-5', + Col1: 320, + Col4: 17, + Col2: 69.325, + Col3: 142.68, + }, + { + title: 'Data item 6', + entityLink: '/explore/data-item-6', + Col1: 534, + Col3: 0, + Col5: 10, + Col6: 3, + }, +]; + +export const groupedRows = [ + { + title: 'Data item 1', + children: [ + { + title: 'Sub item 1', + Col1: 0, + Col2: '0', + Col3: 43.4, + Col4: 59.5, + }, + { + title: 'Sub item 2', + Col1: 6.76, + Col4: 74.2, + Col3: 44.998, + }, + { + title: 'Sub item 3', + Col1: 33.9, + Col4: 11.749, + Col2: 6.347, + Col3: 35.9, + }, + { + title: 'Sub item 4', + Col1: 0.05, + Col4: 32.11, + Col2: 0, + Col3: 7.1, + }, + { + title: 'Sub item 5', + Col1: 320, + Col4: 17, + Col2: 69.325, + Col3: 142.68, + }, + { + title: 'Sub item 6', + Col1: 534, + Col3: 0, + }, + ], + }, + { + title: 'Data item 2', + children: [ + { + title: 'Sub item 7', + Col1: 661, + Col4: 0, + Col2: 3.78, + Col3: 0, + }, + { + title: 'Sub item 8', + Col4: 24, + Col3: 53.4, + }, + { + title: 'Sub item 9', + Col4: 0, + Col2: 7.92, + }, + { + title: 'Sub item 10', + Col1: 46.22, + Col4: 2.35, + Col3: 1.62, + }, + { + title: 'Sub item 11', + }, + ], + }, + { + title: 'Data item 3', + children: [ + { + title: 'Sub item 12', + children: [ + { + title: 'Sub item 13', + Col1: 661.7, + Col4: 0, + Col2: 3.78, + Col3: 0, + }, + { + title: 'Sub item 14', + Col4: 24.34, + Col3: 53.43, + }, + ], + }, + { + title: 'Sub item 15', + children: [ + { + title: 'Sub item 16', + Col4: 22.86, + Col2: 19.24, + Col3: 1.532, + }, + { + title: 'Sub item 17', + Col4: 0, + Col2: 7.92, + }, + ], + }, + ], + }, +]; + +export const groupedRowsWithCategoryData = [ + { + title: 'Data item 1', + Col1: 10, + Col2: 20, + Col3: 30, + Col4: 40, + children: [ + { + title: 'Sub item 1', + Col4: 59.5, + Col2: 43.4, + }, + { + title: 'Sub item 2', + Col1: 6.76, + Col4: 74.2, + Col3: 44.998, + }, + { + title: 'Sub item 3', + Col1: 33.9, + Col4: 11.749, + Col2: 6.347, + Col3: 35.9, + }, + { + title: 'Sub item 4', + Col1: 0.05, + Col4: 32.11, + Col2: 0, + Col3: 7.1, + }, + { + title: 'Sub item 5', + Col1: 320, + Col4: 17, + Col2: 69.325, + Col3: 142.68, + }, + { + title: 'Sub item 6', + Col1: 534, + Col3: 0, + }, + ], + }, + { + title: 'Data item 2', + Col1: 1, + Col2: 2, + Col3: 4, + Col4: 25, + children: [ + { + title: 'Sub item 7', + Col1: 661, + Col4: 0, + Col2: 3.78, + Col3: 0, + }, + { + title: 'Sub item 8', + Col4: 24, + Col3: 53.4, + }, + { + title: 'Sub item 9', + Col4: 0, + Col2: 7.92, + }, + { + title: 'Sub item 10', + Col1: 46.22, + Col4: 2.35, + Col3: 1.62, + }, + { + title: 'Sub item 11', + }, + ], + }, + { + title: 'Data item 3', + Col1: 2, + Col4: 40, + children: [ + { + title: 'Sub item 12', + Col1: 10, + Col4: 50, + children: [ + { + title: 'Sub item 13', + Col1: 661.7, + Col4: 0, + Col2: 3.78, + Col3: 0, + }, + { + title: 'Sub item 14', + Col4: 24.34, + Col3: 53.43, + }, + ], + }, + { + title: 'Sub item 15', + children: [ + { + title: 'Sub item 16', + Col4: 22.86, + Col2: 19.24, + Col3: 1.532, + }, + { + title: 'Sub item 17', + Col4: 0, + Col2: 7.92, + }, + ], + }, + ], + }, +]; + +export const rangeCategoryPresentationOptions = { + red: { + max: 20, + min: 0, + color: '#b71c1c', + label: '', + description: 'Averaged score:', + }, + type: 'range', + green: { + max: 100, + min: 70, + color: '#33691e', + label: '', + description: 'Averaged score:', + }, + yellow: { + max: 69, + min: 21, + color: '#fdd835', + label: '', + description: 'Averaged score:', + }, + showRawValue: true, +}; + +export const markdownPresentationOptions = { + conditions: [ + { + key: '0', + color: '#525258', + label: '', + description: 'A **Blank** cell means this.\n', + }, + { + key: '1', + color: '#279A63', + label: '', + description: '**Green** signifies something else.\n', + }, + { + key: '2', + color: '#EE9A30', + label: '', + description: '**Orange** signifies another thing.\n', + }, + { + key: '3', + color: '#EE4230', + label: '', + description: '**Red** signifies this other thing.\n', + }, + ], +}; + +export const dotPresentationOptions = { + type: 'condition', + conditions: [ + { + key: 'red', + color: '#b71c1c', + label: 'Secondary header', + condition: 0, + description: 'Months of stock: ', + legendLabel: 'Stock out (MOS 0)', + }, + { + key: 'orange', + color: '#EE9A30', + label: '', + condition: { + '<': 6, + '>': 0, + }, + description: 'Months of stock: ', + legendLabel: 'Below minimum (MOS 1-5)', + }, + { + key: 'green', + color: '#33691e', + label: '', + condition: { + '<': 18, + '>=': 6, + }, + description: 'Months of stock: ', + legendLabel: 'Stocked appropriately (MOS 6-17)', + }, + { + key: 'yellow', + color: '#fdd835', + label: '', + condition: { + '>=': 18, + }, + description: 'Months of stock: ', + legendLabel: 'Overstock (MOS 18+)', + }, + ], + showRawValue: true, +}; diff --git a/packages/ui-components/stories/matrix.stories.jsx b/packages/ui-components/stories/matrix.stories.jsx index a0fe55e5e5..1970593572 100644 --- a/packages/ui-components/stories/matrix.stories.jsx +++ b/packages/ui-components/stories/matrix.stories.jsx @@ -6,7 +6,19 @@ import React from 'react'; import { Box } from '@material-ui/core'; import styled from 'styled-components'; -import { Matrix } from '../src/components/Matrix/Matrix.tsx'; +import { Matrix } from '../src'; +import { + groupedRows, + entityLinkRows, + groupedColumns, + basicColumns, + basicRows, + groupedRowsWithCategoryData, + dotPresentationOptions, + markdownPresentationOptions, + rangeCategoryPresentationOptions, + entityLinkColumns, +} from './fixtures/matrix'; const Container = styled(Box)` background: ${({ theme }) => theme.palette.background.default}; @@ -16,6 +28,7 @@ const Container = styled(Box)` display: flex; flex-direction: column; `; + export default { title: 'Matrix', decorators: [ @@ -31,448 +44,6 @@ export default { }, }; -const groupedRows = [ - { - title: 'Data item 1', - children: [ - { - title: 'Sub item 1', - Col1: 0, - Col2: '0', - Col3: 43.4, - Col4: 59.5, - }, - { - title: 'Sub item 2', - Col1: 6.76, - Col4: 74.2, - Col3: 44.998, - }, - { - title: 'Sub item 3', - Col1: 33.9, - Col4: 11.749, - Col2: 6.347, - Col3: 35.9, - }, - { - title: 'Sub item 4', - Col1: 0.05, - Col4: 32.11, - Col2: 0, - Col3: 7.1, - }, - { - title: 'Sub item 5', - Col1: 320, - Col4: 17, - Col2: 69.325, - Col3: 142.68, - }, - { - title: 'Sub item 6', - Col1: 534, - Col3: 0, - }, - ], - }, - { - title: 'Data item 2', - children: [ - { - title: 'Sub item 7', - Col1: 661, - Col4: 0, - Col2: 3.78, - Col3: 0, - }, - { - title: 'Sub item 8', - Col4: 24, - Col3: 53.4, - }, - { - title: 'Sub item 9', - Col4: 0, - Col2: 7.92, - }, - { - title: 'Sub item 10', - Col1: 46.22, - Col4: 2.35, - Col3: 1.62, - }, - { - title: 'Sub item 11', - }, - ], - }, - { - title: 'Data item 3', - children: [ - { - title: 'Sub item 12', - children: [ - { - title: 'Sub item 13', - Col1: 661.7, - Col4: 0, - Col2: 3.78, - Col3: 0, - }, - { - title: 'Sub item 14', - Col4: 24.34, - Col3: 53.43, - }, - ], - }, - { - title: 'Sub item 15', - children: [ - { - title: 'Sub item 16', - Col4: 22.86, - Col2: 19.24, - Col3: 1.532, - }, - { - title: 'Sub item 17', - Col4: 0, - Col2: 7.92, - }, - ], - }, - ], - }, -]; - -const basicRows = [ - { - title: 'Data item 1', - Col4: 59.5, - Col2: 43.4, - }, - { - title: 'Data item 2', - Col1: 6.76, - Col4: 74.2, - Col3: 44.998, - }, - { - title: 'Data item 3', - Col1: 33.9, - Col4: 11.749, - Col2: 6.347, - Col3: 35.9, - }, - { - title: 'Data item 4', - Col1: 0.05, - Col4: 32.11, - Col2: 0, - Col3: 7.1, - }, - { - title: 'Data item 5', - Col1: 320, - Col4: 17, - Col2: 69.325, - Col3: 142.68, - }, - { - title: 'Data item 6', - Col1: 534, - Col3: 0, - Col5: 10, - Col6: 3, - }, -]; - -const basicColumns = [ - { - key: 'Col1', - title: 'Demo country 1', - }, - { - key: 'Col2', - title: 'Demo country 2', - }, - { - key: 'Col3', - title: 'Demo country 3', - }, - { - key: 'Col4', - title: 'Demo country 4', - }, -]; - -const groupedColumns = [ - { - title: 'Column group 1', - children: [ - { - key: 'Col1', - title: 'Col1', - }, - { - key: 'Col2', - title: 'Col2', - }, - { - key: 'Col3', - title: 'Col3', - }, - ], - }, - { - title: 'Column group 2', - children: [ - { - key: 'Col4', - title: 'Col4', - }, - { - key: 'Col5', - title: 'Col5', - }, - { - key: 'Col6', - title: 'Col6', - }, - ], - }, -]; - -const dotPresentationOptions = { - type: 'condition', - conditions: [ - { - key: 'red', - color: '#b71c1c', - label: 'Secondary header', - condition: 0, - description: 'Months of stock: ', - legendLabel: 'Stock out (MOS 0)', - }, - { - key: 'orange', - color: '#EE9A30', - label: '', - condition: { - '<': 6, - '>': 0, - }, - description: 'Months of stock: ', - legendLabel: 'Below minimum (MOS 1-5)', - }, - { - key: 'green', - color: '#33691e', - label: '', - condition: { - '<': 18, - '>=': 6, - }, - description: 'Months of stock: ', - legendLabel: 'Stocked appropriately (MOS 6-17)', - }, - { - key: 'yellow', - color: '#fdd835', - label: '', - condition: { - '>=': 18, - }, - description: 'Months of stock: ', - legendLabel: 'Overstock (MOS 18+)', - }, - ], - showRawValue: true, -}; - -const markdownPresentationOptions = { - conditions: [ - { - key: '0', - color: '#525258', - label: '', - description: 'A **Blank** cell means this.\n', - }, - { - key: '1', - color: '#279A63', - label: '', - description: '**Green** signifies something else.\n', - }, - { - key: '2', - color: '#EE9A30', - label: '', - description: '**Orange** signifies another thing.\n', - }, - { - key: '3', - color: '#EE4230', - label: '', - description: '**Red** signifies this other thing.\n', - }, - ], -}; - -const groupedRowsWithCategoryData = [ - { - title: 'Data item 1', - Col1: 10, - Col2: 20, - Col3: 30, - Col4: 40, - children: [ - { - title: 'Sub item 1', - Col4: 59.5, - Col2: 43.4, - }, - { - title: 'Sub item 2', - Col1: 6.76, - Col4: 74.2, - Col3: 44.998, - }, - { - title: 'Sub item 3', - Col1: 33.9, - Col4: 11.749, - Col2: 6.347, - Col3: 35.9, - }, - { - title: 'Sub item 4', - Col1: 0.05, - Col4: 32.11, - Col2: 0, - Col3: 7.1, - }, - { - title: 'Sub item 5', - Col1: 320, - Col4: 17, - Col2: 69.325, - Col3: 142.68, - }, - { - title: 'Sub item 6', - Col1: 534, - Col3: 0, - }, - ], - }, - { - title: 'Data item 2', - Col1: 1, - Col2: 2, - Col3: 4, - Col4: 25, - children: [ - { - title: 'Sub item 7', - Col1: 661, - Col4: 0, - Col2: 3.78, - Col3: 0, - }, - { - title: 'Sub item 8', - Col4: 24, - Col3: 53.4, - }, - { - title: 'Sub item 9', - Col4: 0, - Col2: 7.92, - }, - { - title: 'Sub item 10', - Col1: 46.22, - Col4: 2.35, - Col3: 1.62, - }, - { - title: 'Sub item 11', - }, - ], - }, - { - title: 'Data item 3', - Col1: 2, - Col4: 40, - children: [ - { - title: 'Sub item 12', - Col1: 10, - Col4: 50, - children: [ - { - title: 'Sub item 13', - Col1: 661.7, - Col4: 0, - Col2: 3.78, - Col3: 0, - }, - { - title: 'Sub item 14', - Col4: 24.34, - Col3: 53.43, - }, - ], - }, - { - title: 'Sub item 15', - children: [ - { - title: 'Sub item 16', - Col4: 22.86, - Col2: 19.24, - Col3: 1.532, - }, - { - title: 'Sub item 17', - Col4: 0, - Col2: 7.92, - }, - ], - }, - ], - }, -]; - -const rangeCategoryPresentationOptions = { - red: { - max: 20, - min: 0, - color: '#b71c1c', - label: '', - description: 'Averaged score:', - }, - type: 'range', - green: { - max: 100, - min: 70, - color: '#33691e', - label: '', - description: 'Averaged score:', - }, - yellow: { - max: 69, - min: 21, - color: '#fdd835', - label: '', - description: 'Averaged score:', - }, - showRawValue: true, -}; - const applyLocationPresentationOptions = { ...dotPresentationOptions, applyLocation: { @@ -488,6 +59,12 @@ Simple.args = { columns: basicColumns, }; +export const RowEntityLinks = Template.bind({}); +RowEntityLinks.args = { + rows: entityLinkRows, + columns: entityLinkColumns, +}; + export const Dots = Template.bind({}); Dots.args = { rows: [ diff --git a/packages/web-config-server/src/export/excelFormatters/formatMatrixDataForExcel.js b/packages/web-config-server/src/export/excelFormatters/formatMatrixDataForExcel.js index 0385585166..be832b265c 100644 --- a/packages/web-config-server/src/export/excelFormatters/formatMatrixDataForExcel.js +++ b/packages/web-config-server/src/export/excelFormatters/formatMatrixDataForExcel.js @@ -1,9 +1,14 @@ import { addExportedDateAndOriginAtTheSheetBottom, formatDataValueByType } from '@tupaia/utils'; +import { MatrixEntityCell } from '@tupaia/types'; const DEFAULT_CONFIG = { dataElementHeader: 'Data Element', }; +function isMatrixEntityCell(cell) { + return typeof cell === 'object' && cell !== null && 'entityLabel' in cell && 'entityCode' in cell; +} + // Takes in the data for a matrix type view and converts it into an array of arrays representing // an Excel spreadsheet in the format required by the xlsx library, i.e. an array of arrays, // with each child array in the array representing a row in the spreadsheet, @@ -56,7 +61,11 @@ export const formatMatrixDataForExcel = ( columns.map(col => addValueOrEmpty(row[col.key], row.valueType)); // prepend dataElementHeader - rowData.unshift(row.dataElement); + if (isMatrixEntityCell(row.dataElement)) { + rowData.unshift(row.dataElement.entityLabel); + } else { + rowData.unshift(row.dataElement); + } return rowData; }; diff --git a/packages/web-config-server/src/export/exportChartHandler.js b/packages/web-config-server/src/export/exportChartHandler.js index 8ae51ce0e5..811835f9ac 100644 --- a/packages/web-config-server/src/export/exportChartHandler.js +++ b/packages/web-config-server/src/export/exportChartHandler.js @@ -57,7 +57,7 @@ export const exportChartHandler = async (req, res) => { true, { authorization: authHeader, - } + }, ); const matrixData = {