diff --git a/packages/main/src/components/AnalyticalTable/react-table/plugin-hooks/useGroupBy.ts b/packages/main/src/components/AnalyticalTable/hooks/useGroupBy.ts similarity index 92% rename from packages/main/src/components/AnalyticalTable/react-table/plugin-hooks/useGroupBy.ts rename to packages/main/src/components/AnalyticalTable/hooks/useGroupBy.ts index 49cfd7343af..f6b86fd02bc 100644 --- a/packages/main/src/components/AnalyticalTable/react-table/plugin-hooks/useGroupBy.ts +++ b/packages/main/src/components/AnalyticalTable/hooks/useGroupBy.ts @@ -1,8 +1,14 @@ import { useCallback, useMemo } from 'react'; -import type { ReactTableHooks, TableInstance, ColumnType, RowType, PluginHook } from '../../types/index.js'; -import * as aggregations from '../aggregations.js'; -import { actions, makePropGetter, ensurePluginOrder, useMountedLayoutEffect, useGetLatest } from '../publicUtils.js'; -import { getFirstDefined, flattenBy } from '../utils.js'; +import * as aggregations from '../react-table/aggregations.js'; +import { + actions, + makePropGetter, + ensurePluginOrder, + useMountedLayoutEffect, + useGetLatest, +} from '../react-table/publicUtils.js'; +import { getFirstDefined, flattenBy } from '../react-table/utils.js'; +import type { ReactTableHooks, TableInstance, ColumnType, RowType, PluginHook } from '../types/index.js'; const emptyArray: RowType[] = []; const emptyObject: Record = {}; @@ -12,6 +18,13 @@ actions.resetGroupBy = 'resetGroupBy'; actions.setGroupBy = 'setGroupBy'; actions.toggleGroupBy = 'toggleGroupBy'; +/** + * UI5WCR fork of react-table v7's useGroupBy hook. + * Original source: https://github.com/TanStack/table/blob/v7/src/plugin-hooks/useGroupBy.js + * + * This is a fork of react-table's `useGroupBy` with the following changes: + * - Aggregate grouped columns that define an `aggregate` above their own grouping level, instead of copying the first leaf value + */ export const useGroupBy: PluginHook = (hooks: ReactTableHooks) => { hooks.getGroupByToggleProps = [defaultGetGroupByToggleProps]; hooks.stateReducers.push(reducer); @@ -198,18 +211,19 @@ function useInstance(instance: TableInstance) { const values: Record = {}; allColumns.forEach((column: ColumnType) => { - // Don't aggregate columns that are in the groupBy - if (existingGroupBy.includes(column.id)) { - values[column.id] = groupedRows[0] ? groupedRows[0].values[column.id] : null; - return; - } + const groupedIndex = existingGroupBy.indexOf(column.id); - // Aggregate the values const aggregateFn = typeof column.aggregate === 'function' ? column.aggregate : userAggregations[column.aggregate] || (aggregations as Record)[column.aggregate]; + // UI5WCR: aggregate grouped columns above their own grouping level instead of copying the first leaf value + if (groupedIndex > -1 && (groupedIndex <= depth || !aggregateFn)) { + values[column.id] = groupedRows[0] ? groupedRows[0].values[column.id] : null; + return; + } + if (aggregateFn) { // Get the columnValues to aggregate const groupedValues = groupedRows.map((row) => row.values[column.id]); diff --git a/packages/main/src/components/AnalyticalTable/index.tsx b/packages/main/src/components/AnalyticalTable/index.tsx index 28b93ebcd04..f62d4632948 100644 --- a/packages/main/src/components/AnalyticalTable/index.tsx +++ b/packages/main/src/components/AnalyticalTable/index.tsx @@ -59,6 +59,7 @@ import { useColumnsDeps } from './hooks/useColumnsDeps.js'; import { useColumnDragAndDrop } from './hooks/useDragAndDrop.js'; import { useDynamicColumnWidths } from './hooks/useDynamicColumnWidths.js'; import { useFontsReady } from './hooks/useFontsReady.js'; +import { useGroupBy } from './hooks/useGroupBy.js'; import { useKeyboardNavigation } from './hooks/useKeyboardNavigation.js'; import { useNativeScrollbar } from './hooks/useNativeScrollbar.js'; import { usePopIn } from './hooks/usePopIn.js'; @@ -73,15 +74,7 @@ import { useStyling } from './hooks/useStyling.js'; import { useSyncScroll } from './hooks/useSyncScroll.js'; import { useToggleRowExpand } from './hooks/useToggleRowExpand.js'; import { useVisibleColumnsWidth } from './hooks/useVisibleColumnsWidth.js'; -import { - useColumnOrder, - useExpanded, - useFilters, - useGlobalFilter, - useGroupBy, - useSortBy, - useTable, -} from './react-table/index.js'; +import { useColumnOrder, useExpanded, useFilters, useGlobalFilter, useSortBy, useTable } from './react-table/index.js'; import { VerticalScrollbar } from './scrollbars/VerticalScrollbar.js'; import { VirtualTableBody } from './TableBody/VirtualTableBody.js'; import { VirtualTableBodyContainer } from './TableBody/VirtualTableBodyContainer.js'; diff --git a/packages/main/src/components/AnalyticalTable/react-table/index.ts b/packages/main/src/components/AnalyticalTable/react-table/index.ts index 377f46732c3..0bfb89e21c3 100644 --- a/packages/main/src/components/AnalyticalTable/react-table/index.ts +++ b/packages/main/src/components/AnalyticalTable/react-table/index.ts @@ -2,7 +2,6 @@ export { useTable } from './hooks/useTable.js'; export { useExpanded } from './plugin-hooks/useExpanded.js'; export { useFilters } from './plugin-hooks/useFilters.js'; export { useGlobalFilter } from './plugin-hooks/useGlobalFilter.js'; -export { useGroupBy, defaultGroupByFn } from './plugin-hooks/useGroupBy.js'; export { useSortBy, defaultOrderByFn } from './plugin-hooks/useSortBy.js'; export { useColumnOrder } from './plugin-hooks/useColumnOrder.js'; export { diff --git a/packages/main/src/components/AnalyticalTable/test/AnalyticalTableGrouping.gallery.tsx b/packages/main/src/components/AnalyticalTable/test/AnalyticalTableGrouping.gallery.tsx new file mode 100644 index 00000000000..9b1fce5ae65 --- /dev/null +++ b/packages/main/src/components/AnalyticalTable/test/AnalyticalTableGrouping.gallery.tsx @@ -0,0 +1,90 @@ +import { useMemo } from 'react'; +import type { AnalyticalTableColumnDefinition } from '../index.js'; +import { AnalyticalTable } from '../index.js'; + +interface Row { + targetLanguage: string; + fileName: string; + status: string; + missingWork: number; +} + +// "finished" pt-BR row comes first on purpose: exposes the first-leaf copy on the ancestor group row. +const data: Row[] = [ + { targetLanguage: 'pt-BR', fileName: 'text3', status: 'finished', missingWork: 3 }, + { targetLanguage: 'pt-BR', fileName: 'text1', status: 'in_progress', missingWork: 5 }, + { targetLanguage: 'pt-BR', fileName: 'text2', status: 'not_started', missingWork: 3 }, + { targetLanguage: 'pt-BR', fileName: 'text4', status: 'not_started', missingWork: 3 }, + { targetLanguage: 'en-US', fileName: 'text1', status: 'finished', missingWork: 3 }, + { targetLanguage: 'en-US', fileName: 'text2', status: 'finished', missingWork: 0 }, +]; + +// `status` is grouped below `targetLanguage`, so its aggregated value is surfaced via the +// Missing-work `Aggregated` renderer (`data-agg-status`) — the status column's own ancestor cell is a placeholder. +export const GroupingAggregationHarness = () => { + const columns = useMemo( + () => [ + { Header: 'Target Language', accessor: 'targetLanguage', width: 200 }, + { Header: 'File Name', accessor: 'fileName', width: 200 }, + { + Header: 'Status', + accessor: 'status', + width: 200, + aggregate: (_leafValues: string[], aggregatedValues: string[]) => { + const uniqueValues = new Set(aggregatedValues); + if (uniqueValues.has('not_started')) { + return 'not_started'; + } + if (uniqueValues.has('in_progress')) { + return 'in_progress'; + } + return 'finished'; + }, + }, + { + Header: 'Missing work', + accessor: 'missingWork', + width: 200, + aggregate: 'sum', + Cell: (props: any) => { + const isWorkflowFinished = props.cell.row.values.status === 'finished'; + return ( + + {isWorkflowFinished ? 'all finished' : props.value} + + ); + }, + // `Aggregated` reads `row.values.status` — the aggregated grouped value under test. + Aggregated: (props: any) => { + const isWorkflowFinished = props.row.values.status === 'finished'; + return ( + + {isWorkflowFinished ? 'all finished' : props.value} + + ); + }, + }, + ], + [], + ); + + const reactTableOptions = useMemo( + () => ({ + autoResetGroupBy: false, + autoResetExpanded: false, + initialState: { groupBy: ['targetLanguage', 'status'] }, + }), + [], + ); + + return ( + + ); +}; diff --git a/packages/main/src/components/AnalyticalTable/test/AnalyticalTableGrouping.spec.tsx b/packages/main/src/components/AnalyticalTable/test/AnalyticalTableGrouping.spec.tsx new file mode 100644 index 00000000000..0ba592830d7 --- /dev/null +++ b/packages/main/src/components/AnalyticalTable/test/AnalyticalTableGrouping.spec.tsx @@ -0,0 +1,24 @@ +import { expect, test } from '../../../../../../playwright/fixtures/gallery-fixtures.js'; +import type { GroupingAggregationHarness } from './AnalyticalTableGrouping.gallery.js'; + +const STORY = 'AnalyticalTableGrouping/GroupingAggregationHarness'; + +test.describe('AnalyticalTable', () => { + test('grouped column is aggregated on ancestor group rows', async ({ mount, page }) => { + await mount(STORY); + + const ptBr = page.getByTestId('mw-targetLanguage:pt-BR'); + const enUs = page.getByTestId('mw-targetLanguage:en-US'); + await expect(ptBr).toBeVisible(); + await expect(enUs).toBeVisible(); + + // pt-BR mixes finished/in_progress/not_started → aggregated status is 'not_started', not the first leaf's 'finished'. + await expect(ptBr).toHaveAttribute('data-agg-status', 'not_started'); + await expect(ptBr).not.toHaveText('all finished'); + await expect(ptBr).toHaveText('14'); + + // en-US is genuinely all finished. + await expect(enUs).toHaveAttribute('data-agg-status', 'finished'); + await expect(enUs).toHaveText('all finished'); + }); +});