Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<string, RowType> = {};
Expand All @@ -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);
Expand Down Expand Up @@ -198,18 +211,19 @@ function useInstance(instance: TableInstance) {
const values: Record<string, any> = {};

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<string, any>)[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]);
Expand Down
11 changes: 2 additions & 9 deletions packages/main/src/components/AnalyticalTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AnalyticalTableColumnDefinition[]>(
() => [
{ 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 (
<span data-testid={`mw-${props.cell.row.id}`} data-agg-status={String(props.cell.row.values.status)}>
{isWorkflowFinished ? 'all finished' : props.value}
</span>
);
},
// `Aggregated` reads `row.values.status` — the aggregated grouped value under test.
Aggregated: (props: any) => {
const isWorkflowFinished = props.row.values.status === 'finished';
return (
<span data-testid={`mw-${props.row.id}`} data-agg-status={String(props.row.values.status)}>
{isWorkflowFinished ? 'all finished' : props.value}
</span>
);
},
},
],
[],
);

const reactTableOptions = useMemo(
() => ({
autoResetGroupBy: false,
autoResetExpanded: false,
initialState: { groupBy: ['targetLanguage', 'status'] },
}),
[],
);

return (
<AnalyticalTable
data={data}
columns={columns}
groupable
sortable={false}
visibleRows={10}
reactTableOptions={reactTableOptions}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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<typeof GroupingAggregationHarness>(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');
});
});
Loading