From 39af92e71de18752e9b8ad2d5d7da700e2761204 Mon Sep 17 00:00:00 2001 From: radubrehar Date: Tue, 29 Oct 2024 13:53:58 +0200 Subject: [PATCH] fix tree selection bug and release version patch fixes #255 --- .../tests/table/treegrid/selection2.page.tsx | 182 ++++++++++++++++++ .../tests/table/treegrid/selection2.spec.ts | 21 ++ .../pages/tests/table/utils/DeepMap.spec.ts | 58 ++++++ .../DataSource/TreeSelectionState.ts | 3 +- source/src/utils/DeepMap/index.ts | 76 +++++++- www/content/docs/releases/index.page.md | 5 + 6 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 examples/src/pages/tests/table/treegrid/selection2.page.tsx create mode 100644 examples/src/pages/tests/table/treegrid/selection2.spec.ts diff --git a/examples/src/pages/tests/table/treegrid/selection2.page.tsx b/examples/src/pages/tests/table/treegrid/selection2.page.tsx new file mode 100644 index 00000000..50e95aed --- /dev/null +++ b/examples/src/pages/tests/table/treegrid/selection2.page.tsx @@ -0,0 +1,182 @@ +import { + DataSourceApi, + InfiniteTableColumn, + TreeDataSource, + TreeGrid, + TreeSelectionValue, +} 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', + defaultWidth: 500, + renderValue: ({ value, rowInfo }) => { + return ( +
+ {rowInfo.id} - {value} +
+ ); + }, + renderTreeIcon: true, + renderSelectionCheckBox: 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 defaultTreeSelection: TreeSelectionValue = { + defaultSelection: true, + deselectedPaths: [ + ['1', '10'], + ['3', '31'], + ], + selectedPaths: [['3']], +}; + +export default function App() { + const [dataSourceApi, setDataSourceApi] = + useState | null>(); + + return ( + <> + +
+ + +
+ + +
+ + ); +} + +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', + sizeInKB: 1000, + type: 'folder', + children: [ + { + id: '20', + name: 'unknown.txt', + sizeInKB: 100, + type: 'file', + }, + ], + }, + { + 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/examples/src/pages/tests/table/treegrid/selection2.spec.ts b/examples/src/pages/tests/table/treegrid/selection2.spec.ts new file mode 100644 index 00000000..631e7462 --- /dev/null +++ b/examples/src/pages/tests/table/treegrid/selection2.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from '@testing'; + +export default test.describe('TreeSelectionProp', () => { + test('when defined, makes selectionMode default to multi-row', async ({ + page, + }) => { + await page.waitForInfinite(); + + const headerCheckbox = await page.locator( + '.InfiniteHeader input[type="checkbox"]', + ); + expect( + await headerCheckbox?.evaluate((el) => { + return { + checked: (el as HTMLInputElement).checked, + indeterminate: (el as HTMLInputElement).indeterminate, + }; + }), + ).toEqual({ checked: false, indeterminate: true }); + }); +}); diff --git a/examples/src/pages/tests/table/utils/DeepMap.spec.ts b/examples/src/pages/tests/table/utils/DeepMap.spec.ts index 186c21f6..94412cf7 100644 --- a/examples/src/pages/tests/table/utils/DeepMap.spec.ts +++ b/examples/src/pages/tests/table/utils/DeepMap.spec.ts @@ -472,6 +472,64 @@ export default test.describe('DeepMap', () => { ]); }); + test('getUnnestedKeysStartingWith should work correctly - 1', () => { + let map = new DeepMap(); + + map.set(['3', '31'], true); + map.set(['1'], true); + map.set(['1', '10'], true); + map.set(['3'], true); + + expect(map.getUnnestedKeysStartingWith([], true)).toEqual([['1'], ['3']]); + expect(map.getKeysStartingWith([], true)).toEqual([ + ['3'], + ['3', '31'], + ['1'], + ['1', '10'], + ]); + + map = new DeepMap(); + + map.set(['3', '31'], true); + + map.set(['1', '10'], true); + map.set(['3'], true); + map.set(['1'], true); + + expect(map.getUnnestedKeysStartingWith([], true)).toEqual([['3'], ['1']]); + }); + + test.only('getUnnestedKeysStartingWith should work correctly - 2', () => { + let map = new DeepMap(); + + map.set(['3', '31', '300'], true); + + map.set(['1', '10'], true); + map.set(['3', '30'], true); + map.set(['3', '31', '400'], true); + map.set(['3', '31', '400', '200'], true); + map.set(['1'], true); + map.set(['1', '100'], true); + + const expected = [ + ['3', '31', '300'], + ['3', '30'], + ['3', '31', '400'], + ['1'], + ]; + expect(map.getUnnestedKeysStartingWith([], true)).toEqual(expected); + + map.set([], true); + + expect(map.getUnnestedKeysStartingWith([])).toEqual([[]]); + expect(map.getUnnestedKeysStartingWith([], true)).toEqual(expected); + expect(map.getUnnestedKeysStartingWith([], true, 1)).toEqual([['1']]); + expect(map.getUnnestedKeysStartingWith([], true, 2)).toEqual([ + ['3', '30'], + ['1'], + ]); + }); + test('visit depth first, with index', () => { const map = new DeepMap(); diff --git a/source/src/components/DataSource/TreeSelectionState.ts b/source/src/components/DataSource/TreeSelectionState.ts index cff66750..af5d1e53 100644 --- a/source/src/components/DataSource/TreeSelectionState.ts +++ b/source/src/components/DataSource/TreeSelectionState.ts @@ -192,7 +192,8 @@ export class TreeSelectionState { const { selectionMap } = this; const childPaths = selectionMap - .getKeysStartingWith(nodePath, true) + // todo this could be replaced with a .hasKeysUnder call (to be implemented in the deep map later) + .getUnnestedKeysStartingWith(nodePath, true) .sort(shortestToLongest); if (!childPaths.length) { diff --git a/source/src/utils/DeepMap/index.ts b/source/src/utils/DeepMap/index.ts index 3d8e3725..85b0b821 100644 --- a/source/src/utils/DeepMap/index.ts +++ b/source/src/utils/DeepMap/index.ts @@ -85,6 +85,77 @@ export class DeepMap { return result; } + getUnnestedKeysStartingWith( + keys: KeyType[], + excludeSelf?: boolean, + depthLimit?: number, + ): KeyType[][] { + const pairs: (Pair & { keys: KeyType[] })[] = []; + + const fn: (pair: Pair & { keys: KeyType[] }) => void = ( + pair, + ) => { + pairs.push(pair); + }; + + let currentMap = this.map; + let pair: Pair | undefined; + let stop = false; + + if (keys.length) { + for (let i = 0, len = keys.length; i < len; i++) { + const key = keys[i]; + + pair = currentMap.get(key); + + if (!pair || !pair.map) { + stop = true; + if (i === len - 1) { + // if on the last key + // we want to allow the if clause below to run and check if the value on the last + // pair is present + stop = true; + break; + } else { + return []; + } + } + + currentMap = pair.map; + } + } else { + if (!excludeSelf) { + const hasEmptyKey = currentMap.has(this.emptyKey); + if (hasEmptyKey) { + return [[]]; + } + } + } + + if (pair && pair.value !== undefined) { + if (!excludeSelf) { + fn({ ...pair, keys }); + stop = true; + } + } + if (stop) { + return pairs.sort(SORT_ASC_REVISION).map((pair) => pair.keys); + } + + this.visitWithNext( + keys, + (_value, keys, _i, _next, pair) => { + fn({ ...pair, keys }); + // don't call next to go deeper + }, + currentMap, + depthLimit, + excludeSelf, + ); + + return pairs.sort(SORT_ASC_REVISION).map((pair) => pair.keys); + } + getKeysStartingWith( keys: KeyType[], excludeSelf?: boolean, @@ -443,7 +514,8 @@ export class DeepMap { value: ValueType, keys: KeyType[], indexInGroup: number, - next?: VoidFn, + next: VoidFn | undefined, + pair: Pair, ) => void, currentMap: Map> = this.map, depthLimit?: number, @@ -488,7 +560,7 @@ export class DeepMap { : undefined; if (pair.hasOwnProperty('value')) { - fn(pair.value!, keys, i, next); + fn(pair.value!, keys, i, next, pair); i++; } else { next?.(); diff --git a/www/content/docs/releases/index.page.md b/www/content/docs/releases/index.page.md index be2dc9b0..340328fb 100644 --- a/www/content/docs/releases/index.page.md +++ b/www/content/docs/releases/index.page.md @@ -2,6 +2,11 @@ title: Releases description: All releases | Infinite Table DataGrid for React --- + +## 6.0.5 + +@milestone id="131" + ## 6.0.0 @milestone id="130"