From a064006313f0c088dafdbe54fa89ef494b36b19d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yhoan=20Alejandro=20Guzm=C3=A1n=20Garc=C3=ADa?= <41337901+yaguzmang@users.noreply.github.com> Date: Wed, 1 Jan 2025 23:49:15 -0500 Subject: [PATCH] 4001 - Datatable use Datagrid - Data download (#4192) * 4001 - Use filename logic from ButtonTableExport in ButtonGridExport * 4001 - Handle cells that are calculated and input * 4001 - Handle spanning cells in DataGrid export * 4001 - Fix table 7c NWFP category export * 4001 - Fix odp year with status not being exported * 4001 - Fix table 3a duplicated text bug * 4001 - Fix growingStockComposition2025 export layout * 4001 - Update useClassName hook * 4001 - Use route params in OdpHeaderCell * 4001 - Highlight only non text input calculated cells --- .../ButtonGridExport/ButtonGridExport.tsx | 4 +- .../ButtonGridExport/hooks/useFilename.ts | 13 +++ .../DataGrid/ButtonGridExport/utils.ts | 91 ++++++++++++++++--- .../NationalClasses/components/Title.tsx | 2 +- .../OdpHeaderCell/OdpHeaderCell.tsx | 35 +++---- .../DataTable/Table/RowData/Cell/Cell.scss | 6 +- .../Table/RowData/Cell/hooks/useClassName.ts | 27 ++++-- .../pages/Section/DataTable/Table/Table.tsx | 13 ++- ...-datatable-grid-layouts-and-taxon-codes.ts | 19 +++- 9 files changed, 165 insertions(+), 45 deletions(-) create mode 100644 src/client/components/DataGrid/ButtonGridExport/hooks/useFilename.ts diff --git a/src/client/components/DataGrid/ButtonGridExport/ButtonGridExport.tsx b/src/client/components/DataGrid/ButtonGridExport/ButtonGridExport.tsx index a065eb7902..657cf488ef 100644 --- a/src/client/components/DataGrid/ButtonGridExport/ButtonGridExport.tsx +++ b/src/client/components/DataGrid/ButtonGridExport/ButtonGridExport.tsx @@ -6,6 +6,7 @@ import { useIsPrintRoute } from 'client/hooks/useIsRoute' import { ButtonProps, useButtonClassName } from 'client/components/Buttons/Button' import Icon from 'client/components/Icon' +import { useFilename } from './hooks/useFilename' import * as Utils from './utils' type Props = Pick & { @@ -15,7 +16,7 @@ type Props = Pick & { } const ButtonGridExport: React.FC = (props) => { - const { disabled, filename, gridRef, size } = props + const { disabled, filename: filenameProp, gridRef, size } = props const [data, setData] = useState>([]) @@ -23,6 +24,7 @@ const ButtonGridExport: React.FC = (props) => { const isLocked = useIsDataLocked() const className = useButtonClassName({ disabled: !isLocked || disabled, iconName: 'hit-down', label: 'CSV', size }) + const filename = useFilename(filenameProp) if (print) return null diff --git a/src/client/components/DataGrid/ButtonGridExport/hooks/useFilename.ts b/src/client/components/DataGrid/ButtonGridExport/hooks/useFilename.ts new file mode 100644 index 0000000000..7866e6620e --- /dev/null +++ b/src/client/components/DataGrid/ButtonGridExport/hooks/useFilename.ts @@ -0,0 +1,13 @@ +import { useMemo } from 'react' + +import { Dates } from 'utils/dates' + +import { useCycleRouteParams } from 'client/hooks/useRouteParams' + +export const useFilename = (filename: string): string => { + const { assessmentName, cycleName } = useCycleRouteParams() + return useMemo(() => { + const date = Dates.format(new Date(), 'yyyy-MM-dd') + return `${assessmentName}-${cycleName}-${filename}-${date}.csv` + }, [assessmentName, cycleName, filename]) +} diff --git a/src/client/components/DataGrid/ButtonGridExport/utils.ts b/src/client/components/DataGrid/ButtonGridExport/utils.ts index 9623a71819..4988774311 100644 --- a/src/client/components/DataGrid/ButtonGridExport/utils.ts +++ b/src/client/components/DataGrid/ButtonGridExport/utils.ts @@ -1,12 +1,15 @@ +import { Objects } from 'utils/objects' + // Converts one or mutiple white spaces or break lines into one space. const normalizeString = (string = '') => string.trim().replace(/\s+/g, ' ') const _getElementText = (element: HTMLElement): string => { if (typeof element === 'string') return normalizeString(element) - const { children, innerText, style } = element + const { children, innerText } = element + const computedStyle = getComputedStyle(element) - if (style && (style.visibility === 'hidden' || style.display === 'none')) return '' + if (computedStyle && (computedStyle.visibility === 'hidden' || computedStyle.display === 'none')) return '' if (element.classList.contains('no-csv')) return '' @@ -33,27 +36,89 @@ const _getElementText = (element: HTMLElement): string => { return normalizeString(innerText) } -type DataRow = Array +const _getCellSpans = (props: { + cell: Element + columnCount: number + rowCount: number +}): { colSpan: number; rowSpan: number } => { + const { cell, columnCount, rowCount } = props + const style = getComputedStyle(cell) + + const parseValue = (value: string, type: 'col' | 'row'): number => { + if (value === 'auto') return 1 + if (value.startsWith('span')) return parseInt(value.split(' ')[1], 10) + if (value.includes('/')) { + const [start, end] = value.split('/').map((v) => v.trim()) + if (start === '1' && end === '-1') { + // Spans the entire row or column + return type === 'col' ? columnCount : rowCount + } + return Math.abs(parseInt(end, 10) - parseInt(start, 10)) + } + return parseInt(value, 10) + } + + const colSpan = parseValue(style.gridColumn, 'col') + const rowSpan = parseValue(style.gridRow, 'row') + + return { colSpan, rowSpan } +} + +type DataRow = Array type TableData = Array export const getDataGridData = (grid: HTMLDivElement): TableData => { if (!grid) { return [] } - const data: TableData = [] - const cells = grid.querySelectorAll('.data-cell') - let currentRow: DataRow = [] + const gridStyle = getComputedStyle(grid) + const columnCount = gridStyle.getPropertyValue('grid-template-columns').split(' ').length + const rowCount = gridStyle.getPropertyValue('grid-template-rows').split(' ').length - cells.forEach((cell) => { - const isLastCol = cell.classList.contains('lastCol') + const data: TableData = Array.from({ length: rowCount }, () => new Array(columnCount).fill(null)) - const cellText = _getElementText(cell as HTMLElement) + const cells = Array.from(grid.children) - currentRow.push(cellText) + const findNextAvailablePosition = (): { row: number; col: number } | null => { + for (let row = 0; row < data.length; row += 1) { + for (let col = 0; col < data[row].length; col += 1) { + if (Objects.isNil(data[row][col])) { + return { col, row } + } + } + } + return null + } - if (isLastCol) { - data.push([...currentRow]) - currentRow = [] + cells.forEach((cell) => { + const nextAvailablePosition = findNextAvailablePosition() + if (Objects.isNil(nextAvailablePosition)) return + const { col, row } = nextAvailablePosition + + const isNoticeMessage = cell.classList.contains('table-grid__notice-message-cell') + + let cellContent = _getElementText(cell as HTMLElement) + const spaceFreeContent = cellContent.replace(/\s/g, '') + cellContent = + Number.isNaN(Number.parseFloat(spaceFreeContent)) || Number.isNaN(Number(spaceFreeContent)) + ? cellContent + : spaceFreeContent + + const { rowSpan, colSpan } = _getCellSpans({ cell, columnCount, rowCount }) + + for (let r = row; r < row + rowSpan && r < rowCount; r += 1) { + for (let c = col; c < col + colSpan && c < columnCount; c += 1) { + // Place notice message only in the first cell of the row + if (isNoticeMessage) { + if (r === row && c === col) { + data[r][c] = cellContent + } else { + data[r][c] = '' + } + } else { + data[r][c] = cellContent + } + } } }) diff --git a/src/client/pages/OriginalDataPoint/components/NationalClasses/components/Title.tsx b/src/client/pages/OriginalDataPoint/components/NationalClasses/components/Title.tsx index e8974d6e69..6d508b6694 100644 --- a/src/client/pages/OriginalDataPoint/components/NationalClasses/components/Title.tsx +++ b/src/client/pages/OriginalDataPoint/components/NationalClasses/components/Title.tsx @@ -22,7 +22,7 @@ export const Title = (props: Props) => { return (
- +

{t(`nationalDataPoint.${cycleName !== '2020' ? 'nationalClassifications' : 'nationalClasses'}`)}

diff --git a/src/client/pages/Section/DataTable/Table/GridHeadCell/OdpHeaderCell/OdpHeaderCell.tsx b/src/client/pages/Section/DataTable/Table/GridHeadCell/OdpHeaderCell/OdpHeaderCell.tsx index d9b1a6118c..01ee074f61 100644 --- a/src/client/pages/Section/DataTable/Table/GridHeadCell/OdpHeaderCell/OdpHeaderCell.tsx +++ b/src/client/pages/Section/DataTable/Table/GridHeadCell/OdpHeaderCell/OdpHeaderCell.tsx @@ -8,10 +8,10 @@ import classNames from 'classnames' import { Routes } from 'meta/routes' import { TooltipId } from 'meta/tooltip' -import { useAssessment, useCycle } from 'client/store/assessment' import { useOdpReviewSummary } from 'client/store/ui/review/hooks' import { useCountryIso } from 'client/hooks' import { useIsPrintRoute } from 'client/hooks/useIsRoute' +import { useCycleRouteParams } from 'client/hooks/useRouteParams' import { DataCell } from 'client/components/DataGrid' import ReviewSummaryIndicator from 'client/components/ReviewSummaryIndicator' @@ -28,9 +28,8 @@ type Props = { const OdpHeaderCell: React.FC = (props) => { const { className, gridColumn, gridRow, lastCol, odpId, odpYear, sectionName } = props - const assessment = useAssessment() + const { assessmentName, cycleName } = useCycleRouteParams() const countryIso = useCountryIso() - const cycle = useCycle() const { print } = useIsPrintRoute() const { t } = useTranslation() @@ -53,21 +52,23 @@ const OdpHeaderCell: React.FC = (props) => { header lastCol={lastCol} > - - {odpYear} +
+ + {odpYear} + - +
) } diff --git a/src/client/pages/Section/DataTable/Table/RowData/Cell/Cell.scss b/src/client/pages/Section/DataTable/Table/RowData/Cell/Cell.scss index adbded01b0..5cf1a5c42f 100644 --- a/src/client/pages/Section/DataTable/Table/RowData/Cell/Cell.scss +++ b/src/client/pages/Section/DataTable/Table/RowData/Cell/Cell.scss @@ -22,9 +22,13 @@ display: flex; justify-content: end; - &.calculated { + &.readonly, + &.calculated-input { cursor: default; font-weight: 600; + } + + &.readonly { padding: $spacing-xxs; } diff --git a/src/client/pages/Section/DataTable/Table/RowData/Cell/hooks/useClassName.ts b/src/client/pages/Section/DataTable/Table/RowData/Cell/hooks/useClassName.ts index ba94f09be7..8641d3224b 100644 --- a/src/client/pages/Section/DataTable/Table/RowData/Cell/hooks/useClassName.ts +++ b/src/client/pages/Section/DataTable/Table/RowData/Cell/hooks/useClassName.ts @@ -1,3 +1,5 @@ +import { useMemo } from 'react' + import classNames from 'classnames' import { Col, Cols, ColType, Cycle, NodeValueValidation, Row } from 'meta/assessment' @@ -11,12 +13,25 @@ type Props = { export const useClassName = (props: Props): string => { const { cycle, col, row, validation } = props - const { colType } = col.props - let className = '' - if (Cols.isReadOnly({ cycle, col, row })) className = 'calculated' - if ([ColType.text, ColType.textarea, ColType.select, ColType.taxon].includes(colType)) className = 'left' - if (colType === ColType.placeholder) className = 'category header left' + return useMemo(() => { + const { colType } = col.props + + const isPlaceholder = colType === ColType.placeholder + const isTextInput = [ColType.text, ColType.textarea, ColType.select, ColType.taxon].includes(colType) + const isCalculated = Cols.isCalculated({ col, row }) + const isCalculatedInput = isCalculated && colType !== ColType.calculated + const isReadOnly = Cols.isReadOnly({ cycle, col, row }) && !isCalculatedInput - return classNames('table-grid__data-cell', className, { 'validation-error': !validation.valid }) + return classNames( + 'table-grid__data-cell', + { 'validation-error': !validation.valid }, + { + 'calculated-input': isCalculatedInput && !isTextInput, + 'category header left': isPlaceholder, + left: isTextInput, + readonly: isReadOnly, + } + ) + }, [col, cycle, row, validation]) } diff --git a/src/client/pages/Section/DataTable/Table/Table.tsx b/src/client/pages/Section/DataTable/Table/Table.tsx index b9e92cd7f2..6bd0ecc96b 100644 --- a/src/client/pages/Section/DataTable/Table/Table.tsx +++ b/src/client/pages/Section/DataTable/Table/Table.tsx @@ -10,8 +10,7 @@ import { useIsDataLocked } from 'client/store/ui/dataLock' import { useCanEdit } from 'client/store/user' import { useCanViewReview } from 'client/store/user/hooks' import { useIsPrintRoute } from 'client/hooks/useIsRoute' -import ButtonTableExport from 'client/components/ButtonTableExport' -import { DataGrid } from 'client/components/DataGrid' +import { ButtonGridExport, DataGrid } from 'client/components/DataGrid' import ButtonCopyValues from 'client/pages/Section/DataTable/Table/ButtonCopyValues' import ButtonTableClear from 'client/pages/Section/DataTable/Table/ButtonTableClear' import GridHeadCell from 'client/pages/Section/DataTable/Table/GridHeadCell' @@ -40,6 +39,7 @@ const Table: React.FC = (props) => { const { print } = useIsPrintRoute() const tableRef = useRef(null) + const gridRef = useRef(null) const { headers, noticeMessages, rowsData, rowsHeader, table, withReview } = useParsedTable({ assessmentName, @@ -63,12 +63,17 @@ const Table: React.FC = (props) => {
- {!print && } + {!print && } {canClearData && }
- + {rowsHeader.map((row, rowIndex) => ( {row.cols.map((col, colIndex) => ( diff --git a/src/test/migrations/steps/20241114204645-step-fix-datatable-grid-layouts-and-taxon-codes.ts b/src/test/migrations/steps/20241114204645-step-fix-datatable-grid-layouts-and-taxon-codes.ts index 6e2622a24a..8d2c796f92 100644 --- a/src/test/migrations/steps/20241114204645-step-fix-datatable-grid-layouts-and-taxon-codes.ts +++ b/src/test/migrations/steps/20241114204645-step-fix-datatable-grid-layouts-and-taxon-codes.ts @@ -700,7 +700,10 @@ const _fixFraTaxonCodes = async (client: BaseProtocol) => { } const _fixGrowingStockComposition2025HeaderRows = async (client: BaseProtocol) => { - const assessment = await AssessmentController.getOne({ assessmentName: AssessmentNames.fra }, client) + const { assessment, cycle } = await AssessmentController.getOneWithCycle( + { assessmentName: AssessmentNames.fra, cycleName: '2025' }, + client + ) const schemaAssessment = Schemas.getName(assessment) @@ -744,7 +747,19 @@ const _fixGrowingStockComposition2025HeaderRows = async (client: BaseProtocol) = join ${schemaAssessment}.table t on r.table_id = t.id where r.props ->> 'index' = 'header_1' and t.props ->> 'name' = 'growingStockComposition2025' - );` + ); + + update ${schemaAssessment}.col c + set props = jsonb_set(props, '{style,${cycle.uuid},rowSpan}', '1', false) + where c.props ->> 'colType' = 'header' + and c.row_id in ( + select r.id from ${schemaAssessment}.row r + left join ${schemaAssessment}."table" t on t.id = r.table_id + where r.props ->> 'type' = 'header' + and r.props ->> 'index' = 'header_0' + and t.props ->> 'name' = 'growingStockComposition2025' + ); + ` ) }