Skip to content

Commit 350149c

Browse files
authored
fix(AnalyticalTable): calculate subcomponents correctly in tree table (#5444)
Fixes #5407
1 parent d717db8 commit 350149c

File tree

3 files changed

+269
-12
lines changed

3 files changed

+269
-12
lines changed

packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx

Lines changed: 259 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,7 @@ const generateMoreData = (count) => {
2828
}));
2929
};
3030

31-
interface PropTypes {
32-
onRowSelect: (
33-
e?: CustomEvent<{
34-
allRowsSelected: boolean;
35-
row?: Record<string, unknown>;
36-
isSelected?: boolean;
37-
selectedFlatRows: Record<string, unknown>[];
38-
selectedRowIds: Record<string | number, boolean>;
39-
}>
40-
) => void;
41-
}
31+
type PropTypes = AnalyticalTablePropTypes['onRowSelect'];
4232

4333
const columns = [
4434
{
@@ -2716,9 +2706,267 @@ describe('AnalyticalTable', () => {
27162706
cy.get('[data-empty-row]').should('exist').and('have.length', 5).and('be.visible');
27172707
});
27182708

2709+
it('TreeTable + SubComps + lazy-load', () => {
2710+
const initialData = [
2711+
{
2712+
displayId: '1337',
2713+
name: 'root1',
2714+
nodeId: 'root1',
2715+
parentId: null
2716+
},
2717+
{
2718+
displayId: '1337',
2719+
name: 'root2',
2720+
nodeId: 'root2',
2721+
parentId: null
2722+
}
2723+
];
2724+
2725+
const columns = [
2726+
{
2727+
Header: 'Test',
2728+
accessor: 'name'
2729+
},
2730+
{
2731+
accessor: 'displayId'
2732+
}
2733+
];
2734+
2735+
/**
2736+
* This example will render a tree table using AnalyticalTable.
2737+
* the children nodes will be lazy loaded from server when expanding the parent node.
2738+
*a "Load more" button is rendered if the parent node's children are not completely loaded.
2739+
*/
2740+
const TestComp = () => {
2741+
// flattend data. will be transformed before passed to the tree table
2742+
const [raw, setRaw] = useState(initialData);
2743+
const rowById = useRef({});
2744+
const names = useRef(mockNames);
2745+
2746+
// simulate getting children from server. randomly generate a child node.
2747+
const fetchChildren = (nodeId) => {
2748+
return Promise.resolve({
2749+
value: [
2750+
{
2751+
displayId: `1337`,
2752+
name: `${nodeId}-${names.current[0]}`,
2753+
nodeId: `${nodeId}-${names.current[0]}`,
2754+
parentId: nodeId
2755+
}
2756+
]
2757+
});
2758+
};
2759+
2760+
const getChildren = useCallback(
2761+
(nodeId) => {
2762+
return fetchChildren(nodeId).then((result) => {
2763+
names.current.shift();
2764+
setRaw([...raw, ...result.value]);
2765+
});
2766+
},
2767+
[raw]
2768+
);
2769+
2770+
const handleRowExpandChange = useCallback(
2771+
(event) => {
2772+
const row = event.detail.row;
2773+
if (!row.isExpanded && row.canExpand && !row.original.subRows?.length) {
2774+
void getChildren(row.original.nodeId, row.original.subRows?.length || 0);
2775+
}
2776+
},
2777+
[getChildren]
2778+
);
2779+
2780+
// render "Load more" button
2781+
// the "Load more" button will be rendered as the row's subcomponent if the row is the last child of its parent node
2782+
const renderLoadMore = (row) => {
2783+
const parentId = row.original.parentId;
2784+
2785+
// root node
2786+
const parentNode = rowById.current[parentId];
2787+
if (!parentNode) {
2788+
return null;
2789+
}
2790+
2791+
// current node is not the last node of the parent's children: do not render the Load more button
2792+
const currentChildrenCount = parentNode.subRows?.length || 0;
2793+
const currentRowIndex = parentNode.subRows?.findIndex((subRow) => subRow.nodeId === row.original.nodeId);
2794+
if (currentRowIndex !== currentChildrenCount - 1) {
2795+
return null;
2796+
}
2797+
2798+
const arrowWidth = 35;
2799+
2800+
return (
2801+
<div
2802+
style={{
2803+
paddingBottom: '0.25rem',
2804+
paddingInlineStart: `calc(var(--_ui5wcr-AnalyticalTableTreePaddingLevel${row.depth}) + ${arrowWidth}px)`
2805+
}}
2806+
>
2807+
<Button
2808+
design="Transparent"
2809+
onClick={() => {
2810+
getChildren && getChildren(parentId, currentChildrenCount);
2811+
}}
2812+
>
2813+
Load more for {parentNode.name}
2814+
</Button>
2815+
</div>
2816+
);
2817+
};
2818+
2819+
const customTableHook = (hooks) => {
2820+
hooks.prepareRow.push((row) => {
2821+
row.canExpand = true;
2822+
});
2823+
};
2824+
2825+
// transform data to the pattern which is accepted by the tree table
2826+
// NOTES: this algorithm is less likely related to the bug, because in our reality project there is a different algorithm to generate the tree table and the bug still occurs.
2827+
const data = useMemo(() => {
2828+
raw.forEach((item) => {
2829+
const newItem = { ...item };
2830+
rowById.current[newItem.nodeId] = {
2831+
...(rowById[newItem.node] || {}),
2832+
...newItem
2833+
};
2834+
if (!newItem.parentId) {
2835+
rowById.current[newItem.nodeId] = {
2836+
...newItem,
2837+
...(rowById.current[newItem.nodeId] || {})
2838+
};
2839+
} else {
2840+
if (!rowById.current[newItem.parentId]) {
2841+
rowById.current[newItem.parentId] = {
2842+
nodeId: newItem.parentId,
2843+
subRows: []
2844+
};
2845+
} else if (!rowById.current[newItem.parentId].subRows) {
2846+
rowById.current[newItem.parentId].subRows = [];
2847+
}
2848+
rowById.current[newItem.parentId].subRows.push(rowById.current[newItem.nodeId]);
2849+
}
2850+
});
2851+
2852+
return Object.values(rowById.current).filter((row) => !row.parentId);
2853+
}, [raw]);
2854+
2855+
return (
2856+
<AnalyticalTable
2857+
columns={columns}
2858+
data={data}
2859+
isTreeTable
2860+
onRowExpandChange={handleRowExpandChange}
2861+
reactTableOptions={{
2862+
autoResetExpanded: false
2863+
}}
2864+
renderRowSubComponent={renderLoadMore}
2865+
subComponentsBehavior={'IncludeHeight'}
2866+
tableHooks={[customTableHook]}
2867+
minRows={1}
2868+
/>
2869+
);
2870+
};
2871+
2872+
cy.mount(<TestComp />);
2873+
2874+
cy.findByText('root1').siblings().click();
2875+
cy.findByText('Load more for root1').should('have.length', 1).click();
2876+
cy.findByText('Load more for root1').should('have.length', 1).click();
2877+
2878+
cy.findByText('root1-John').siblings().click();
2879+
cy.findByText('Load more for root1-John').should('have.length', 1).click();
2880+
2881+
cy.get('[aria-rowindex="6"]').should('have.css', 'transform', 'matrix(1, 0, 0, 1, 0, 260)');
2882+
});
2883+
27192884
cypressPassThroughTestsFactory(AnalyticalTable, { data, columns });
27202885
});
27212886

2887+
const mockNames = [
2888+
'John',
2889+
'Jane',
2890+
'Bob',
2891+
'Alice',
2892+
'Charlie',
2893+
'David',
2894+
'Eva',
2895+
'Frank',
2896+
'Grace',
2897+
'Henry',
2898+
'Isabel',
2899+
'Jack',
2900+
'Kate',
2901+
'Liam',
2902+
'Mia',
2903+
'Noah',
2904+
'Olivia',
2905+
'Parker',
2906+
'Quinn',
2907+
'Ryan',
2908+
'Sophia',
2909+
'Thomas',
2910+
'Uma',
2911+
'Vincent',
2912+
'Willow',
2913+
'Xavier',
2914+
'Yara',
2915+
'Zane',
2916+
'Ava',
2917+
'Benjamin',
2918+
'Cora',
2919+
'Dylan',
2920+
'Emily',
2921+
'Finn',
2922+
'Gabriella',
2923+
'Hudson',
2924+
'Isla',
2925+
'Julian',
2926+
'Katherine',
2927+
'Leo',
2928+
'Mila',
2929+
'Nathan',
2930+
'Oliver',
2931+
'Penelope',
2932+
'Quentin',
2933+
'Rose',
2934+
'Samuel',
2935+
'Tessa',
2936+
'Ulysses',
2937+
'Victoria',
2938+
'Wesley',
2939+
'Xander',
2940+
'Yasmine',
2941+
'Zachary',
2942+
'Abigail',
2943+
'Brady',
2944+
'Chloe',
2945+
'Daniel',
2946+
'Eleanor',
2947+
'Felix',
2948+
'Giselle',
2949+
'Hayden',
2950+
'Isabella',
2951+
'Jasper',
2952+
'Kylie',
2953+
'Landon',
2954+
'Maddison',
2955+
'Natalie',
2956+
'Oscar',
2957+
'Paige',
2958+
'Quincy',
2959+
'Riley',
2960+
'Savannah',
2961+
'Theodore',
2962+
'Ursula',
2963+
'Violet',
2964+
'Wyatt',
2965+
'Ximena',
2966+
'Yannick',
2967+
'Zara'
2968+
];
2969+
27222970
const columnsWithPopIn = [
27232971
{
27242972
Header: 'Name',

packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBody.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,18 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
210210
updatedHeight += subComponentsHeight?.[virtualRow.index]?.subComponentHeight ?? 0;
211211
}
212212

213+
const measureRef =
214+
isTreeTable && renderRowSubComponent && (row.isExpanded || alwaysShowSubComponent)
215+
? (node) => {
216+
rowVirtualizer.measureElement(node);
217+
}
218+
: rowVirtualizer.measureElement;
219+
213220
return (
214221
// eslint-disable-next-line react/jsx-key
215222
<div
216223
{...rowProps}
217-
ref={rowVirtualizer.measureElement}
224+
ref={measureRef}
218225
style={{
219226
...(rowProps.style ?? {}),
220227
transform: `translateY(${virtualRow.start}px)`,

packages/main/src/components/AnalyticalTable/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,8 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
561561
* Defines the subcomponent that should be displayed below each row.
562562
*
563563
* __Note:__ When rendering active elements inside the subcomponent, make sure to add the `data-subcomponent-active-element' attribute, otherwise focus behavior won't be consistent.
564+
*
565+
* __Note:__ Subcomponents can affect performance, especially when used in a tree table (`isTreeTable={true}`). If you face performance issues, please try memoizing your subcomponent.
564566
*/
565567
renderRowSubComponent?: (row?: any) => ReactNode;
566568
/**

0 commit comments

Comments
 (0)