From e6fce90474952d74561fbcd8096111549de6eb68 Mon Sep 17 00:00:00 2001 From: radubrehar Date: Fri, 1 Nov 2024 16:21:57 +0200 Subject: [PATCH] fix #260 and release version patch --- ...remove-children-of-collapsed-node.page.tsx | 107 ++++++++++++ .../remove-children-of-collapsed-node.spec.ts | 28 ++++ .../treegrid/test-isNodeExpanded.page.tsx | 140 ++++++++++++++++ .../treegrid/test-isNodeExpanded.spec.ts | 27 ++++ .../tests/testUtils/TableTestingModel.ts | 7 + source/src/components/DataSource/TreeApi.ts | 4 +- .../components/DataSource/getDataSourceApi.ts | 12 +- .../components/DataSource/state/reducer.ts | 17 +- source/src/components/DataSource/types.ts | 1 + .../components/icons/ExpandCollapseIcon.tsx | 5 +- .../reference/datasource-props/index.page.md | 55 +++++++ .../tree-isNodeReadOnly-example.page.tsx | 152 ++++++++++++++++++ .../docs/reference/tree-api/index.page.md | 16 +- www/content/docs/releases/index.page.md | 4 + 14 files changed, 557 insertions(+), 18 deletions(-) create mode 100644 examples/src/pages/tests/table/treegrid/remove-children-of-collapsed-node.page.tsx create mode 100644 examples/src/pages/tests/table/treegrid/remove-children-of-collapsed-node.spec.ts create mode 100644 examples/src/pages/tests/table/treegrid/test-isNodeExpanded.page.tsx create mode 100644 examples/src/pages/tests/table/treegrid/test-isNodeExpanded.spec.ts create mode 100644 www/content/docs/reference/datasource-props/tree-isNodeReadOnly-example.page.tsx diff --git a/examples/src/pages/tests/table/treegrid/remove-children-of-collapsed-node.page.tsx b/examples/src/pages/tests/table/treegrid/remove-children-of-collapsed-node.page.tsx new file mode 100644 index 00000000..453f4609 --- /dev/null +++ b/examples/src/pages/tests/table/treegrid/remove-children-of-collapsed-node.page.tsx @@ -0,0 +1,107 @@ +import * as React from 'react'; + +import { TreeDataSource, TreeGrid } from '@infinite-table/infinite-react'; +import { + DataSourceApi, + type InfiniteTableColumn, +} from '@infinite-table/infinite-react'; + +export type FileSystemNode = { + name: string; + type: 'file' | 'folder'; + children?: FileSystemNode[] | null; + sizeKB?: number; + id: string; + collapsed?: boolean; +}; + +const nodes: FileSystemNode[] = [ + { + name: 'Documents', + type: 'folder', + id: '1', + children: [ + { + name: 'report.doc', + type: 'file', + sizeKB: 100, + id: '2', + }, + { + type: 'folder', + name: 'pictures', + id: '3', + collapsed: true, + children: [ + { + name: 'mountain.jpg', + type: 'file', + sizeKB: 302, + id: '5', + }, + ], + }, + + { + type: 'file', + name: 'last.txt', + id: '7', + }, + ], + }, +]; + +const columns: Record> = { + name: { + field: 'name', + renderTreeIcon: true, + + renderValue: ({ value, data }) => { + return ( + <> + {value} - {data!.id} + + ); + }, + }, + type: { field: 'type' }, + sizeKB: { field: 'sizeKB' }, +}; +export default function App() { + const [dataSourceApi, setDataSourceApi] = + React.useState | null>(null); + + const removeRowsByPrimaryKey = async () => { + if (!dataSourceApi) { + return; + } + + dataSourceApi.removeDataArray([{ id: '3' }, { id: '7' }]); + }; + + return ( + <> + + + onReady={setDataSourceApi} + data={nodes} + primaryKey="id" + nodesKey="children" + > + + domProps={{ + style: { + margin: '5px', + height: 900, + border: '1px solid gray', + position: 'relative', + }, + }} + columns={columns} + /> + + + ); +} diff --git a/examples/src/pages/tests/table/treegrid/remove-children-of-collapsed-node.spec.ts b/examples/src/pages/tests/table/treegrid/remove-children-of-collapsed-node.spec.ts new file mode 100644 index 00000000..f72f8fce --- /dev/null +++ b/examples/src/pages/tests/table/treegrid/remove-children-of-collapsed-node.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@testing'; + +export default test.describe.parallel('TreeDataSourceApi', () => { + test('removeDataArray - with ids of collapsed children', async ({ + page, + rowModel, + treeModel, + }) => { + await page.waitForInfinite(); + + let rowCount = await rowModel.getRenderedRowCount(); + + expect(rowCount).toBe(5); + + await treeModel.toggleParentNode(0); + + rowCount = await rowModel.getRenderedRowCount(); + + expect(rowCount).toBe(1); + + await page.click('button:text("Click to remove children")'); + await treeModel.toggleParentNode(0); + + rowCount = await rowModel.getRenderedRowCount(); + + expect(rowCount).toBe(2); + }); +}); diff --git a/examples/src/pages/tests/table/treegrid/test-isNodeExpanded.page.tsx b/examples/src/pages/tests/table/treegrid/test-isNodeExpanded.page.tsx new file mode 100644 index 00000000..509ff165 --- /dev/null +++ b/examples/src/pages/tests/table/treegrid/test-isNodeExpanded.page.tsx @@ -0,0 +1,140 @@ +import * as React from 'react'; + +import { + DataSourcePropIsNodeExpanded, + InfiniteTableColumn, + TreeDataSource, + TreeExpandState, + TreeGrid, +} from '@infinite-table/infinite-react'; +import { useState } from 'react'; + +export type FileSystemNode = { + name: string; + type: 'file' | 'folder'; + children?: FileSystemNode[] | null; + sizeKB?: number; + id: string; + collapsed?: boolean; +}; + +export const nodes: FileSystemNode[] = [ + { + name: 'Documents', + type: 'folder', + id: '1', + children: [ + { + name: 'report.doc', + type: 'file', + sizeKB: 100, + id: '2', + }, + { + type: 'folder', + name: 'pictures', + id: '3', + children: [ + { + name: 'mountain.jpg', + type: 'file', + sizeKB: 302, + id: '5', + }, + ], + }, + { + type: 'folder', + name: 'diverse', + id: '4', + children: [ + { + name: 'beach.jpg', + type: 'file', + sizeKB: 2024, + id: '6', + }, + ], + }, + { + type: 'file', + name: 'last.txt', + id: '7', + }, + ], + }, +]; + +const columns: Record> = { + name: { + field: 'name', + renderTreeIcon: true, + + renderValue: ({ value, data }) => { + return ( + <> + {value} - {data!.id} + + ); + }, + }, + type: { field: 'type' }, + sizeKB: { field: 'sizeKB' }, +}; + +export default function DataTestPage() { + const [treeExpandState] = useState(() => { + return new TreeExpandState({ + defaultExpanded: false, + expandedPaths: [['1', '4', '5'], ['1']], + }); + }); + const [key, setKey] = useState(0); + const isNodeExpanded = React.useCallback< + DataSourcePropIsNodeExpanded + >( + (rowInfo) => { + return rowInfo.data.id === '3' + ? false + : treeExpandState.isNodeExpanded(rowInfo.nodePath); + }, + [key], + ); + + return ( + + + + data={nodes} + primaryKey="id" + nodesKey="children" + treeExpandState={treeExpandState} + onTreeExpandStateChange={(treeExpandStateValue) => { + treeExpandState.update(treeExpandStateValue); + setKey((k) => k + 1); + }} + isNodeExpanded={isNodeExpanded} + > + + wrapRowsHorizontally + domProps={{ + style: { + margin: '5px', + height: 900, + border: '1px solid gray', + position: 'relative', + }, + }} + columns={columns} + /> + + + ); +} diff --git a/examples/src/pages/tests/table/treegrid/test-isNodeExpanded.spec.ts b/examples/src/pages/tests/table/treegrid/test-isNodeExpanded.spec.ts new file mode 100644 index 00000000..596aea43 --- /dev/null +++ b/examples/src/pages/tests/table/treegrid/test-isNodeExpanded.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@testing'; + +export default test.describe('isNodeExpanded', () => { + test('works as expected', async ({ page, rowModel, tableModel }) => { + await page.waitForInfinite(); + + const cell = tableModel.withCell({ + rowIndex: 2, + colIndex: 0, + }); + + const collapsedNode = tableModel.withCell({ + rowIndex: 3, + colIndex: 0, + }); + + expect(await rowModel.getRenderedRowCount()).toBe(5); + expect(await cell.isTreeIconExpanded()).toBe(false); + expect(await collapsedNode.isTreeIconExpanded()).toBe(false); + + await page.click('button:text("expand all")'); + + expect(await rowModel.getRenderedRowCount()).toBe(6); + expect(await cell.isTreeIconExpanded()).toBe(false); + expect(await collapsedNode.isTreeIconExpanded()).toBe(true); + }); +}); diff --git a/examples/src/pages/tests/testUtils/TableTestingModel.ts b/examples/src/pages/tests/testUtils/TableTestingModel.ts index 45c9da62..d9d9a90b 100644 --- a/examples/src/pages/tests/testUtils/TableTestingModel.ts +++ b/examples/src/pages/tests/testUtils/TableTestingModel.ts @@ -138,6 +138,13 @@ export class TableTestingModel { ); }, + isTreeIconExpanded: async () => { + const cellLocator = this.rowModel.getCellLocator(cellLocation); + + const icon = cellLocator.locator('[data-name="expand-collapse-icon"]'); + return (await icon.getAttribute('data-state')) === 'expanded'; + }, + getLocator: () => { return this.rowModel.getCellLocator(cellLocation); }, diff --git a/source/src/components/DataSource/TreeApi.ts b/source/src/components/DataSource/TreeApi.ts index d8474529..80fcf1bb 100644 --- a/source/src/components/DataSource/TreeApi.ts +++ b/source/src/components/DataSource/TreeApi.ts @@ -159,10 +159,10 @@ export class TreeApiImpl implements TreeApi { return false; } if (isNodeCollapsed) { - return !isNodeExpanded!(rowInfo); + return !isNodeExpanded!(rowInfo, treeExpandState); } if (isNodeExpanded) { - return isNodeExpanded!(rowInfo); + return isNodeExpanded!(rowInfo, treeExpandState); } } diff --git a/source/src/components/DataSource/getDataSourceApi.ts b/source/src/components/DataSource/getDataSourceApi.ts index 3b693878..a0433775 100644 --- a/source/src/components/DataSource/getDataSourceApi.ts +++ b/source/src/components/DataSource/getDataSourceApi.ts @@ -178,18 +178,18 @@ class DataSourceApiImpl implements DataSourceApi { case 'delete': if (operation.primaryKeys) { operation.primaryKeys.forEach((key) => { - const rowInfo = this.getRowInfoByPrimaryKey(key); - if (rowInfo && !rowInfo.isGroupRow) { - cache.delete(key, rowInfo.data, operation.metadata); + const originalData = this.getDataByPrimaryKey(key); + if (originalData) { + cache.delete(key, originalData, operation.metadata); } }); } else if (operation.nodePaths) { operation.nodePaths.forEach((nodePath) => { - const rowInfo = this.getRowInfoByNodePath(nodePath); - if (rowInfo && !rowInfo.isGroupRow) { + const originalData = this.getDataByNodePath(nodePath); + if (originalData) { cache.deleteNodePath( nodePath, - rowInfo.data, + originalData, operation.metadata, ); } diff --git a/source/src/components/DataSource/state/reducer.ts b/source/src/components/DataSource/state/reducer.ts index 02804eee..82e60008 100644 --- a/source/src/components/DataSource/state/reducer.ts +++ b/source/src/components/DataSource/state/reducer.ts @@ -528,17 +528,22 @@ export function concludeReducer(params: { let isNodeExpanded: | ((rowInfo: InfiniteTable_Tree_RowInfoParentNode) => boolean) - | undefined = state.isNodeExpanded; + | undefined; + + if (state.isNodeExpanded) { + isNodeExpanded = (rowInfo) => + state.isNodeExpanded!(rowInfo, treeExpandState!); + } if (state.isNodeCollapsed) { - isNodeExpanded = (rowInfo) => !state.isNodeExpanded!(rowInfo); + isNodeExpanded = (rowInfo) => + !state.isNodeExpanded!(rowInfo, treeExpandState!); } if (!isNodeExpanded) { - const defaultIsRowExpanded = (rowInfo: InfiniteTableRowInfo) => { - if (!rowInfo.isTreeNode || !rowInfo.isParentNode) { - return false; - } + const defaultIsRowExpanded = ( + rowInfo: InfiniteTable_Tree_RowInfoParentNode, + ) => { return treeExpandState!.isNodeExpanded(rowInfo.nodePath); }; diff --git a/source/src/components/DataSource/types.ts b/source/src/components/DataSource/types.ts index dc5d60cb..72835e5b 100644 --- a/source/src/components/DataSource/types.ts +++ b/source/src/components/DataSource/types.ts @@ -488,6 +488,7 @@ export type DataSourcePropIsNodeSelectable = ( export type DataSourcePropIsNodeExpanded = ( rowInfo: InfiniteTable_Tree_RowInfoParentNode, + treeExpandState: TreeExpandState, ) => boolean; // export type DataSourcePropIsCellSelected = ( // TODO implement this diff --git a/source/src/components/InfiniteTable/components/icons/ExpandCollapseIcon.tsx b/source/src/components/InfiniteTable/components/icons/ExpandCollapseIcon.tsx index 73354358..bc36bb5d 100644 --- a/source/src/components/InfiniteTable/components/icons/ExpandCollapseIcon.tsx +++ b/source/src/components/InfiniteTable/components/icons/ExpandCollapseIcon.tsx @@ -41,9 +41,12 @@ export function ExpandCollapseIcon(props: ExpandCollapseIconProps) { setExpanded(props.expanded); } }, [props.expanded]); + + const currentState = expanded ? 'expanded' : 'collapsed'; return ( and ca + + +> Decides if the current (non-leaf) node is expanded. + +The inverse prop, is also available. Only one of these props can be specified. + + + +If this prop is specified, is ignored. + + + + + + + +> Decides if the current (non-leaf) node is collapsed. See related prop. + +The inverse prop, is also available. Only one of these props can be specified. + + + +If this prop is specified, is ignored. + + + + + + + +> Decides if the current (non-leaf) node can be expanded or collapsed and if the tree icon is disabled. + +By default, parent nodes with `children: []` are read-only, meaning they won't respond to expand/collapse clicks. + +However, if you specify a custom `isNodeReadOnly` function, you can change this behavior. + + + +When a node is read-only, the and methods need the `options.force` flag to be set to `true` in order to override the read-only restriction. + +However, and will work regardless of the `isNodeReadOnly` setting. + +For full control over the expand/collapse state of read-only nodes, you can use the / props. + + + + + +```ts file="tree-isNodeReadOnly-example.page.tsx" + +``` + + + + > Called when a node is collapsed. See related and props. diff --git a/www/content/docs/reference/datasource-props/tree-isNodeReadOnly-example.page.tsx b/www/content/docs/reference/datasource-props/tree-isNodeReadOnly-example.page.tsx new file mode 100644 index 00000000..b590e678 --- /dev/null +++ b/www/content/docs/reference/datasource-props/tree-isNodeReadOnly-example.page.tsx @@ -0,0 +1,152 @@ +import { + InfiniteTableColumn, + TreeDataSource, + TreeExpandStateValue, + TreeGrid, +} from '@infinite-table/infinite-react'; +import { useState } from 'react'; + +type FileSystemNode = { + id: string; + name: string; + type: 'folder' | 'file'; + extension?: string; + mimeType?: string; + sizeInKB: number; + children?: FileSystemNode[]; +}; + +const columns: Record> = { + name: { field: 'name', header: 'Name', renderTreeIcon: true }, + type: { field: 'type', header: 'Type' }, + extension: { field: 'extension', header: 'Extension' }, + mimeType: { field: 'mimeType', header: 'Mime Type' }, + size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, +}; + +const defaultTreeExpandState: TreeExpandStateValue = { + defaultExpanded: true, + collapsedPaths: [['1', '10']], +}; + +const returnFalse = () => false; +export default function App() { + const [allowEmptyNodesExpand, setAllowEmptyNodesExpand] = useState(false); + + return ( + <> + +
+
+            Empty parent nodes {allowEmptyNodesExpand ? 'can' : 'cannot'} be
+            expanded.
+          
+ +
+ + +
+ + ); +} + +const dataSource = () => { + const nodes: FileSystemNode[] = [ + { + id: '1', + name: 'Documents', + sizeInKB: 1200, + type: 'folder', + children: [ + { + id: '10', + name: 'Private', + sizeInKB: 100, + type: 'folder', + children: [ + { + id: '100', + name: 'Report.docx', + sizeInKB: 210, + type: 'file', + extension: 'docx', + mimeType: 'application/msword', + }, + { + id: '101', + name: 'Vacation.docx', + sizeInKB: 120, + type: 'file', + extension: 'docx', + mimeType: 'application/msword', + }, + { + id: '102', + name: 'CV.pdf', + sizeInKB: 108, + type: 'file', + extension: 'pdf', + mimeType: 'application/pdf', + }, + ], + }, + ], + }, + { + id: '2', + name: 'Desktop - empty', + sizeInKB: 1000, + type: 'folder', + children: [], + }, + { + id: '3', + name: 'Media', + sizeInKB: 1000, + type: 'folder', + children: [ + { + id: '30', + name: 'Music - empty', + sizeInKB: 0, + type: 'folder', + children: [], + }, + { + id: '31', + name: 'Videos', + sizeInKB: 5400, + type: 'folder', + children: [ + { + id: '310', + name: 'Vacation.mp4', + sizeInKB: 108, + type: 'file', + extension: 'mp4', + mimeType: 'video/mp4', + }, + ], + }, + ], + }, + ]; + return Promise.resolve(nodes); +}; diff --git a/www/content/docs/reference/tree-api/index.page.md b/www/content/docs/reference/tree-api/index.page.md index fa13ad5d..8bdc0b7e 100644 --- a/www/content/docs/reference/tree-api/index.page.md +++ b/www/content/docs/reference/tree-api/index.page.md @@ -137,7 +137,7 @@ This works if the tree has selection enabled. See [tree selection](/docs/learn/t
- + > Toggles the node with the give node path. @@ -147,7 +147,7 @@ See related and + > Expands the node with the given node path. See related and methods. @@ -160,9 +160,14 @@ Expands the node. Does not affect other child nodes. + + +If `options.force` is `true`, the node will be expanded even if is `true` for the given node. + + - + > Collapses the node with the given node path. See related and methods. @@ -175,6 +180,11 @@ Collapses the node. Does not affect other child nodes. + + +If `options.force` is `true`, the node will be collapsed even if is `true` for the given node. + + diff --git a/www/content/docs/releases/index.page.md b/www/content/docs/releases/index.page.md index a8cd586e..06c552aa 100644 --- a/www/content/docs/releases/index.page.md +++ b/www/content/docs/releases/index.page.md @@ -3,6 +3,10 @@ title: Releases description: All releases | Infinite Table DataGrid for React --- +## 6.0.10 + +@milestone id="134" + ## 6.0.9 @milestone id="133"